mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-04-02 12:32:55 +00:00
Bug 889157 - Uplift Add-on SDK to Firefox r=me
This commit is contained in:
parent
a0301d1781
commit
7fdb9dfa1a
@ -22,12 +22,8 @@ Windows users using cmd.exe should instead run:
|
||||
|
||||
bin\activate.bat
|
||||
|
||||
Then run:
|
||||
|
||||
cfx docs
|
||||
|
||||
This should start a documentation server and open a web browser
|
||||
with further instructions.
|
||||
Then go to https://addons.mozilla.org/developers/docs/sdk/latest/dev-guide to
|
||||
browse the SDK documentation.
|
||||
|
||||
If you get an error when running cfx or have any other problems getting
|
||||
started, see the "Troubleshooting" guide at:
|
||||
|
@ -22,7 +22,7 @@ commands (for example `--help`). `cfx` supports the following global options:
|
||||
|
||||
"Command-specific options" are documented alongside the commands.
|
||||
|
||||
There are five supported cfx commands:
|
||||
There are four supported cfx commands:
|
||||
|
||||
<table>
|
||||
<colgroup>
|
||||
@ -30,15 +30,6 @@ There are five supported cfx commands:
|
||||
<col width="90%">
|
||||
</colgroup>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<a href="dev-guide/cfx-tool.html#cfx-docs"><code>cfx docs</code></a>
|
||||
</td>
|
||||
<td>
|
||||
Display the documentation for the SDK.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<a href="dev-guide/cfx-tool.html#cfx-init"><code>cfx init</code></a>
|
||||
@ -82,27 +73,6 @@ There are also a number of
|
||||
[internal commands](dev-guide/cfx-tool.html#internal-commands),
|
||||
which are more likely to be useful to SDK developers than to add-on developers.
|
||||
|
||||
## <a name="cfx-docs">cfx docs</a> ##
|
||||
|
||||
This command displays the documentation for the SDK. The documentation is
|
||||
shipped with the SDK in [Markdown](http://daringfireball.net/projects/markdown/)
|
||||
format. The first time this command is executed, and any time after the
|
||||
Markdown files on disk have changed, `cfx docs` will generate a set of HTML
|
||||
pages from them and launch a web browser to display them. If the Markdown files
|
||||
haven't changed, `cfx docs` just launches a browser initialized to the set of
|
||||
generated pages.
|
||||
|
||||
To regenerate the documentation associated with a single file, you can
|
||||
specify the file as an argument. For example:
|
||||
|
||||
<pre>
|
||||
cfx docs doc/dev-guide-source/addon-development/cfx-tool.md
|
||||
</pre>
|
||||
|
||||
This command will regenerate only the HTML page you're reading.
|
||||
This is useful if you're iteratively editing a single file, and don't want to wait for cfx to
|
||||
regenerate the complete documentation tree.
|
||||
|
||||
## <a name="cfx-init">cfx init</a> ##
|
||||
|
||||
Create a new directory called "my-addon", change into it, and run `cfx init`.
|
||||
@ -810,8 +780,7 @@ add-on whenever it is run.
|
||||
### cfx sdocs ###
|
||||
|
||||
Executing this command builds a static HTML version of the SDK documentation
|
||||
that can be hosted on a web server without the special application support
|
||||
required by `cfx docs`.
|
||||
that can be hosted on a web server.
|
||||
|
||||
#### Options ####
|
||||
|
||||
|
@ -5,42 +5,203 @@
|
||||
# console #
|
||||
|
||||
The `console` object enables your add-on to log messages. If you have started
|
||||
the host application for your add-on from the command line (for example, by
|
||||
executing `cfx run` or `cfx test`) then these messages appear in the command
|
||||
shell you used. If the add-on has been installed in the host application, then
|
||||
the messages appear in the host application's
|
||||
Firefox for your add-on from the command line with `cfx run` or `cfx test`
|
||||
then these messages appear in the command shell you used. If the add-on has
|
||||
been installed in Firefox, then the messages appear in the host application's
|
||||
[Error Console](https://developer.mozilla.org/en/Error_Console).
|
||||
|
||||
The `console` object has the following methods:
|
||||
If you're developing your add-on using the
|
||||
[Add-on Builder](https://builder.addons.mozilla.org/) or are using
|
||||
the [Extension Auto-installer](https://addons.mozilla.org/en-US/firefox/addon/autoinstaller/),
|
||||
then the add-on is installed in Firefox, meaning that messages will appear in
|
||||
the Error Console. But see the discussion of
|
||||
[logging levels](dev-guide/console.html#Logging Levels): by default, messages
|
||||
logged using `log()`, `info()`, `trace()`, or `warn()` won't be logged in
|
||||
these situations.
|
||||
|
||||
<code>console.**log**(*object*[, *object*, ...])</code>
|
||||
## Console Methods ##
|
||||
|
||||
Logs an informational message to the shell.
|
||||
All console methods except `exception()` and `trace()` accept one or
|
||||
more JavaScript objects as arguments and log them to the console.
|
||||
Depending on the console's underlying implementation and user interface,
|
||||
you may be able to introspect into the properties of non-primitive objects
|
||||
you may be able to examine the properties of non-primitive objects
|
||||
that are logged.
|
||||
|
||||
<code>console.**info**(*object*[, *object*, ...])</code>
|
||||
### <code>console.log(*object*[, *object*, ...])</code> ###
|
||||
|
||||
Logs the arguments to the console, preceded by "info:" and the name of your
|
||||
add-on:
|
||||
|
||||
console.log("This is an informational message");
|
||||
|
||||
<pre>
|
||||
info: my-addon: This is an informational message
|
||||
</pre>
|
||||
|
||||
### <code>console.info(*object*[, *object*, ...])</code> ###
|
||||
|
||||
A synonym for `console.log()`.
|
||||
|
||||
<code>console.**warn**(*object*[, *object*, ...])</code>
|
||||
### <code>console.warn(*object*[, *object*, ...])</code> ###
|
||||
|
||||
Logs a warning message.
|
||||
Logs the arguments to the console, preceded by "warn:" and the name of your
|
||||
add-on:
|
||||
|
||||
<code>console.**error**(*object*[, *object*, ...])</code>
|
||||
console.warn("This is a warning message");
|
||||
|
||||
Logs an error message.
|
||||
<pre>
|
||||
warn: my-addon: This is a warning message
|
||||
</pre>
|
||||
|
||||
<code>console.**debug**(*object*[, *object*, ...])</code>
|
||||
### <code>console.error(*object*[, *object*, ...])</code> ###
|
||||
|
||||
Logs a debug message.
|
||||
Logs the arguments to the console, preceded by "error:" and the name of your
|
||||
add-on:
|
||||
|
||||
<code>console.**exception**(*exception*)</code>
|
||||
console.error("This is an error message");
|
||||
|
||||
<pre>
|
||||
error: my-addon: This is an error message
|
||||
</pre>
|
||||
|
||||
### <code>console.debug(*object*[, *object*, ...])</code> ###
|
||||
|
||||
Logs the arguments to the console, preceded by "debug:" and the name of your
|
||||
add-on:
|
||||
|
||||
console.error("This is a debug message");
|
||||
|
||||
<pre>
|
||||
debug: my-addon: This is a debug message
|
||||
</pre>
|
||||
|
||||
### <code>console.exception(*exception*)</code> ###
|
||||
|
||||
Logs the given exception instance as an error, outputting information
|
||||
about the exception's stack traceback if one is available.
|
||||
|
||||
<code>console.**trace**()</code>
|
||||
try {
|
||||
doThing();
|
||||
} catch (e) {
|
||||
console.exception(e);
|
||||
}
|
||||
|
||||
Logs a stack trace at the point this function is called.
|
||||
function UserException(message) {
|
||||
this.message = message;
|
||||
this.name = "UserException";
|
||||
}
|
||||
|
||||
function doThing() {
|
||||
throw new UserException("Thing could not be done!");
|
||||
}
|
||||
|
||||
<pre>
|
||||
error: my-addon: An exception occurred.
|
||||
UserException: Thing could not be done!
|
||||
</pre>
|
||||
|
||||
### <code>console.trace()</code> ###
|
||||
|
||||
Logs a stack trace at the point the function is called.
|
||||
|
||||
<h2 id="Logging Levels">Logging Levels</h2>
|
||||
|
||||
Logging's useful, of course, especially during development. But the more
|
||||
logging there is, the more noise you see in the console output.
|
||||
Especially when debug logging shows up in a production environment, the
|
||||
noise can make it harder, not easier, to debug issues.
|
||||
|
||||
This is the problem that logging levels are designed to fix. The console
|
||||
defines a number of logging levels, from "more verbose" to "less verbose",
|
||||
and a number of different logging functions that correspond to these levels,
|
||||
which are arranged in order of "severity" from informational
|
||||
messages, through warnings, to errors.
|
||||
|
||||
At a given logging level, only calls to the corresponding functions and
|
||||
functions with a higher severity will have any effect.
|
||||
|
||||
For example, if the logging level is set to "info", then calls to `info()`,
|
||||
`log()`, `warn()`, and `error()` will all result in output being written.
|
||||
But if the logging level is "warn" then only calls to `warn()` and `error()`
|
||||
have any effect, and calls to `info()` and `log()` are simply discarded.
|
||||
|
||||
This means that the same code can be more verbose in a development
|
||||
environment than in a production environment - you just need to arrange for
|
||||
the appropriate logging level to be set.
|
||||
|
||||
The complete set of logging levels is given in the table below, along
|
||||
with the set of functions that will result in output at each level:
|
||||
|
||||
<table>
|
||||
<colgroup>
|
||||
<col width="10%">
|
||||
<col width="90%">
|
||||
</colgroup>
|
||||
|
||||
<tr>
|
||||
<th>Level</th>
|
||||
<th>Will log calls to:</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>all</td>
|
||||
<td>Any console method</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>debug</td>
|
||||
<td><code>debug()</code>, <code>log()</code>, <code>info()</code>, <code>trace()</code>, <code>warn()</code>, <code>exception()</code>, <code>error()</code></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>info</td>
|
||||
<td><code>log()</code>, <code>info()</code>, <code>trace()</code>, <code>warn()</code>, <code>exception()</code>, <code>error()</code></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>warn</td>
|
||||
<td><code>warn()</code>, <code>exception()</code>, <code>error()</code></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>error</td>
|
||||
<td><code>exception()</code>, <code>error()</code></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>off</td>
|
||||
<td>Nothing</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
### Setting the Logging Level ###
|
||||
|
||||
The logging level defaults to "error".
|
||||
|
||||
There are two system preferences that can be used to override this default:
|
||||
|
||||
* **extensions.sdk.console.logLevel**: if set, this determines the logging
|
||||
level for all installed SDK-based add-ons.
|
||||
|
||||
* **extensions.[extension-id].sdk.console.logLevel**: if set, this determines
|
||||
the logging level for the specified add-on. This overrides the global
|
||||
preference if both are set.
|
||||
|
||||
Both these preferences can be set programmatically using the
|
||||
[`preferences/service`](modules/sdk/preferences/service.html) API, or manually
|
||||
using [about:config](http://kb.mozillazine.org/About:config). The value for each
|
||||
preference is the desired logging level, given as a string.
|
||||
|
||||
When you run your add-on using `cfx run` or `cfx test`, the global
|
||||
**extensions.sdk.console.logLevel** preference is automatically set to "info".
|
||||
This means that calls to `console.log()` will appear in the console output.
|
||||
|
||||
When you install an add-on into Firefox, the logging level will be "error"
|
||||
by default (that is, unless you have set one of the two preferences). This
|
||||
means that messages written using `debug()`, `log()`, `info()`, `trace()`,
|
||||
and `warn()` will not appear in the console.
|
||||
|
||||
This includes add-ons being developed using the
|
||||
[Add-on Builder](https://builder.addons.mozilla.org/) or the
|
||||
[Extension Auto-installer](https://addons.mozilla.org/en-US/firefox/addon/autoinstaller/).
|
||||
|
@ -0,0 +1,177 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
# Cross-domain Content Scripts #
|
||||
|
||||
By default, content scripts don't have any cross-domain privileges.
|
||||
In particular, they can't:
|
||||
|
||||
* [access content hosted in an `iframe`, if that content is served from a different domain](dev-guide/guides/content-scripts/cross-domain.html#Cross-domain iframes)
|
||||
* [make cross-domain XMLHttpRequests](dev-guide/guides/content-scripts/cross-domain.html#Cross-domain XMLHttpRequest)
|
||||
|
||||
However, you can enable these features for specific domains
|
||||
by adding them to your add-on's [package.json](dev-guide/package-spec.html)
|
||||
under the `"cross-domain-content"` key, which itself lives under the
|
||||
`"permissions"` key:
|
||||
|
||||
<pre>
|
||||
"permissions": {
|
||||
"cross-domain-content": ["http://example.org/", "http://example.com/"]
|
||||
}
|
||||
</pre>
|
||||
|
||||
* The domains listed must include the scheme and fully qualified domain name,
|
||||
and these must exactly match the domains serving the content - so in the
|
||||
example above, the content script will not be allowed to access content
|
||||
served from `https://example.com/`.
|
||||
* Wildcards are not allowed.
|
||||
* This feature is currently only available for content scripts, not for page
|
||||
scripts included in HTML files shipped with your add-on.
|
||||
|
||||
## Cross-domain iframes ##
|
||||
|
||||
The following "main.js" creates a page-worker which loads a local HTML file
|
||||
called "page.html", attaches a content script called "page.js" to the
|
||||
page, waits for messages from the script, and logs the payload.
|
||||
|
||||
//main.js
|
||||
var data = require("sdk/self").data;
|
||||
|
||||
var pageWorker = require("sdk/page-worker").Page({
|
||||
contentURL: data.url("page.html"),
|
||||
contentScriptFile: data.url("page-script.js")
|
||||
});
|
||||
|
||||
pageWorker.on("message", function(message) {
|
||||
console.log(message);
|
||||
});
|
||||
|
||||
The "page.html" file embeds an iframe whose content is
|
||||
served from "http://en.m.wikipedia.org/":
|
||||
|
||||
<pre class="brush: html">
|
||||
<!doctype html>
|
||||
<!-- page.html -->
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<iframe id="wikipedia" src="http://en.m.wikipedia.org/"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
</pre>
|
||||
|
||||
The "page-script.js" file locates "Today's Featured Article" and sends its
|
||||
content to "main.js":
|
||||
|
||||
// page-script.js
|
||||
var iframe = window.document.getElementById("wikipedia");
|
||||
var todaysFeaturedArticle = iframe.contentWindow.document.getElementById("mp-tfa");
|
||||
self.postMessage(todaysFeaturedArticle.textContent);
|
||||
|
||||
For this to work, we need to add the `"cross-domain-content"` key to
|
||||
"package.json":
|
||||
|
||||
<pre>
|
||||
"permissions": {
|
||||
"cross-domain-content": ["http://en.m.wikipedia.org/"]
|
||||
}
|
||||
</pre>
|
||||
|
||||
The add-on should successfully retrieve the iframe's content.
|
||||
|
||||
## Cross-domain XMLHttpRequest ##
|
||||
|
||||
The following add-on creates a panel whose content is the summary weather
|
||||
forecast for [Shetland](https://en.wikipedia.org/wiki/Shetland).
|
||||
If you want to try it out, you'll need to
|
||||
[register](http://www.metoffice.gov.uk/datapoint/support/API)
|
||||
and get an API key.
|
||||
|
||||
The "main.js":
|
||||
|
||||
* creates a panel whose content is supplied by "panel.html" and
|
||||
adds a content script "panel-script.js" to it
|
||||
* sends the panel a "show" message when it is shown
|
||||
* attaches the panel to a widget
|
||||
|
||||
<!-- terminate Markdown list -->
|
||||
|
||||
// main.js
|
||||
var data = require("sdk/self").data;
|
||||
|
||||
var forecast_panel = require("sdk/panel").Panel({
|
||||
height: 50,
|
||||
contentURL: data.url("panel.html"),
|
||||
contentScriptFile: data.url("panel-script.js")
|
||||
});
|
||||
|
||||
forecast_panel.on("show", function(){
|
||||
forecast_panel.port.emit("show");
|
||||
});
|
||||
|
||||
require("sdk/widget").Widget({
|
||||
id: "forecast",
|
||||
label: "Weather Forecast",
|
||||
contentURL: "http://www.metoffice.gov.uk/favicon.ico",
|
||||
panel: forecast_panel
|
||||
});
|
||||
|
||||
The "panel.html" just includes a `<div>` block for the forecast:
|
||||
|
||||
<pre class="brush: html">
|
||||
<!doctype HTML>
|
||||
<!-- panel.html -->
|
||||
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<div id="forecast_summary"></div>
|
||||
</body>
|
||||
</html>
|
||||
</pre>
|
||||
|
||||
The "panel-script.js" uses [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest)
|
||||
to fetch the latest forecast:
|
||||
|
||||
// panel-script.js
|
||||
|
||||
var url = "http://datapoint.metoffice.gov.uk/public/data/txt/wxfcs/regionalforecast/json/500?key=YOUR-API-KEY";
|
||||
|
||||
self.port.on("show", function () {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open("GET", url, true);
|
||||
request.onload = function () {
|
||||
var jsonResponse = JSON.parse(request.responseText);
|
||||
var summary = getSummary(jsonResponse);
|
||||
var element = document.getElementById("forecast_summary");
|
||||
element.textContent = summary;
|
||||
};
|
||||
request.send();
|
||||
});
|
||||
|
||||
function getSummary(forecast) {
|
||||
return forecast.RegionalFcst.FcstPeriods.Period[0].Paragraph[0].$;
|
||||
}
|
||||
|
||||
|
||||
Finally, we need to add the `"cross-domain-content"` key to "package.json":
|
||||
|
||||
<pre>
|
||||
"permissions": {
|
||||
"cross-domain-content": ["http://datapoint.metoffice.gov.uk"]
|
||||
}
|
||||
</pre>
|
||||
|
||||
## Content Permissions and unsafeWindow ##
|
||||
|
||||
If you use `"cross-domain-content"`, then JavaScript values in content
|
||||
scripts will not be available from pages. Suppose your content script includes
|
||||
a line like:
|
||||
|
||||
// content-script.js:
|
||||
unsafeWindow.myCustomAPI = function () {};
|
||||
|
||||
If you have included the `"cross-domain-content"` key, when the page script
|
||||
tries to access `myCustomAPI` this will result in a "permission denied"
|
||||
exception.
|
@ -92,5 +92,7 @@ how to communicate between your add-on and its content scripts using the
|
||||
* [Communicating using <code>postMessage()</code>](dev-guide/guides/content-scripts/using-postmessage.html):
|
||||
how to communicate between your add-on and its content scripts using the
|
||||
<code>postMessage()</code> API
|
||||
* [Cross-domain Content Scripts](dev-guide/guides/content-scripts/cross-domain.html):
|
||||
how to enable a content script to interact with content served from other domains.
|
||||
* [Example](dev-guide/guides/content-scripts/reddit-example.html):
|
||||
a simple example add-on using content scripts
|
||||
|
@ -143,6 +143,12 @@ This page lists more theoretical in-depth articles about the SDK.
|
||||
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<h4><a href="dev-guide/guides/content-scripts/cross-domain.html">Cross-domain content scripts</a></h4>
|
||||
How to enable content scripts to interact with content served from different domains.
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<h4><a href="dev-guide/guides/content-scripts/reddit-example.html">Reddit example</a></h4>
|
||||
A simple add-on which uses content scripts.
|
||||
|
@ -53,6 +53,77 @@ accomplish most of what it needs using the supported APIs, then it might
|
||||
still be worth migrating: we'll add more supported APIs in future releases
|
||||
to meet important use cases.
|
||||
|
||||
## <a name="user-interface-components">User Interface Components</a>##
|
||||
|
||||
XUL-based add-ons typically implement a user interface using a combination
|
||||
of two techniques: XUL overlays and XUL windows.
|
||||
|
||||
### XUL Overlays ###
|
||||
|
||||
XUL overlays are used to modify existing windows such as the main browser
|
||||
window. In this way an extension can integrate its user interface into the
|
||||
browser: for example, adding menu items, buttons, and toolbars.
|
||||
|
||||
Because SDK-based extensions are restartless, they can't use XUL overlays. To
|
||||
add user interface components to the browser, there are a few different
|
||||
options. In order of complexity, the main options are:
|
||||
|
||||
* the SDK includes modules that implement some basic user interface
|
||||
components including [buttons](modules/sdk/widget.html),
|
||||
[dialogs](modules/sdk/panel.html), and
|
||||
[context menu items](modules/sdk/context-menu.html).
|
||||
|
||||
* there is a collection of
|
||||
[community-developed modules](https://github.com/mozilla/addon-sdk/wiki/Community-developed-modules)
|
||||
that includes various user interface components, including
|
||||
[toolbar buttons](https://github.com/voldsoftware/toolbarbutton-jplib) and
|
||||
[menu items](https://github.com/voldsoftware/menuitems-jplib).
|
||||
|
||||
* by using the SDK's
|
||||
[low-level APIs](dev-guide/guides/xul-migration.html#Using the Low-level APIs)
|
||||
you can directly modify the browser chrome.
|
||||
|
||||
### XUL Windows
|
||||
|
||||
XUL windows are used to define completely new windows to host user interface
|
||||
elements specific to the add-on.
|
||||
|
||||
The SDK generally expects you to specify your user interface using HTML, not
|
||||
XUL. However, you can include a
|
||||
[chrome.manifest file](https://developer.mozilla.org/en-US/docs/Chrome_Registration)
|
||||
in your add-on and it will be included in the generated XPI.
|
||||
|
||||
<ul class="tree">
|
||||
<li>my-addon
|
||||
<ul>
|
||||
<li class="highlight-tree-node">chrome
|
||||
<ul><li>content</li>
|
||||
<li>locale</li>
|
||||
<li>skin</li></ul>
|
||||
</li>
|
||||
<li class="highlight-tree-node">chrome.manifest</li>
|
||||
<li>data</li>
|
||||
<li>lib</li>
|
||||
<li>package.json</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
There are limitations on what you can do in this manifest file: for example,
|
||||
you can't register overlays, `resource:` URIs, or components. However, you
|
||||
can register a `chrome:` URI, with a skin and locale, and this means you
|
||||
can include XUL windows in an SDK-based add-on.
|
||||
|
||||
You can keep the "chrome.manifest" file in your add-on's root directory
|
||||
and create a directory there called "chrome". In that directory you can keep
|
||||
your "content", "locale", and "skin" subdirectories:
|
||||
|
||||
This allows you to refer to objects in these directories from "chrome.manifest" using a relative path, like "chrome/content".
|
||||
|
||||
This is provided only as a migration aid, and it's still a good idea to port XUL windows to HTML.
|
||||
|
||||
<div style="clear:both"></div>
|
||||
|
||||
## <a name="content-scripts">Content Scripts</a> ##
|
||||
|
||||
In a XUL-based add-on, code that uses XPCOM objects, code that manipulates
|
||||
@ -71,27 +142,8 @@ page script.
|
||||
|
||||
A XUL-based add-on will need to be reorganized to respect this distinction.
|
||||
|
||||
Suppose an add-on wants to make a cross-domain XMLHttpRequest based on some
|
||||
data extracted from a web page. In a XUL-based extension you would implement
|
||||
all this in a single script. An SDK-based equivalent would need to be
|
||||
structured like this:
|
||||
|
||||
* the main add-on code (1) attaches a content script to the page, and (2)
|
||||
registers a listener function for messages from the content script
|
||||
* the content script (3) extracts the data from the page and (4) sends
|
||||
it to the main add-on code in a message
|
||||
* the main add-on code (5) receives the message and (6) sends the request,
|
||||
using the SDK's [`request`](modules/sdk/request.html) API
|
||||
|
||||
<img class="image-center" src="static-files/media/xul-migration-cs.png"
|
||||
alt="Content script organization">
|
||||
|
||||
There are two related reasons for this design. The first is security: it
|
||||
reduces the risk that a malicious web page will be able to access privileged
|
||||
APIs. The second is the need to be compatible with the multi-process architecture
|
||||
planned for Firefox: after this is implemented in Firefox, all add-ons will
|
||||
need to use a similar pattern, so it's likely that a XUL-based add-on will
|
||||
need to be rewritten anyway.
|
||||
The main reason for this design is security: it reduces the risk that a
|
||||
malicious web page will be able to access privileged APIs.
|
||||
|
||||
There's much more information on content scripts in the
|
||||
[Working With Content Scripts](dev-guide/guides/content-scripts/index.html) guide.
|
||||
|
@ -179,9 +179,13 @@ directory the first time you run
|
||||
indicating whether or not the
|
||||
add-on supports private browsing. If this value is not <code>true</code>
|
||||
or is omitted, then the add-on will not see any private windows or
|
||||
objects, such as tabs, that are associated with private windows. See the
|
||||
documentation for the
|
||||
<a href="modules/sdk/private-browsing.html"><code>private-browsing</code> module</a>.</p>
|
||||
objects, such as tabs, that are associated with private windows. See the
|
||||
documentation for the
|
||||
<a href="modules/sdk/private-browsing.html"><code>private-browsing</code> module</a>.</p>
|
||||
<p><strong><code>cross-domain-content</code></strong>: a list of domains for
|
||||
which content scripts are given cross-domain privileges to access content in
|
||||
iframes or to make XMLHTTPRequests. See the documentation for
|
||||
<a href="dev-guide/guides/content-scripts/cross-domain.html">enabling cross-domain content scripts</a>.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<div id="cse" style="width: 100%;">Loading</div>
|
||||
<script src="http://www.google.com/jsapi" type="text/javascript"></script>
|
||||
<script src="https://www.google.com/jsapi" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
function parseQueryFromUrl () {
|
||||
var queryParamName = "q";
|
||||
@ -38,7 +38,7 @@ google.setOnLoadCallback(function() {
|
||||
}, true);
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" href="http://www.google.com/cse/style/look/default.css" type="text/css" />
|
||||
<link rel="stylesheet" href="https://www.google.com/cse/style/look/default.css" type="text/css" />
|
||||
|
||||
<style type="text/css">
|
||||
#cse table, #cse tr, #cse td {
|
||||
|
@ -21,39 +21,50 @@ This tutorial does double-duty. It describes the general method for
|
||||
using an external, third-party module in your add-on, and it
|
||||
describes how to add a menu item using the `menuitems` module in particular.
|
||||
|
||||
## Installing `menuitems` ##
|
||||
|
||||
First we'll download the `menuitems` package from
|
||||
[https://github.com/erikvold/menuitems-jplib](https://github.com/erikvold/menuitems-jplib/zipball/51080383cbb0fe2a05f8992a8aae890f4c014176).
|
||||
|
||||
Third-party packages like `menuitems` can be installed in three
|
||||
different places:
|
||||
|
||||
* in the `packages` directory under the SDK root. If you do this the package
|
||||
is available to any other add-ons you're developing using that SDK instance,
|
||||
and the package's documentation is visible through `cfx docs`.
|
||||
* in a `packages` directory you create under your add-on's root: if you
|
||||
do this, the package is only available to that add-on.
|
||||
* in a directory indicated using the `packages` key in
|
||||
your add-on's [package.json](dev-guide/package-spec.html). If you
|
||||
do this, you may not keep any packages in your add-on's `packages`
|
||||
directory, or they will not be found.
|
||||
|
||||
In this example we will install the package under the SDK root. From
|
||||
the SDK root directory, execute something like the following commands:
|
||||
First, create a new add-on. Make a directory called "clickme" wherever you
|
||||
like, navigate to it and run `cfx init`.
|
||||
|
||||
<pre>
|
||||
mkdir clickme
|
||||
cd clickme
|
||||
cfx init
|
||||
</pre>
|
||||
|
||||
The usual directory structure will be created:
|
||||
|
||||
<ul class="tree">
|
||||
<li>clickme
|
||||
<ul>
|
||||
<li>data</li>
|
||||
<li>docs
|
||||
<ul><li>main.md</li></ul>
|
||||
</li>
|
||||
<li>lib
|
||||
<ul><li>main.js</li></ul>
|
||||
</li>
|
||||
<li>package.json</li>
|
||||
<li>README.md</li>
|
||||
<li>tests
|
||||
<ul><li>test-main.js</li></ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div style="clear:both"></div>
|
||||
|
||||
## Installing `menuitems` ##
|
||||
|
||||
Create a directory under "clickme" called "packages".
|
||||
Then download the `menuitems` package from
|
||||
[https://github.com/erikvold/menuitems-jplib](https://github.com/erikvold/menuitems-jplib/zipball/51080383cbb0fe2a05f8992a8aae890f4c014176) and extract it into the "packages" directory you just created:
|
||||
|
||||
<pre>
|
||||
mkdir packages
|
||||
cd packages
|
||||
tar -xf ../erikvold-menuitems-jplib-d80630c.zip
|
||||
</pre>
|
||||
|
||||
Now if you run `cfx docs` you'll see a new section appear in the sidebar
|
||||
labeled "Third-Party APIs", which lists the modules in the `menuitems`
|
||||
package: this package contains a single module, also
|
||||
called `menuitems`.
|
||||
|
||||
Click on the module name and you'll see API documentation for the module.
|
||||
|
||||
## Module Dependencies ##
|
||||
|
||||
If third-party modules only depend on SDK modules, you can use them right
|
||||
@ -75,12 +86,9 @@ and adding it under the `packages` directory alongside `menuitems`.
|
||||
|
||||
## Using `menuitems` ##
|
||||
|
||||
We can use the `menuitems` module in exactly the same way we use built-in
|
||||
modules.
|
||||
|
||||
The documentation for the `menuitems` module tells us to we create a menu
|
||||
item using `MenuItem()`. Of the options accepted by `MenuItem()`, we'll use
|
||||
this minimal set:
|
||||
The [documentation for the `menuitems` module](https://github.com/erikvold/menuitems-jplib/blob/master/docs/menuitems.md)
|
||||
tells us to create a menu item using `MenuItem()`. Of the options
|
||||
accepted by `MenuItem()`, we'll use this minimal set:
|
||||
|
||||
* `id`: identifier for this menu item
|
||||
* `label`: text the item displays
|
||||
@ -89,9 +97,7 @@ this minimal set:
|
||||
* `insertbefore`: identifier for the item before which we want our item to
|
||||
appear
|
||||
|
||||
Next, create a new add-on. Make a directory called 'clickme' wherever you
|
||||
like, navigate to it and run `cfx init`. Open `lib/main.js` and add the
|
||||
following code:
|
||||
<!--comment to terminate Markdown list -->
|
||||
|
||||
var menuitem = require("menuitems").Menuitem({
|
||||
id: "clickme",
|
||||
@ -126,17 +132,6 @@ console.
|
||||
|
||||
## Caveats ##
|
||||
|
||||
Eventually we expect the availability of a rich set of third party packages
|
||||
will be one of the most valuable aspects of the SDK. Right now they're a great
|
||||
way to use features not supported by the supported APIs without the
|
||||
complexity of using the low-level APIs, but there are some caveats you should
|
||||
be aware of:
|
||||
|
||||
* our support for third party packages is still fairly immature. One
|
||||
consequence of this is that it's not always obvious where to find third-party
|
||||
packages, although the
|
||||
[Community Developed Modules](https://github.com/mozilla/addon-sdk/wiki/Community-developed-modules)
|
||||
page in the SDK's GitHub Wiki lists a number of packages.
|
||||
|
||||
* because third party modules typically use low-level APIs, they may be broken
|
||||
by new releases of Firefox.
|
||||
Third-party modules are a great way to use features not directly supported by
|
||||
the SDK, but because third party modules typically use low-level APIs,
|
||||
they may be broken by new releases of Firefox.
|
||||
|
@ -143,11 +143,6 @@ This is the `cfx` command-line program. It's your primary interface to the
|
||||
Add-on SDK. You use it to launch Firefox and test your add-on, package your
|
||||
add-on for distribution, view documentation, and run unit tests.
|
||||
|
||||
## cfx docs ##
|
||||
|
||||
If you're reading these documents online, try running `cfx docs`. This will
|
||||
build the documentation for the SDK and display it in a browser.
|
||||
|
||||
## Problems? ##
|
||||
|
||||
Try the [Troubleshooting](dev-guide/tutorials/troubleshooting.html)
|
||||
|
@ -61,6 +61,15 @@ If you've installed the add-on in Firefox, or you're running the
|
||||
add-on in the Add-on Builder, then the messages appear in Firefox's
|
||||
[Error Console](https://developer.mozilla.org/en/Error_Console).
|
||||
|
||||
But note that **by default, calls to `console.log()` will not result
|
||||
in any output in the Error Console for any installed add-ons**: this
|
||||
includes add-ons installed using the Add-on Builder or using tools
|
||||
like the
|
||||
[Extension Auto-installer](https://addons.mozilla.org/en-US/firefox/addon/autoinstaller/).
|
||||
|
||||
See ["Logging Levels"](dev-guide/console.html#Logging Levels)
|
||||
in the console reference documentation for more information on this.
|
||||
|
||||
## Learning More ##
|
||||
|
||||
For the complete `console` API, see its
|
||||
|
@ -312,7 +312,6 @@ Modules not yet supported in Firefox Mobile are
|
||||
- [loader/sandbox](modules/sdk/loader/sandbox.html)
|
||||
- [net/url](modules/sdk/net/url.html)
|
||||
- [net/xhr](modules/sdk/net/xhr.html)
|
||||
- [page-mod/match-pattern](modules/sdk/page-mod/match-pattern.html)
|
||||
- [platform/xpcom](modules/sdk/platform/xpcom.html)
|
||||
- [preferences/service](modules/sdk/preferences/service.html)
|
||||
- [system/environment](modules/sdk/system/environment.html)
|
||||
@ -330,6 +329,7 @@ Modules not yet supported in Firefox Mobile are
|
||||
- [util/collection](modules/sdk/util/collection.html)
|
||||
- [util/deprecate](modules/sdk/util/deprecate.html)
|
||||
- [util/list](modules/sdk/util/list.html)
|
||||
- [util/match-pattern](modules/sdk/util/match-pattern.html)
|
||||
- util/registry
|
||||
- [util/uuid](modules/sdk/util/uuid.html)
|
||||
- [window/utils](modules/sdk/window/utils.html)
|
||||
|
@ -52,7 +52,7 @@ alt="ietf.org eaten by page-mod" />
|
||||
## Specifying the Match Pattern ##
|
||||
|
||||
The match pattern uses the
|
||||
[`match-pattern`](modules/sdk/page-mod/match-pattern.html)
|
||||
[`match-pattern`](modules/sdk/util/match-pattern.html)
|
||||
syntax. You can pass a single match-pattern string, or an array.
|
||||
|
||||
## Keeping the Content Script in a Separate File ##
|
||||
|
@ -371,27 +371,6 @@ Next we'll repackage the geolocation module.
|
||||
* delete the "main.js" that `cfx` generated, and copy "geolocation.js"
|
||||
there instead.
|
||||
|
||||
### Documentation ###
|
||||
|
||||
If you document your modules, people who install your package and
|
||||
execute `cfx docs` will see the documentation
|
||||
integrated with the SDK's own documentation.
|
||||
|
||||
You can document the geolocation module by creating a file called
|
||||
"geolocation.md" in your package's "doc" directory. This file is also
|
||||
written in Markdown, although you can optionally use some
|
||||
[extended syntax](https://wiki.mozilla.org/Jetpack/SDK/Writing_Documentation#APIDoc_Syntax)
|
||||
to document APIs.
|
||||
|
||||
Try it:
|
||||
|
||||
* add a "geolocation.md" under "doc"
|
||||
* copy your geolocation package under the "packages" directory in the SDK root
|
||||
* execute `cfx docs`
|
||||
|
||||
Once `cfx docs` has finished, you should see a new entry appear in the
|
||||
sidebar called "Third-Party APIs", which lists the geolocation module.
|
||||
|
||||
### Editing "package.json" ###
|
||||
|
||||
The "package.json" file in your package's root directory contains metadata
|
||||
|
@ -121,7 +121,7 @@ exported by the `context-menu` module.
|
||||
the patterns. These are the same match pattern strings that you use with
|
||||
the <a href="modules/sdk/page-mod.html"><code>page-mod</code></a>
|
||||
<code>include</code> property.
|
||||
<a href="modules/sdk/page-mod/match-pattern.html">Read more about patterns</a>.
|
||||
<a href="modules/sdk/util/match-pattern.html">Read more about patterns</a>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -803,7 +803,7 @@ top-level context menu.
|
||||
Creates a context that matches pages with particular URLs. See Specifying
|
||||
Contexts above.
|
||||
@param matchPattern {string,array}
|
||||
A [match pattern](modules/sdk/page-mod/match-pattern.html) string, regexp or an
|
||||
A [match pattern](modules/sdk/util/match-pattern.html) string, regexp or an
|
||||
array of match pattern strings or regexps.
|
||||
</api>
|
||||
</api>
|
||||
|
@ -303,7 +303,7 @@ Creates a page-mod.
|
||||
});
|
||||
|
||||
You can specify a set of URLs using a
|
||||
[regular expression](modules/sdk/page-mod/match-pattern.html#Regular Expressions).
|
||||
[regular expression](modules/sdk/util/match-pattern.html#Regular Expressions).
|
||||
The pattern must match the entire URL, not just a subset, and has
|
||||
`global`, `ignoreCase`, and `multiline` disabled.
|
||||
|
||||
@ -321,7 +321,7 @@ Creates a page-mod.
|
||||
contentScript: 'window.alert("Page matches ruleset");'
|
||||
});
|
||||
|
||||
See the [match-pattern](modules/sdk/page-mod/match-pattern.html) module for
|
||||
See the [match-pattern](modules/sdk/util/match-pattern.html) module for
|
||||
a detailed description of match pattern syntax.
|
||||
|
||||
@prop [contentScriptFile] {string,array}
|
||||
|
@ -33,13 +33,6 @@ listening to its `show` and `hide` events.
|
||||
|
||||
Opening a panel will close an already opened panel.
|
||||
|
||||
<div class="warning">
|
||||
If your add-on has
|
||||
<a href="modules/sdk/private-browsing.html#Opting into private browsing">opted into private browsing</a>,
|
||||
then you can't use panels in your add-on. This is due to a platform bug which we expect to
|
||||
be fixed in Firefox 21.
|
||||
</div>
|
||||
|
||||
## Panel Content ##
|
||||
|
||||
The panel's content is specified as HTML, which is loaded from the URL
|
||||
@ -394,13 +387,9 @@ then the panel's text will be invisible on OS X although it looks fine on Ubuntu
|
||||
|
||||
## Private Browsing ##
|
||||
|
||||
If your add-on has
|
||||
If your add-on has not
|
||||
[opted into private browsing](modules/sdk/private-browsing.html#Opting into private browsing),
|
||||
then **you can't use panels in your add-on**. This is due to a platform bug which we expect to
|
||||
be fixed in Firefox 21.
|
||||
|
||||
If your add-on has not opted into private browsing, and it calls `panel.show()`
|
||||
when the currently active window is a
|
||||
and it calls `panel.show()` when the currently active window is a
|
||||
[private window](modules/sdk/private-browsing.html#Per-window private browsing),
|
||||
then the panel will not be shown.
|
||||
|
||||
|
@ -51,15 +51,10 @@ active window is a private browser window
|
||||
* the [`selection`](modules/sdk/selection.html) module will not include
|
||||
any selections made in private browser windows
|
||||
|
||||
Add-ons that have opted in:
|
||||
|
||||
* will see private windows, so they will need to
|
||||
Add-ons that have opted in will see private windows, so they will need to
|
||||
use the `private-browsing` module to check whether objects are private,
|
||||
so as to avoid storing data derived from such objects.
|
||||
|
||||
* will not be able to use panels in their code. This is due to a platform
|
||||
restriction which will be fixed in Firefox 21.
|
||||
|
||||
Additionally, add-ons that use low-level modules such as
|
||||
[`window/utils`](modules/sdk/window/utils.html) may see private browser
|
||||
windows with certain functions, even if they have not explicitly opted
|
||||
|
@ -7,27 +7,17 @@
|
||||
|
||||
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:
|
||||
## Module-level Operations ##
|
||||
|
||||
var tabs = require('sdk/tabs');
|
||||
for each (var tab in tabs)
|
||||
console.log(tab.title);
|
||||
|
||||
You can also access individual tabs by index:
|
||||
|
||||
var tabs = require('sdk/tabs');
|
||||
|
||||
tabs.on('ready', function () {
|
||||
console.log('first: ' + tabs[0].title);
|
||||
console.log('last: ' + tabs[tabs.length-1].title);
|
||||
});
|
||||
### Open a Tab ###
|
||||
|
||||
You can open a new tab, specifying various properties including location:
|
||||
|
||||
var tabs = require("sdk/tabs");
|
||||
tabs.open("http://www.example.com");
|
||||
|
||||
### Track Tabs ###
|
||||
|
||||
You can register event listeners to be notified when tabs open, close, finish
|
||||
loading DOM content, or are made active or inactive:
|
||||
|
||||
@ -43,6 +33,71 @@ loading DOM content, or are made active or inactive:
|
||||
console.log('tab is loaded', tab.title, tab.url)
|
||||
});
|
||||
|
||||
### Access Tabs ###
|
||||
|
||||
The module itself can be used as a list of all opened
|
||||
tabs across all windows. In particular, you can enumerate it:
|
||||
|
||||
var tabs = require('sdk/tabs');
|
||||
for each (var tab in tabs)
|
||||
console.log(tab.title);
|
||||
|
||||
You can also access individual tabs by index:
|
||||
|
||||
var tabs = require('sdk/tabs');
|
||||
|
||||
tabs.on('ready', function () {
|
||||
console.log('first: ' + tabs[0].title);
|
||||
console.log('last: ' + tabs[tabs.length-1].title);
|
||||
});
|
||||
|
||||
You can access the currently active tab:
|
||||
|
||||
var tabs = require('sdk/tabs');
|
||||
|
||||
tabs.on('activate', function () {
|
||||
console.log('active: ' + tabs.activeTab.url);
|
||||
});
|
||||
|
||||
## Tab-level Operations ##
|
||||
|
||||
### Track a Tab ###
|
||||
|
||||
Given a tab, you can register event listeners to be notified when the
|
||||
tab is closed, activated or deactivated, or when the page hosted by the
|
||||
tab is loaded or retrieved from the
|
||||
["back-forward cache"](https://developer.mozilla.org/en-US/docs/Working_with_BFCache):
|
||||
|
||||
var tabs = require("sdk/tabs");
|
||||
|
||||
function onOpen(tab) {
|
||||
console.log(tab.url + " is open");
|
||||
tab.on("pageshow", logShow);
|
||||
tab.on("activate", logActivate);
|
||||
tab.on("deactivate", logDeactivate);
|
||||
tab.on("close", logClose);
|
||||
}
|
||||
|
||||
function logShow(tab) {
|
||||
console.log(tab.url + " is loaded");
|
||||
}
|
||||
|
||||
function logActivate(tab) {
|
||||
console.log(tab.url + " is activated");
|
||||
}
|
||||
|
||||
function logDeactivate(tab) {
|
||||
console.log(tab.url + " is deactivated");
|
||||
}
|
||||
|
||||
function logClose(tab) {
|
||||
console.log(tab.url + " is closed");
|
||||
}
|
||||
|
||||
tabs.on('open', onOpen);
|
||||
|
||||
### Manipulate a Tab ###
|
||||
|
||||
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`
|
||||
@ -53,6 +108,8 @@ property you can load a new page in the tab:
|
||||
tab.url = "http://www.example.com";
|
||||
});
|
||||
|
||||
### Run Scripts in a Tab ###
|
||||
|
||||
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 (see the
|
||||
@ -162,25 +219,25 @@ 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.
|
||||
A callback function that will be registered for the 'open' event.
|
||||
This is an optional property.
|
||||
@prop [onClose] {function}
|
||||
A callback function that will be registered for 'close' event.
|
||||
A callback function that will be registered for the 'close' event.
|
||||
This is an optional property.
|
||||
@prop [onReady] {function}
|
||||
A callback function that will be registered for 'ready' event.
|
||||
A callback function that will be registered for the 'ready' event.
|
||||
This is an optional property.
|
||||
@prop [onLoad] {function}
|
||||
A callback function that will be registered for 'load' event.
|
||||
A callback function that will be registered for the 'load' event.
|
||||
This is an optional property.
|
||||
@prop [onPageShow] {function}
|
||||
A callback function that will be registered for 'pageshow' event.
|
||||
A callback function that will be registered for the 'pageshow' event.
|
||||
This is an optional property.
|
||||
@prop [onActivate] {function}
|
||||
A callback function that will be registered for 'activate' event.
|
||||
A callback function that will be registered for the 'activate' event.
|
||||
This is an optional property.
|
||||
@prop [onDeactivate] {function}
|
||||
A callback function that will be registered for 'deactivate' event.
|
||||
A callback function that will be registered for the 'deactivate' event.
|
||||
This is an optional property.
|
||||
</api>
|
||||
|
||||
@ -340,11 +397,12 @@ Listeners are passed the tab object.
|
||||
@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.
|
||||
equivalent to the
|
||||
[`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/Reference/Events/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.
|
||||
|
||||
@ -356,16 +414,19 @@ Listeners are passed the tab object.
|
||||
@event
|
||||
|
||||
This event is emitted when the page for the tab's content is loaded. It is
|
||||
equivalent to the `load` event for the given content page.
|
||||
equivalent to the
|
||||
[`load`](https://developer.mozilla.org/en-US/docs/Web/Reference/Events/load)
|
||||
event for the given content page.
|
||||
|
||||
A single tab will emit this event every time the page is loaded: so it will be
|
||||
emitted again if the tab's location changes or the content is reloaded.
|
||||
This event is similar to the [`ready`](modules/sdk/tabs.html#ready) event,
|
||||
except that it can be used for pages that do not have a `DOMContentLoaded`
|
||||
event, like images.
|
||||
|
||||
After this event has been emitted, all properties relating to the tab's
|
||||
content can be used.
|
||||
|
||||
This is fired after the `ready` event on DOM content pages and can be used
|
||||
for pages that do not have a `DOMContentLoaded` event, like images.
|
||||
content can be used. For pages that have a `DOMContentLoaded` event, `load`
|
||||
is fired after `ready`.
|
||||
|
||||
@argument {Tab}
|
||||
Listeners are passed the tab object.
|
||||
@ -374,23 +435,32 @@ Listeners are passed the tab object.
|
||||
<api name="pageshow">
|
||||
@event
|
||||
|
||||
This event is emitted when the page for the tab's content is potentially
|
||||
from the cache. It is equivilent to the [pageshow](https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/pageshow) event for the given
|
||||
content page.
|
||||
The `pageshow` event is emitted when the page for a tab's content is loaded.
|
||||
It is equivalent to the
|
||||
[`pageshow`](https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/pageshow)
|
||||
event for the given content page.
|
||||
|
||||
This event is similar to the [`load`](modules/sdk/tabs.html#load) and
|
||||
[`ready`](modules/sdk/tabs.html#ready) events, except unlike
|
||||
`load` and `ready`, `pageshow` is triggered if the page was retrieved from the
|
||||
[bfcache](https://developer.mozilla.org/en-US/docs/Working_with_BFCache).
|
||||
This means that if the user loads a page, loads a new page, then
|
||||
moves back to the previous page using the "Back" button,
|
||||
the `pageshow` event is emitted when the user moves back to the previous
|
||||
page, while the `load` and `ready` events are not.
|
||||
|
||||
This event is *not* emitted when the tab is made the active tab: to get
|
||||
notified about that, you need to listen to the
|
||||
[`activate`](modules/sdk/tabs.html#activate) event.
|
||||
|
||||
After this event has been emitted, all properties relating to the tab's
|
||||
content can be used.
|
||||
|
||||
While the `ready` and `load` events will not be fired when a user uses the back
|
||||
or forward buttons to navigate history, the `pageshow` event will be fired.
|
||||
If the `persisted` argument is true, then the contents were loaded from the
|
||||
bfcache.
|
||||
content can be used. It is emitted after `load` and `ready`.
|
||||
|
||||
@argument {Tab}
|
||||
Listeners are passed the tab object.
|
||||
@argument {persisted}
|
||||
Listeners are passed a boolean value indicating whether or not the page
|
||||
was loaded from the [bfcache](https://developer.mozilla.org/en-US/docs/Working_with_BFCache) or not.
|
||||
was loaded from the [bfcache](https://developer.mozilla.org/en-US/docs/Working_with_BFCache).
|
||||
</api>
|
||||
|
||||
<api name="activate">
|
||||
|
259
addon-sdk/source/doc/module-source/sdk/util/match-pattern.md
Normal file
259
addon-sdk/source/doc/module-source/sdk/util/match-pattern.md
Normal file
@ -0,0 +1,259 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.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="20%">
|
||||
<col width="25%">
|
||||
<col width="55%">
|
||||
</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`, `resource`, or `data`, use a scheme
|
||||
followed by an asterisk, as below.
|
||||
|
||||
<table>
|
||||
|
||||
<colgroup>
|
||||
<col width="20%">
|
||||
<col width="25%">
|
||||
<col width="55%">
|
||||
</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><br>
|
||||
<code>resource://me/my-addon/data/file.html</code><br>
|
||||
<code>data:text/html,Hi there</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="20%">
|
||||
<col width="25%">
|
||||
<col width="55%">
|
||||
</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="20%">
|
||||
<col width="25%">
|
||||
<col width="55%">
|
||||
</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://*`, and to match files loaded from your
|
||||
add-on's [data](modules/sdk/self.html#data) directory, use `resource://`.
|
||||
|
||||
<table>
|
||||
|
||||
<colgroup>
|
||||
<col width="20%">
|
||||
<col width="80%">
|
||||
</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>
|
||||
|
||||
<tr>
|
||||
<td><code>"resource://*"</code></td>
|
||||
<td><code>resource://my-addon-at-me-dot-org/my-addon/data/file.html</code></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><code>"data:*"</code></td>
|
||||
<td><code>data:text/html,Hi there</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("sdk/util/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("sdk/util/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>
|
@ -37,12 +37,7 @@ WindowTracker({
|
||||
// Augmenting the behavior of `hideChromeForLocation` method, as
|
||||
// suggested by https://developer.mozilla.org/en-US/docs/Hiding_browser_chrome
|
||||
XULBrowserWindow.hideChromeForLocation = function(url) {
|
||||
if (url.indexOf(addonURL) === 0) {
|
||||
let rest = url.substr(addonURL.length);
|
||||
return rest.length === 0 || ['#','?'].indexOf(rest.charAt(0)) > -1
|
||||
}
|
||||
|
||||
return hideChromeForLocation.call(this, url);
|
||||
return isAddonURL(url) || hideChromeForLocation.call(this, url);
|
||||
}
|
||||
},
|
||||
|
||||
@ -52,8 +47,16 @@ WindowTracker({
|
||||
}
|
||||
});
|
||||
|
||||
function isAddonURL(url) {
|
||||
if (url.indexOf(addonURL) === 0) {
|
||||
let rest = url.substr(addonURL.length);
|
||||
return ((rest.length === 0) || (['#','?'].indexOf(rest.charAt(0)) > -1));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function tabFilter(tab) {
|
||||
return getURI(tab) === addonURL;
|
||||
return isAddonURL(getURI(tab));
|
||||
}
|
||||
|
||||
function untrackTab(window, tab) {
|
||||
@ -62,7 +65,7 @@ function untrackTab(window, tab) {
|
||||
let { hideChromeForLocation } = windows(window);
|
||||
|
||||
if (hideChromeForLocation) {
|
||||
window.XULBrowserWindow.hideChromeForLocation = hideChromeForLocation;
|
||||
window.XULBrowserWindow.hideChromeForLocation = hideChromeForLocation.bind(window.XULBrowserWindow);
|
||||
windows(window).hideChromeForLocation = null;
|
||||
}
|
||||
|
||||
|
54
addon-sdk/source/lib/sdk/addon/events.js
Normal file
54
addon-sdk/source/lib/sdk/addon/events.js
Normal file
@ -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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'experimental'
|
||||
};
|
||||
|
||||
let { request: hostReq, response: hostRes } = require('./host');
|
||||
let { defer: async } = require('../lang/functional');
|
||||
let { defer } = require('../core/promise');
|
||||
let { emit: emitSync, on, off } = require('../event/core');
|
||||
let { uuid } = require('../util/uuid');
|
||||
let emit = async(emitSync);
|
||||
|
||||
// Map of IDs to deferreds
|
||||
let requests = new Map();
|
||||
|
||||
// May not be necessary to wrap this in `async`
|
||||
// once promises are async via bug 881047
|
||||
let receive = async(function ({data, id, error}) {
|
||||
let request = requests.get(id);
|
||||
if (request) {
|
||||
if (error) request.reject(error);
|
||||
else request.resolve(clone(data));
|
||||
requests.delete(id);
|
||||
}
|
||||
});
|
||||
on(hostRes, 'data', receive);
|
||||
|
||||
/*
|
||||
* Send is a helper to be used in client APIs to send
|
||||
* a request to host
|
||||
*/
|
||||
function send (eventName, data) {
|
||||
let id = uuid();
|
||||
let deferred = defer();
|
||||
requests.set(id, deferred);
|
||||
emit(hostReq, 'data', {
|
||||
id: id,
|
||||
data: clone(data),
|
||||
event: eventName
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
exports.send = send;
|
||||
|
||||
/*
|
||||
* Implement internal structured cloning algorithm in the future?
|
||||
* http://www.whatwg.org/specs/web-apps/current-work/multipage/common-dom-interfaces.html#internal-structured-cloning-algorithm
|
||||
*/
|
||||
function clone (obj) JSON.parse(JSON.stringify(obj || {}))
|
12
addon-sdk/source/lib/sdk/addon/host.js
Normal file
12
addon-sdk/source/lib/sdk/addon/host.js
Normal file
@ -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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
exports.request = {};
|
||||
exports.response = {};
|
@ -30,7 +30,7 @@ let create = map(windowCreate, function({target, data, type}) {
|
||||
return { target: target.document, type: type, data: data }
|
||||
});
|
||||
|
||||
function readStates({document}) {
|
||||
function streamEventsFrom({document}) {
|
||||
// Map supported event types to a streams of those events on the given
|
||||
// `window` for the inserted document and than merge these streams into
|
||||
// single form stream off all window state change events.
|
||||
@ -43,15 +43,15 @@ function readStates({document}) {
|
||||
return target instanceof Ci.nsIDOMDocument
|
||||
})
|
||||
}
|
||||
|
||||
exports.streamEventsFrom = streamEventsFrom;
|
||||
|
||||
let opened = windows(null, { includePrivate: true });
|
||||
let state = merge(opened.map(readStates));
|
||||
let state = merge(opened.map(streamEventsFrom));
|
||||
|
||||
|
||||
let futureReady = filter(windowEvents, function({type})
|
||||
type === "DOMContentLoaded");
|
||||
let futureWindows = map(futureReady, function({target}) target);
|
||||
let futureState = expand(futureWindows, readStates);
|
||||
let futureState = expand(futureWindows, streamEventsFrom);
|
||||
|
||||
exports.events = merge([insert, create, state, futureState]);
|
||||
|
43
addon-sdk/source/lib/sdk/content/utils.js
Normal file
43
addon-sdk/source/lib/sdk/content/utils.js
Normal file
@ -0,0 +1,43 @@
|
||||
/* -*- 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
let assetsURI = require("../self").data.url();
|
||||
let isArray = Array.isArray;
|
||||
|
||||
function isAddonContent({ contentURL }) {
|
||||
return typeof(contentURL) === "string" && contentURL.indexOf(assetsURI) === 0;
|
||||
}
|
||||
exports.isAddonContent = isAddonContent;
|
||||
|
||||
function hasContentScript({ contentScript, contentScriptFile }) {
|
||||
return (isArray(contentScript) ? contentScript.length > 0 :
|
||||
!!contentScript) ||
|
||||
(isArray(contentScriptFile) ? contentScriptFile.length > 0 :
|
||||
!!contentScriptFile);
|
||||
}
|
||||
exports.hasContentScript = hasContentScript;
|
||||
|
||||
function requiresAddonGlobal(model) {
|
||||
return isAddonContent(model) && !hasContentScript(model);
|
||||
}
|
||||
exports.requiresAddonGlobal = requiresAddonGlobal;
|
||||
|
||||
function getAttachEventType(model) {
|
||||
if (!model) return null;
|
||||
let when = model.contentScriptWhen;
|
||||
return requiresAddonGlobal(model) ? "document-element-inserted" :
|
||||
when === "start" ? "document-element-inserted" :
|
||||
when === "end" ? "load" :
|
||||
when === "ready" ? "DOMContentLoaded" :
|
||||
null;
|
||||
}
|
||||
exports.getAttachEventType = getAttachEventType;
|
||||
|
@ -15,7 +15,7 @@ const { URL, isValidURI } = require("./url");
|
||||
const { WindowTracker, browserWindowIterator } = require("./deprecated/window-utils");
|
||||
const { isBrowser, getInnerId } = require("./window/utils");
|
||||
const { Ci } = require("chrome");
|
||||
const { MatchPattern } = require("./page-mod/match-pattern");
|
||||
const { MatchPattern } = require("./util/match-pattern");
|
||||
const { Worker } = require("./content/worker");
|
||||
const { EventTarget } = require("./event/target");
|
||||
const { emit } = require('./event/core');
|
||||
|
@ -160,8 +160,12 @@ var Class = new function() {
|
||||
getDataProperties(prototype));
|
||||
|
||||
constructor.attributes = attributes;
|
||||
constructor.prototype = prototype;
|
||||
return freeze(constructor);
|
||||
Object.defineProperty(constructor, 'prototype', {
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: prototype
|
||||
});
|
||||
return constructor;
|
||||
};
|
||||
}
|
||||
Class.prototype = extend(null, obscure({
|
||||
|
@ -92,7 +92,7 @@ const eventEmitter = {
|
||||
*/
|
||||
_listeners: function listeners(type) {
|
||||
let events = this._events || (this._events = {});
|
||||
return events[type] || (events[type] = []);
|
||||
return (events.hasOwnProperty(type) && events[type]) || (events[type] = []);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -55,16 +55,29 @@ TestFinder.prototype = {
|
||||
function(suite) {
|
||||
// Load each test file as a main module in its own loader instance
|
||||
// `suite` is defined by cuddlefish/manifest.py:ManifestBuilder.build
|
||||
var loader = Loader(module);
|
||||
var module = cuddlefish.main(loader, suite);
|
||||
let loader = Loader(module);
|
||||
let suiteModule;
|
||||
|
||||
try {
|
||||
suiteModule = cuddlefish.main(loader, suite);
|
||||
}
|
||||
catch (e) {
|
||||
if (!/^Unsupported Application/.test(e.message))
|
||||
throw e;
|
||||
// If `Unsupported Application` error thrown during test,
|
||||
// skip the test suite
|
||||
suiteModule = {
|
||||
'test suite skipped': assert => assert.pass(e.message)
|
||||
};
|
||||
}
|
||||
|
||||
if (self.testInProcess)
|
||||
for each (let name in Object.keys(module).sort()) {
|
||||
for each (let name in Object.keys(suiteModule).sort()) {
|
||||
if(NOT_TESTS.indexOf(name) === -1 && filter(suite, name)) {
|
||||
tests.push({
|
||||
setup: module.setup,
|
||||
teardown: module.teardown,
|
||||
testFunction: module[name],
|
||||
setup: suiteModule.setup,
|
||||
teardown: suiteModule.teardown,
|
||||
testFunction: suiteModule[name],
|
||||
name: suite + "." + name
|
||||
});
|
||||
}
|
||||
|
500
addon-sdk/source/lib/sdk/fs/path.js
Normal file
500
addon-sdk/source/lib/sdk/fs/path.js
Normal file
@ -0,0 +1,500 @@
|
||||
// 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.
|
||||
|
||||
// Adapted version of:
|
||||
// https://github.com/joyent/node/blob/v0.11.3/lib/path.js
|
||||
|
||||
// Shim process global from node.
|
||||
var process = Object.create(require('../system'));
|
||||
process.cwd = process.pathFor.bind(process, 'CurProcD');
|
||||
|
||||
// Update original check in node `process.platform === 'win32'` since in SDK it's `winnt`.
|
||||
var isWindows = process.platform.indexOf('win') === 0;
|
||||
|
||||
|
||||
|
||||
// resolves . and .. elements in a path array with directory names there
|
||||
// must be no slashes, empty elements, or device names (c:\) in the array
|
||||
// (so also no leading and trailing slashes - it does not distinguish
|
||||
// relative and absolute paths)
|
||||
function normalizeArray(parts, allowAboveRoot) {
|
||||
// if the path tries to go above the root, `up` ends up > 0
|
||||
var up = 0;
|
||||
for (var i = parts.length - 1; i >= 0; i--) {
|
||||
var last = parts[i];
|
||||
if (last === '.') {
|
||||
parts.splice(i, 1);
|
||||
} else if (last === '..') {
|
||||
parts.splice(i, 1);
|
||||
up++;
|
||||
} else if (up) {
|
||||
parts.splice(i, 1);
|
||||
up--;
|
||||
}
|
||||
}
|
||||
|
||||
// if the path is allowed to go above the root, restore leading ..s
|
||||
if (allowAboveRoot) {
|
||||
for (; up--; up) {
|
||||
parts.unshift('..');
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
|
||||
if (isWindows) {
|
||||
// Regex to split a windows path into three parts: [*, device, slash,
|
||||
// tail] windows-only
|
||||
var splitDeviceRe =
|
||||
/^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/;
|
||||
|
||||
// Regex to split the tail part of the above into [*, dir, basename, ext]
|
||||
var splitTailRe =
|
||||
/^([\s\S]*?)((?:\.{1,2}|[^\\\/]+?|)(\.[^.\/\\]*|))(?:[\\\/]*)$/;
|
||||
|
||||
// Function to split a filename into [root, dir, basename, ext]
|
||||
// windows version
|
||||
var splitPath = function(filename) {
|
||||
// Separate device+slash from tail
|
||||
var result = splitDeviceRe.exec(filename),
|
||||
device = (result[1] || '') + (result[2] || ''),
|
||||
tail = result[3] || '';
|
||||
// Split the tail into dir, basename and extension
|
||||
var result2 = splitTailRe.exec(tail),
|
||||
dir = result2[1],
|
||||
basename = result2[2],
|
||||
ext = result2[3];
|
||||
return [device, dir, basename, ext];
|
||||
};
|
||||
|
||||
var normalizeUNCRoot = function(device) {
|
||||
return '\\\\' + device.replace(/^[\\\/]+/, '').replace(/[\\\/]+/g, '\\');
|
||||
};
|
||||
|
||||
// path.resolve([from ...], to)
|
||||
// windows version
|
||||
exports.resolve = function() {
|
||||
var resolvedDevice = '',
|
||||
resolvedTail = '',
|
||||
resolvedAbsolute = false;
|
||||
|
||||
for (var i = arguments.length - 1; i >= -1; i--) {
|
||||
var path;
|
||||
if (i >= 0) {
|
||||
path = arguments[i];
|
||||
} else if (!resolvedDevice) {
|
||||
path = process.cwd();
|
||||
} else {
|
||||
// Windows has the concept of drive-specific current working
|
||||
// directories. If we've resolved a drive letter but not yet an
|
||||
// absolute path, get cwd for that drive. We're sure the device is not
|
||||
// an unc path at this points, because unc paths are always absolute.
|
||||
path = process.env['=' + resolvedDevice];
|
||||
// Verify that a drive-local cwd was found and that it actually points
|
||||
// to our drive. If not, default to the drive's root.
|
||||
if (!path || path.substr(0, 3).toLowerCase() !==
|
||||
resolvedDevice.toLowerCase() + '\\') {
|
||||
path = resolvedDevice + '\\';
|
||||
}
|
||||
}
|
||||
|
||||
// Skip empty and invalid entries
|
||||
if (typeof path !== 'string') {
|
||||
throw new TypeError('Arguments to path.resolve must be strings');
|
||||
} else if (!path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = splitDeviceRe.exec(path),
|
||||
device = result[1] || '',
|
||||
isUnc = device && device.charAt(1) !== ':',
|
||||
isAbsolute = exports.isAbsolute(path),
|
||||
tail = result[3];
|
||||
|
||||
if (device &&
|
||||
resolvedDevice &&
|
||||
device.toLowerCase() !== resolvedDevice.toLowerCase()) {
|
||||
// This path points to another device so it is not applicable
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!resolvedDevice) {
|
||||
resolvedDevice = device;
|
||||
}
|
||||
if (!resolvedAbsolute) {
|
||||
resolvedTail = tail + '\\' + resolvedTail;
|
||||
resolvedAbsolute = isAbsolute;
|
||||
}
|
||||
|
||||
if (resolvedDevice && resolvedAbsolute) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert slashes to backslashes when `resolvedDevice` points to an UNC
|
||||
// root. Also squash multiple slashes into a single one where appropriate.
|
||||
if (isUnc) {
|
||||
resolvedDevice = normalizeUNCRoot(resolvedDevice);
|
||||
}
|
||||
|
||||
// At this point the path should be resolved to a full absolute path,
|
||||
// but handle relative paths to be safe (might happen when process.cwd()
|
||||
// fails)
|
||||
|
||||
// Normalize the tail path
|
||||
|
||||
function f(p) {
|
||||
return !!p;
|
||||
}
|
||||
|
||||
resolvedTail = normalizeArray(resolvedTail.split(/[\\\/]+/).filter(f),
|
||||
!resolvedAbsolute).join('\\');
|
||||
|
||||
return (resolvedDevice + (resolvedAbsolute ? '\\' : '') + resolvedTail) ||
|
||||
'.';
|
||||
};
|
||||
|
||||
// windows version
|
||||
exports.normalize = function(path) {
|
||||
var result = splitDeviceRe.exec(path),
|
||||
device = result[1] || '',
|
||||
isUnc = device && device.charAt(1) !== ':',
|
||||
isAbsolute = exports.isAbsolute(path),
|
||||
tail = result[3],
|
||||
trailingSlash = /[\\\/]$/.test(tail);
|
||||
|
||||
// If device is a drive letter, we'll normalize to lower case.
|
||||
if (device && device.charAt(1) === ':') {
|
||||
device = device[0].toLowerCase() + device.substr(1);
|
||||
}
|
||||
|
||||
// Normalize the tail path
|
||||
tail = normalizeArray(tail.split(/[\\\/]+/).filter(function(p) {
|
||||
return !!p;
|
||||
}), !isAbsolute).join('\\');
|
||||
|
||||
if (!tail && !isAbsolute) {
|
||||
tail = '.';
|
||||
}
|
||||
if (tail && trailingSlash) {
|
||||
tail += '\\';
|
||||
}
|
||||
|
||||
// Convert slashes to backslashes when `device` points to an UNC root.
|
||||
// Also squash multiple slashes into a single one where appropriate.
|
||||
if (isUnc) {
|
||||
device = normalizeUNCRoot(device);
|
||||
}
|
||||
|
||||
return device + (isAbsolute ? '\\' : '') + tail;
|
||||
};
|
||||
|
||||
// windows version
|
||||
exports.isAbsolute = function(path) {
|
||||
var result = splitDeviceRe.exec(path),
|
||||
device = result[1] || '',
|
||||
isUnc = device && device.charAt(1) !== ':';
|
||||
// UNC paths are always absolute
|
||||
return !!result[2] || isUnc;
|
||||
};
|
||||
|
||||
// windows version
|
||||
exports.join = function() {
|
||||
function f(p) {
|
||||
if (typeof p !== 'string') {
|
||||
throw new TypeError('Arguments to path.join must be strings');
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
var paths = Array.prototype.filter.call(arguments, f);
|
||||
var joined = paths.join('\\');
|
||||
|
||||
// Make sure that the joined path doesn't start with two slashes, because
|
||||
// normalize() will mistake it for an UNC path then.
|
||||
//
|
||||
// This step is skipped when it is very clear that the user actually
|
||||
// intended to point at an UNC path. This is assumed when the first
|
||||
// non-empty string arguments starts with exactly two slashes followed by
|
||||
// at least one more non-slash character.
|
||||
//
|
||||
// Note that for normalize() to treat a path as an UNC path it needs to
|
||||
// have at least 2 components, so we don't filter for that here.
|
||||
// This means that the user can use join to construct UNC paths from
|
||||
// a server name and a share name; for example:
|
||||
// path.join('//server', 'share') -> '\\\\server\\share\')
|
||||
if (!/^[\\\/]{2}[^\\\/]/.test(paths[0])) {
|
||||
joined = joined.replace(/^[\\\/]{2,}/, '\\');
|
||||
}
|
||||
|
||||
return exports.normalize(joined);
|
||||
};
|
||||
|
||||
// path.relative(from, to)
|
||||
// it will solve the relative path from 'from' to 'to', for instance:
|
||||
// from = 'C:\\orandea\\test\\aaa'
|
||||
// to = 'C:\\orandea\\impl\\bbb'
|
||||
// The output of the function should be: '..\\..\\impl\\bbb'
|
||||
// windows version
|
||||
exports.relative = function(from, to) {
|
||||
from = exports.resolve(from);
|
||||
to = exports.resolve(to);
|
||||
|
||||
// windows is not case sensitive
|
||||
var lowerFrom = from.toLowerCase();
|
||||
var lowerTo = to.toLowerCase();
|
||||
|
||||
function trim(arr) {
|
||||
var start = 0;
|
||||
for (; start < arr.length; start++) {
|
||||
if (arr[start] !== '') break;
|
||||
}
|
||||
|
||||
var end = arr.length - 1;
|
||||
for (; end >= 0; end--) {
|
||||
if (arr[end] !== '') break;
|
||||
}
|
||||
|
||||
if (start > end) return [];
|
||||
return arr.slice(start, end - start + 1);
|
||||
}
|
||||
|
||||
var toParts = trim(to.split('\\'));
|
||||
|
||||
var lowerFromParts = trim(lowerFrom.split('\\'));
|
||||
var lowerToParts = trim(lowerTo.split('\\'));
|
||||
|
||||
var length = Math.min(lowerFromParts.length, lowerToParts.length);
|
||||
var samePartsLength = length;
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (lowerFromParts[i] !== lowerToParts[i]) {
|
||||
samePartsLength = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (samePartsLength == 0) {
|
||||
return to;
|
||||
}
|
||||
|
||||
var outputParts = [];
|
||||
for (var i = samePartsLength; i < lowerFromParts.length; i++) {
|
||||
outputParts.push('..');
|
||||
}
|
||||
|
||||
outputParts = outputParts.concat(toParts.slice(samePartsLength));
|
||||
|
||||
return outputParts.join('\\');
|
||||
};
|
||||
|
||||
exports.sep = '\\';
|
||||
exports.delimiter = ';';
|
||||
|
||||
} else /* posix */ {
|
||||
|
||||
// Split a filename into [root, dir, basename, ext], unix version
|
||||
// 'root' is just a slash, or nothing.
|
||||
var splitPathRe =
|
||||
/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;
|
||||
var splitPath = function(filename) {
|
||||
return splitPathRe.exec(filename).slice(1);
|
||||
};
|
||||
|
||||
// path.resolve([from ...], to)
|
||||
// posix version
|
||||
exports.resolve = function() {
|
||||
var resolvedPath = '',
|
||||
resolvedAbsolute = false;
|
||||
|
||||
for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
|
||||
var path = (i >= 0) ? arguments[i] : process.cwd();
|
||||
|
||||
// Skip empty and invalid entries
|
||||
if (typeof path !== 'string') {
|
||||
throw new TypeError('Arguments to path.resolve must be strings');
|
||||
} else if (!path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
resolvedPath = path + '/' + resolvedPath;
|
||||
resolvedAbsolute = path.charAt(0) === '/';
|
||||
}
|
||||
|
||||
// At this point the path should be resolved to a full absolute path, but
|
||||
// handle relative paths to be safe (might happen when process.cwd() fails)
|
||||
|
||||
// Normalize the path
|
||||
resolvedPath = normalizeArray(resolvedPath.split('/').filter(function(p) {
|
||||
return !!p;
|
||||
}), !resolvedAbsolute).join('/');
|
||||
|
||||
return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.';
|
||||
};
|
||||
|
||||
// path.normalize(path)
|
||||
// posix version
|
||||
exports.normalize = function(path) {
|
||||
var isAbsolute = exports.isAbsolute(path),
|
||||
trailingSlash = path.substr(-1) === '/';
|
||||
|
||||
// Normalize the path
|
||||
path = normalizeArray(path.split('/').filter(function(p) {
|
||||
return !!p;
|
||||
}), !isAbsolute).join('/');
|
||||
|
||||
if (!path && !isAbsolute) {
|
||||
path = '.';
|
||||
}
|
||||
if (path && trailingSlash) {
|
||||
path += '/';
|
||||
}
|
||||
|
||||
return (isAbsolute ? '/' : '') + path;
|
||||
};
|
||||
|
||||
// posix version
|
||||
exports.isAbsolute = function(path) {
|
||||
return path.charAt(0) === '/';
|
||||
};
|
||||
|
||||
// posix version
|
||||
exports.join = function() {
|
||||
var paths = Array.prototype.slice.call(arguments, 0);
|
||||
return exports.normalize(paths.filter(function(p, index) {
|
||||
if (typeof p !== 'string') {
|
||||
throw new TypeError('Arguments to path.join must be strings');
|
||||
}
|
||||
return p;
|
||||
}).join('/'));
|
||||
};
|
||||
|
||||
|
||||
// path.relative(from, to)
|
||||
// posix version
|
||||
exports.relative = function(from, to) {
|
||||
from = exports.resolve(from).substr(1);
|
||||
to = exports.resolve(to).substr(1);
|
||||
|
||||
function trim(arr) {
|
||||
var start = 0;
|
||||
for (; start < arr.length; start++) {
|
||||
if (arr[start] !== '') break;
|
||||
}
|
||||
|
||||
var end = arr.length - 1;
|
||||
for (; end >= 0; end--) {
|
||||
if (arr[end] !== '') break;
|
||||
}
|
||||
|
||||
if (start > end) return [];
|
||||
return arr.slice(start, end - start + 1);
|
||||
}
|
||||
|
||||
var fromParts = trim(from.split('/'));
|
||||
var toParts = trim(to.split('/'));
|
||||
|
||||
var length = Math.min(fromParts.length, toParts.length);
|
||||
var samePartsLength = length;
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (fromParts[i] !== toParts[i]) {
|
||||
samePartsLength = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var outputParts = [];
|
||||
for (var i = samePartsLength; i < fromParts.length; i++) {
|
||||
outputParts.push('..');
|
||||
}
|
||||
|
||||
outputParts = outputParts.concat(toParts.slice(samePartsLength));
|
||||
|
||||
return outputParts.join('/');
|
||||
};
|
||||
|
||||
exports.sep = '/';
|
||||
exports.delimiter = ':';
|
||||
}
|
||||
|
||||
exports.dirname = function(path) {
|
||||
var result = splitPath(path),
|
||||
root = result[0],
|
||||
dir = result[1];
|
||||
|
||||
if (!root && !dir) {
|
||||
// No dirname whatsoever
|
||||
return '.';
|
||||
}
|
||||
|
||||
if (dir) {
|
||||
// It has a dirname, strip trailing slash
|
||||
dir = dir.substr(0, dir.length - 1);
|
||||
}
|
||||
|
||||
return root + dir;
|
||||
};
|
||||
|
||||
|
||||
exports.basename = function(path, ext) {
|
||||
var f = splitPath(path)[2];
|
||||
// TODO: make this comparison case-insensitive on windows?
|
||||
if (ext && f.substr(-1 * ext.length) === ext) {
|
||||
f = f.substr(0, f.length - ext.length);
|
||||
}
|
||||
return f;
|
||||
};
|
||||
|
||||
|
||||
exports.extname = function(path) {
|
||||
return splitPath(path)[3];
|
||||
};
|
||||
|
||||
if (isWindows) {
|
||||
exports._makeLong = function(path) {
|
||||
// Note: this will *probably* throw somewhere.
|
||||
if (typeof path !== 'string')
|
||||
return path;
|
||||
|
||||
if (!path) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var resolvedPath = exports.resolve(path);
|
||||
|
||||
if (/^[a-zA-Z]\:\\/.test(resolvedPath)) {
|
||||
// path is local filesystem path, which needs to be converted
|
||||
// to long UNC path.
|
||||
return '\\\\?\\' + resolvedPath;
|
||||
} else if (/^\\\\[^?.]/.test(resolvedPath)) {
|
||||
// path is network UNC path, which needs to be converted
|
||||
// to long UNC path.
|
||||
return '\\\\?\\UNC\\' + resolvedPath.substring(2);
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
} else {
|
||||
exports._makeLong = function(path) {
|
||||
return path;
|
||||
};
|
||||
}
|
82
addon-sdk/source/lib/sdk/io/buffer.js
Normal file
82
addon-sdk/source/lib/sdk/io/buffer.js
Normal file
@ -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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
|
||||
const { Cc, Ci, CC } = require("chrome");
|
||||
const { Class } = require("../core/heritage");
|
||||
|
||||
const Transcoder = CC("@mozilla.org/intl/scriptableunicodeconverter",
|
||||
"nsIScriptableUnicodeConverter");
|
||||
|
||||
var Buffer = Class({
|
||||
initialize: function initialize(subject, encoding) {
|
||||
subject = subject ? subject.valueOf() : 0;
|
||||
let length = typeof subject === "number" ? subject : 0;
|
||||
this.encoding = encoding || "utf-8";
|
||||
this.valueOf(Array.isArray(subject) ? subject : new Array(length));
|
||||
|
||||
if (typeof subject === "string") this.write(subject);
|
||||
},
|
||||
get length() {
|
||||
return this.valueOf().length;
|
||||
},
|
||||
get: function get(index) {
|
||||
return this.valueOf()[index];
|
||||
},
|
||||
set: function set(index, value) {
|
||||
return this.valueOf()[index] = value;
|
||||
},
|
||||
valueOf: function valueOf(value) {
|
||||
Object.defineProperty(this, "valueOf", {
|
||||
value: Array.prototype.valueOf.bind(value),
|
||||
configurable: false,
|
||||
writable: false,
|
||||
enumerable: false
|
||||
});
|
||||
},
|
||||
toString: function toString(encoding, start, end) {
|
||||
let bytes = this.valueOf().slice(start || 0, end || this.length);
|
||||
let transcoder = Transcoder();
|
||||
transcoder.charset = String(encoding || this.encoding).toUpperCase();
|
||||
return transcoder.convertFromByteArray(bytes, this.length);
|
||||
},
|
||||
toJSON: function toJSON() {
|
||||
return this.toString()
|
||||
},
|
||||
write: function write(string, offset, encoding) {
|
||||
offset = Math.max(offset || 0, 0);
|
||||
let value = this.valueOf();
|
||||
let transcoder = Transcoder();
|
||||
transcoder.charset = String(encoding || this.encoding).toUpperCase();
|
||||
let bytes = transcoder.convertToByteArray(string, {});
|
||||
value.splice.apply(value, [
|
||||
offset,
|
||||
Math.min(value.length - offset, bytes.length, bytes)
|
||||
].concat(bytes));
|
||||
return bytes;
|
||||
},
|
||||
slice: function slice(start, end) {
|
||||
return new Buffer(this.valueOf().slice(start, end));
|
||||
},
|
||||
copy: function copy(target, offset, start, end) {
|
||||
offset = Math.max(offset || 0, 0);
|
||||
target = target.valueOf();
|
||||
let bytes = this.valueOf();
|
||||
bytes.slice(Math.max(start || 0, 0), end);
|
||||
target.splice.apply(target, [
|
||||
offset,
|
||||
Math.min(target.length - offset, bytes.length),
|
||||
].concat(bytes));
|
||||
}
|
||||
});
|
||||
Buffer.isBuffer = function isBuffer(buffer) {
|
||||
return buffer instanceof Buffer
|
||||
};
|
||||
exports.Buffer = Buffer;
|
906
addon-sdk/source/lib/sdk/io/fs.js
Normal file
906
addon-sdk/source/lib/sdk/io/fs.js
Normal file
@ -0,0 +1,906 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { Cc, Ci, CC } = require("chrome");
|
||||
|
||||
const { setTimeout } = require("../timers");
|
||||
const { Stream, InputStream, OutputStream } = require("./stream");
|
||||
const { Buffer } = require("./buffer");
|
||||
const { ns } = require("../core/namespace");
|
||||
const { Class } = require("../core/heritage");
|
||||
|
||||
const nsILocalFile = CC("@mozilla.org/file/local;1", "nsILocalFile",
|
||||
"initWithPath");
|
||||
const FileOutputStream = CC("@mozilla.org/network/file-output-stream;1",
|
||||
"nsIFileOutputStream", "init");
|
||||
const FileInputStream = CC("@mozilla.org/network/file-input-stream;1",
|
||||
"nsIFileInputStream", "init");
|
||||
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
|
||||
"nsIBinaryInputStream", "setInputStream");
|
||||
const BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1",
|
||||
"nsIBinaryOutputStream", "setOutputStream");
|
||||
const StreamPump = CC("@mozilla.org/network/input-stream-pump;1",
|
||||
"nsIInputStreamPump", "init");
|
||||
|
||||
const { createOutputTransport, createInputTransport } =
|
||||
Cc["@mozilla.org/network/stream-transport-service;1"].
|
||||
getService(Ci.nsIStreamTransportService);
|
||||
|
||||
|
||||
const { REOPEN_ON_REWIND, DEFER_OPEN } = Ci.nsIFileInputStream;
|
||||
const { DIRECTORY_TYPE, NORMAL_FILE_TYPE } = Ci.nsIFile;
|
||||
const { NS_SEEK_SET, NS_SEEK_CUR, NS_SEEK_END } = Ci.nsISeekableStream;
|
||||
|
||||
const FILE_PERMISSION = parseInt("0666", 8);
|
||||
const PR_UINT32_MAX = 0xfffffff;
|
||||
// Values taken from:
|
||||
// http://mxr.mozilla.org/mozilla-central/source/nsprpub/pr/include/prio.h#615
|
||||
const PR_RDONLY = 0x01;
|
||||
const PR_WRONLY = 0x02;
|
||||
const PR_RDWR = 0x04;
|
||||
const PR_CREATE_FILE = 0x08;
|
||||
const PR_APPEND = 0x10;
|
||||
const PR_TRUNCATE = 0x20;
|
||||
const PR_SYNC = 0x40;
|
||||
const PR_EXCL = 0x80;
|
||||
|
||||
const FLAGS = {
|
||||
"r": PR_RDONLY,
|
||||
"r+": PR_RDWR,
|
||||
"w": PR_CREATE_FILE | PR_TRUNCATE | PR_WRONLY,
|
||||
"w+": PR_CREATE_FILE | PR_TRUNCATE | PR_RDWR,
|
||||
"a": PR_APPEND | PR_CREATE_FILE | PR_WRONLY,
|
||||
"a+": PR_APPEND | PR_CREATE_FILE | PR_RDWR
|
||||
};
|
||||
|
||||
function accessor() {
|
||||
let map = new WeakMap();
|
||||
return function(fd, value) {
|
||||
if (value === null) map.delete(fd);
|
||||
if (value !== undefined) map.set(fd, value);
|
||||
return map.get(fd);
|
||||
}
|
||||
}
|
||||
|
||||
let nsIFile = accessor();
|
||||
let nsIFileInputStream = accessor();
|
||||
let nsIFileOutputStream = accessor();
|
||||
let nsIBinaryInputStream = accessor();
|
||||
let nsIBinaryOutputStream = accessor();
|
||||
|
||||
// Just a contstant object used to signal that all of the file
|
||||
// needs to be read.
|
||||
const ALL = new String("Read all of the file");
|
||||
|
||||
function isWritable(mode) !!(mode & PR_WRONLY || mode & PR_RDWR)
|
||||
function isReadable(mode) !!(mode & PR_RDONLY || mode & PR_RDWR)
|
||||
|
||||
function isString(value) typeof(value) === "string"
|
||||
function isFunction(value) typeof(value) === "function"
|
||||
|
||||
function toArray(enumerator) {
|
||||
let value = [];
|
||||
while(enumerator.hasMoreElements())
|
||||
value.push(enumerator.getNext())
|
||||
return value
|
||||
}
|
||||
|
||||
function getFileName(file) file.QueryInterface(Ci.nsIFile).leafName
|
||||
|
||||
|
||||
function remove(path, recursive) {
|
||||
let fd = new nsILocalFile(path)
|
||||
if (fd.exists()) {
|
||||
fd.remove(recursive || false);
|
||||
}
|
||||
else {
|
||||
throw FSError("remove", "ENOENT", 34, path);
|
||||
}
|
||||
}
|
||||
|
||||
function Mode(mode, fallback) {
|
||||
return isString(mode) ? parseInt(mode) : mode || fallback;
|
||||
}
|
||||
function Flags(flag) {
|
||||
return !isString(flag) ? flag :
|
||||
FLAGS[flag] || Error("Unknown file open flag: " + flag);
|
||||
}
|
||||
|
||||
|
||||
function FSError(op, code, errno, path, file, line) {
|
||||
let error = Error(code + ", " + op + " " + path, file, line);
|
||||
error.code = code;
|
||||
error.path = path;
|
||||
error.errno = errno;
|
||||
return error;
|
||||
}
|
||||
|
||||
const ReadStream = Class({
|
||||
extends: InputStream,
|
||||
initialize: function initialize(path, options) {
|
||||
this.position = -1;
|
||||
this.length = -1;
|
||||
this.flags = "r";
|
||||
this.mode = FILE_PERMISSION;
|
||||
this.bufferSize = 64 * 1024;
|
||||
|
||||
options = options || {};
|
||||
|
||||
if ("flags" in options && options.flags)
|
||||
this.flags = options.flags;
|
||||
if ("bufferSize" in options && options.bufferSize)
|
||||
this.bufferSize = options.bufferSize;
|
||||
if ("length" in options && options.length)
|
||||
this.length = options.length;
|
||||
if ("position" in options && options.position !== undefined)
|
||||
this.position = options.position;
|
||||
|
||||
let { flags, mode, position, length } = this;
|
||||
let fd = isString(path) ? openSync(path, flags, mode) : path;
|
||||
this.fd = fd;
|
||||
|
||||
let input = nsIFileInputStream(fd);
|
||||
// Setting a stream position, unless it"s `-1` which means current position.
|
||||
if (position >= 0)
|
||||
input.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position);
|
||||
// We use `nsIStreamTransportService` service to transform blocking
|
||||
// file input stream into a fully asynchronous stream that can be written
|
||||
// without blocking the main thread.
|
||||
let transport = createInputTransport(input, position, length, false);
|
||||
// Open an input stream on a transport. We don"t pass flags to guarantee
|
||||
// non-blocking stream semantics. Also we use defaults for segment size &
|
||||
// count.
|
||||
let asyncInputStream = transport.openInputStream(null, 0, 0);
|
||||
let binaryInputStream = BinaryInputStream(asyncInputStream);
|
||||
nsIBinaryInputStream(fd, binaryInputStream);
|
||||
let pump = StreamPump(asyncInputStream, position, length, 0, 0, false);
|
||||
|
||||
InputStream.prototype.initialize.call(this, {
|
||||
input: binaryInputStream, pump: pump
|
||||
});
|
||||
this.read();
|
||||
},
|
||||
destroy: function() {
|
||||
closeSync(this.fd);
|
||||
InputStream.prototype.destroy.call(this);
|
||||
}
|
||||
});
|
||||
exports.ReadStream = ReadStream;
|
||||
exports.createReadStream = function createReadStream(path, options) {
|
||||
return new ReadStream(path, options);
|
||||
};
|
||||
|
||||
const WriteStream = Class({
|
||||
extends: OutputStream,
|
||||
initialize: function initialize(path, options) {
|
||||
this.drainable = true;
|
||||
this.flags = "w";
|
||||
this.position = -1;
|
||||
this.mode = FILE_PERMISSION;
|
||||
|
||||
options = options || {};
|
||||
|
||||
if ("flags" in options && options.flags)
|
||||
this.flags = options.flags;
|
||||
if ("mode" in options && options.mode)
|
||||
this.mode = options.mode;
|
||||
if ("position" in options && options.position !== undefined)
|
||||
this.position = options.position;
|
||||
|
||||
let { position, flags, mode } = this;
|
||||
// If pass was passed we create a file descriptor out of it. Otherwise
|
||||
// we just use given file descriptor.
|
||||
let fd = isString(path) ? openSync(path, flags, mode) : path;
|
||||
this.fd = fd;
|
||||
|
||||
let output = nsIFileOutputStream(fd);
|
||||
// Setting a stream position, unless it"s `-1` which means current position.
|
||||
if (position >= 0)
|
||||
output.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position);
|
||||
// We use `nsIStreamTransportService` service to transform blocking
|
||||
// file output stream into a fully asynchronous stream that can be written
|
||||
// without blocking the main thread.
|
||||
let transport = createOutputTransport(output, position, -1, false);
|
||||
// Open an output stream on a transport. We don"t pass flags to guarantee
|
||||
// non-blocking stream semantics. Also we use defaults for segment size &
|
||||
// count.
|
||||
let asyncOutputStream = transport.openOutputStream(null, 0, 0);
|
||||
// Finally we create a non-blocking binary output stream. This will allows
|
||||
// us to write buffers as byte arrays without any further transcoding.
|
||||
let binaryOutputStream = BinaryOutputStream(asyncOutputStream);
|
||||
nsIBinaryOutputStream(fd, binaryOutputStream);
|
||||
|
||||
// Storing output stream so that it can beaccessed later.
|
||||
OutputStream.prototype.initialize.call(this, {
|
||||
output: binaryOutputStream,
|
||||
asyncOutputStream: asyncOutputStream
|
||||
});
|
||||
},
|
||||
destroy: function() {
|
||||
closeSync(this.fd);
|
||||
OutputStream.prototype.destroy.call(this);
|
||||
}
|
||||
});
|
||||
exports.WriteStream = WriteStream;
|
||||
exports.createWriteStream = function createWriteStream(path, options) {
|
||||
return new WriteStream(path, options);
|
||||
};
|
||||
|
||||
const Stats = Class({
|
||||
initialize: function initialize(path) {
|
||||
let file = new nsILocalFile(path);
|
||||
if (!file.exists()) throw FSError("stat", "ENOENT", 34, path);
|
||||
nsIFile(this, file);
|
||||
},
|
||||
isDirectory: function() nsIFile(this).isDirectory(),
|
||||
isFile: function() nsIFile(this).isFile(),
|
||||
isSymbolicLink: function() nsIFile(this).isSymlink(),
|
||||
get mode() nsIFile(this).permissions,
|
||||
get size() nsIFile(this).fileSize,
|
||||
get mtime() nsIFile(this).lastModifiedTime,
|
||||
isBlockDevice: function() nsIFile(this).isSpecial(),
|
||||
isCharacterDevice: function() nsIFile(this).isSpecial(),
|
||||
isFIFO: function() nsIFile(this).isSpecial(),
|
||||
isSocket: function() nsIFile(this).isSpecial(),
|
||||
// non standard
|
||||
get exists() nsIFile(this).exists(),
|
||||
get hidden() nsIFile(this).isHidden(),
|
||||
get writable() nsIFile(this).isWritable(),
|
||||
get readable() nsIFile(this).isReadable()
|
||||
});
|
||||
exports.Stats = Stats;
|
||||
|
||||
const LStats = Class({
|
||||
extends: Stats,
|
||||
get size() this.isSymbolicLink() ? nsIFile(this).fileSizeOfLink :
|
||||
nsIFile(this).fileSize,
|
||||
get mtime() this.isSymbolicLink() ? nsIFile(this).lastModifiedTimeOfLink :
|
||||
nsIFile(this).lastModifiedTime,
|
||||
// non standard
|
||||
get permissions() this.isSymbolicLink() ? nsIFile(this).permissionsOfLink :
|
||||
nsIFile(this).permissions
|
||||
});
|
||||
|
||||
const FStat = Class({
|
||||
extends: Stats,
|
||||
initialize: function initialize(fd) {
|
||||
nsIFile(this, nsIFile(fd));
|
||||
}
|
||||
});
|
||||
|
||||
function noop() {}
|
||||
function Async(wrapped) {
|
||||
return function (path, callback) {
|
||||
let args = Array.slice(arguments);
|
||||
callback = args.pop();
|
||||
// If node is not given a callback argument
|
||||
// it just does not calls it.
|
||||
if (typeof(callback) !== "function") {
|
||||
args.push(callback);
|
||||
callback = noop;
|
||||
}
|
||||
setTimeout(function() {
|
||||
try {
|
||||
var result = wrapped.apply(this, args);
|
||||
if (result === undefined) callback(null);
|
||||
else callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Synchronous rename(2)
|
||||
*/
|
||||
function renameSync(oldPath, newPath) {
|
||||
let source = new nsILocalFile(oldPath);
|
||||
let target = new nsILocalFile(newPath);
|
||||
if (!source.exists()) throw FSError("rename", "ENOENT", 34, oldPath);
|
||||
return source.moveTo(target.parent, target.leafName);
|
||||
};
|
||||
exports.renameSync = renameSync;
|
||||
|
||||
/**
|
||||
* Asynchronous rename(2). No arguments other than a possible exception are
|
||||
* given to the completion callback.
|
||||
*/
|
||||
let rename = Async(renameSync);
|
||||
exports.rename = rename;
|
||||
|
||||
/**
|
||||
* Test whether or not the given path exists by checking with the file system.
|
||||
*/
|
||||
function existsSync(path) {
|
||||
return new nsILocalFile(path).exists();
|
||||
}
|
||||
exports.existsSync = existsSync;
|
||||
|
||||
let exists = Async(existsSync);
|
||||
exports.exists = exists;
|
||||
|
||||
/**
|
||||
* Synchronous ftruncate(2).
|
||||
*/
|
||||
function truncateSync(path, length) {
|
||||
let fd = openSync(path, "w");
|
||||
ftruncateSync(fd, length);
|
||||
closeSync(fd);
|
||||
}
|
||||
exports.truncateSync = truncateSync;
|
||||
|
||||
/**
|
||||
* Asynchronous ftruncate(2). No arguments other than a possible exception are
|
||||
* given to the completion callback.
|
||||
*/
|
||||
function truncate(path, length, callback) {
|
||||
open(path, "w", function(error, fd) {
|
||||
if (error) return callback(error);
|
||||
ftruncate(fd, length, function(error) {
|
||||
if (error) {
|
||||
closeSync(fd);
|
||||
callback(error);
|
||||
}
|
||||
else {
|
||||
close(fd, callback);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
exports.truncate = truncate;
|
||||
|
||||
function ftruncate(fd, length, callback) {
|
||||
write(fd, new Buffer(length), 0, length, 0, function(error) {
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
exports.ftruncate = ftruncate;
|
||||
|
||||
function ftruncateSync(fd, length) {
|
||||
writeSync(fd, new Buffer(length), 0, length, 0);
|
||||
}
|
||||
exports.ftruncateSync = ftruncateSync;
|
||||
|
||||
function chownSync(path, uid, gid) {
|
||||
throw Error("Not implemented yet!!");
|
||||
}
|
||||
exports.chownSync = chownSync;
|
||||
|
||||
let chown = Async(chownSync);
|
||||
exports.chown = chown;
|
||||
|
||||
function lchownSync(path, uid, gid) {
|
||||
throw Error("Not implemented yet!!");
|
||||
}
|
||||
exports.lchownSync = chownSync;
|
||||
|
||||
let lchown = Async(lchown);
|
||||
exports.lchown = lchown;
|
||||
|
||||
/**
|
||||
* Synchronous chmod(2).
|
||||
*/
|
||||
function chmodSync (path, mode) {
|
||||
throw Error("Not implemented yet!!");
|
||||
};
|
||||
exports.chmodSync = chmodSync;
|
||||
/**
|
||||
* Asynchronous chmod(2). No arguments other than a possible exception are
|
||||
* given to the completion callback.
|
||||
*/
|
||||
let chmod = Async(chmodSync);
|
||||
exports.chmod = chmod;
|
||||
|
||||
/**
|
||||
* Synchronous chmod(2).
|
||||
*/
|
||||
function fchmodSync(fd, mode) {
|
||||
throw Error("Not implemented yet!!");
|
||||
};
|
||||
exports.fchmodSync = fchmodSync;
|
||||
/**
|
||||
* Asynchronous chmod(2). No arguments other than a possible exception are
|
||||
* given to the completion callback.
|
||||
*/
|
||||
let fchmod = Async(fchmodSync);
|
||||
exports.chmod = fchmod;
|
||||
|
||||
|
||||
/**
|
||||
* Synchronous stat(2). Returns an instance of `fs.Stats`
|
||||
*/
|
||||
function statSync(path) {
|
||||
return new Stats(path);
|
||||
};
|
||||
exports.statSync = statSync;
|
||||
|
||||
/**
|
||||
* Asynchronous stat(2). The callback gets two arguments (err, stats) where
|
||||
* stats is a `fs.Stats` object. It looks like this:
|
||||
*/
|
||||
let stat = Async(statSync);
|
||||
exports.stat = stat;
|
||||
|
||||
/**
|
||||
* Synchronous lstat(2). Returns an instance of `fs.Stats`.
|
||||
*/
|
||||
function lstatSync(path) {
|
||||
return new LStats(path);
|
||||
};
|
||||
exports.lstatSync = lstatSync;
|
||||
|
||||
/**
|
||||
* Asynchronous lstat(2). The callback gets two arguments (err, stats) where
|
||||
* stats is a fs.Stats object. lstat() is identical to stat(), except that if
|
||||
* path is a symbolic link, then the link itself is stat-ed, not the file that
|
||||
* it refers to.
|
||||
*/
|
||||
let lstat = Async(lstatSync);
|
||||
exports.lstat = lstat;
|
||||
|
||||
/**
|
||||
* Synchronous fstat(2). Returns an instance of `fs.Stats`.
|
||||
*/
|
||||
function fstatSync(fd) {
|
||||
return new FStat(fd);
|
||||
};
|
||||
exports.fstatSync = fstatSync;
|
||||
|
||||
/**
|
||||
* Asynchronous fstat(2). The callback gets two arguments (err, stats) where
|
||||
* stats is a fs.Stats object.
|
||||
*/
|
||||
let fstat = Async(fstatSync);
|
||||
exports.fstat = fstat;
|
||||
|
||||
/**
|
||||
* Synchronous link(2).
|
||||
*/
|
||||
function linkSync(source, target) {
|
||||
throw Error("Not implemented yet!!");
|
||||
};
|
||||
exports.linkSync = linkSync;
|
||||
|
||||
/**
|
||||
* Asynchronous link(2). No arguments other than a possible exception are given
|
||||
* to the completion callback.
|
||||
*/
|
||||
let link = Async(linkSync);
|
||||
exports.link = link;
|
||||
|
||||
/**
|
||||
* Synchronous symlink(2).
|
||||
*/
|
||||
function symlinkSync(source, target) {
|
||||
throw Error("Not implemented yet!!");
|
||||
};
|
||||
exports.symlinkSync = symlinkSync;
|
||||
|
||||
/**
|
||||
* Asynchronous symlink(2). No arguments other than a possible exception are
|
||||
* given to the completion callback.
|
||||
*/
|
||||
let symlink = Async(symlinkSync);
|
||||
exports.symlink = symlink;
|
||||
|
||||
/**
|
||||
* Synchronous readlink(2). Returns the resolved path.
|
||||
*/
|
||||
function readlinkSync(path) {
|
||||
return new nsILocalFile(path).target;
|
||||
};
|
||||
exports.readlinkSync = readlinkSync;
|
||||
|
||||
/**
|
||||
* Asynchronous readlink(2). The callback gets two arguments
|
||||
* `(error, resolvedPath)`.
|
||||
*/
|
||||
let readlink = Async(readlinkSync);
|
||||
exports.readlink = readlink;
|
||||
|
||||
/**
|
||||
* Synchronous realpath(2). Returns the resolved path.
|
||||
*/
|
||||
function realpathSync(path) {
|
||||
return new nsILocalFile(path).path;
|
||||
};
|
||||
exports.realpathSync = realpathSync;
|
||||
|
||||
/**
|
||||
* Asynchronous realpath(2). The callback gets two arguments
|
||||
* `(err, resolvedPath)`.
|
||||
*/
|
||||
let realpath = Async(realpathSync);
|
||||
exports.realpath = realpath;
|
||||
|
||||
/**
|
||||
* Synchronous unlink(2).
|
||||
*/
|
||||
let unlinkSync = remove;
|
||||
exports.unlinkSync = unlinkSync;
|
||||
|
||||
/**
|
||||
* Asynchronous unlink(2). No arguments other than a possible exception are
|
||||
* given to the completion callback.
|
||||
*/
|
||||
let unlink = Async(remove);
|
||||
exports.unlink = unlink;
|
||||
|
||||
/**
|
||||
* Synchronous rmdir(2).
|
||||
*/
|
||||
let rmdirSync = remove;
|
||||
exports.rmdirSync = rmdirSync;
|
||||
|
||||
/**
|
||||
* Asynchronous rmdir(2). No arguments other than a possible exception are
|
||||
* given to the completion callback.
|
||||
*/
|
||||
let rmdir = Async(rmdirSync);
|
||||
exports.rmdir = rmdir;
|
||||
|
||||
/**
|
||||
* Synchronous mkdir(2).
|
||||
*/
|
||||
function mkdirSync(path, mode) {
|
||||
try {
|
||||
return nsILocalFile(path).create(DIRECTORY_TYPE, Mode(mode));
|
||||
} catch (error) {
|
||||
// Adjust exception thorw to match ones thrown by node.
|
||||
if (error.name === "NS_ERROR_FILE_ALREADY_EXISTS") {
|
||||
let { fileName, lineNumber } = error;
|
||||
error = FSError("mkdir", "EEXIST", 47, path, fileName, lineNumber);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
exports.mkdirSync = mkdirSync;
|
||||
|
||||
/**
|
||||
* Asynchronous mkdir(2). No arguments other than a possible exception are
|
||||
* given to the completion callback.
|
||||
*/
|
||||
let mkdir = Async(mkdirSync);
|
||||
exports.mkdir = mkdir;
|
||||
|
||||
/**
|
||||
* Synchronous readdir(3). Returns an array of filenames excluding `"."` and
|
||||
* `".."`.
|
||||
*/
|
||||
function readdirSync(path) {
|
||||
try {
|
||||
return toArray(new nsILocalFile(path).directoryEntries).map(getFileName);
|
||||
}
|
||||
catch (error) {
|
||||
// Adjust exception thorw to match ones thrown by node.
|
||||
if (error.name === "NS_ERROR_FILE_TARGET_DOES_NOT_EXIST" ||
|
||||
error.name === "NS_ERROR_FILE_NOT_FOUND")
|
||||
{
|
||||
let { fileName, lineNumber } = error;
|
||||
error = FSError("readdir", "ENOENT", 34, path, fileName, lineNumber);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
exports.readdirSync = readdirSync;
|
||||
|
||||
/**
|
||||
* Asynchronous readdir(3). Reads the contents of a directory. The callback
|
||||
* gets two arguments `(error, files)` where `files` is an array of the names
|
||||
* of the files in the directory excluding `"."` and `".."`.
|
||||
*/
|
||||
let readdir = Async(readdirSync);
|
||||
exports.readdir = readdir;
|
||||
|
||||
/**
|
||||
* Synchronous close(2).
|
||||
*/
|
||||
function closeSync(fd) {
|
||||
let input = nsIFileInputStream(fd);
|
||||
let output = nsIFileOutputStream(fd);
|
||||
|
||||
// Closing input stream and removing reference.
|
||||
if (input) input.close();
|
||||
// Closing output stream and removing reference.
|
||||
if (output) output.close();
|
||||
|
||||
nsIFile(fd, null);
|
||||
nsIFileInputStream(fd, null);
|
||||
nsIFileOutputStream(fd, null);
|
||||
nsIBinaryInputStream(fd, null);
|
||||
nsIBinaryOutputStream(fd, null);
|
||||
};
|
||||
exports.closeSync = closeSync;
|
||||
/**
|
||||
* Asynchronous close(2). No arguments other than a possible exception are
|
||||
* given to the completion callback.
|
||||
*/
|
||||
let close = Async(closeSync);
|
||||
exports.close = close;
|
||||
|
||||
/**
|
||||
* Synchronous open(2).
|
||||
*/
|
||||
function openSync(path, flags, mode) {
|
||||
let [ fd, flags, mode, file ] =
|
||||
[ { path: path }, Flags(flags), Mode(mode), nsILocalFile(path) ];
|
||||
|
||||
// If trying to open file for just read that does not exists
|
||||
// need to throw exception as node does.
|
||||
if (!file.exists() && !isWritable(flags))
|
||||
throw FSError("open", "ENOENT", 34, path);
|
||||
|
||||
// If we want to open file in read mode we initialize input stream.
|
||||
if (isReadable(flags)) {
|
||||
let input = FileInputStream(file, flags, mode, DEFER_OPEN);
|
||||
nsIFileInputStream(fd, input);
|
||||
}
|
||||
|
||||
// If we want to open file in write mode we initialize output stream for it.
|
||||
if (isWritable(flags)) {
|
||||
let output = FileOutputStream(file, flags, mode, DEFER_OPEN);
|
||||
nsIFileOutputStream(fd, output);
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
exports.openSync = openSync;
|
||||
/**
|
||||
* Asynchronous file open. See open(2). Flags can be
|
||||
* `"r", "r+", "w", "w+", "a"`, or `"a+"`. mode defaults to `0666`.
|
||||
* The callback gets two arguments `(error, fd).
|
||||
*/
|
||||
let open = Async(openSync);
|
||||
exports.open = open;
|
||||
|
||||
/**
|
||||
* Synchronous version of buffer-based fs.write(). Returns the number of bytes
|
||||
* written.
|
||||
*/
|
||||
function writeSync(fd, buffer, offset, length, position) {
|
||||
if (length + offset > buffer.length) {
|
||||
throw Error("Length is extends beyond buffer");
|
||||
}
|
||||
else if (length + offset !== buffer.length) {
|
||||
buffer = buffer.slice(offset, offset + length);
|
||||
}
|
||||
let writeStream = new WriteStream(fd, { position: position,
|
||||
length: length });
|
||||
let output = nsIBinaryOutputStream(fd);
|
||||
// We write content as a byte array as this will avoid any transcoding
|
||||
// if content was a buffer.
|
||||
output.writeByteArray(buffer.valueOf(), buffer.length);
|
||||
output.flush();
|
||||
};
|
||||
exports.writeSync = writeSync;
|
||||
|
||||
/**
|
||||
* Write buffer to the file specified by fd.
|
||||
*
|
||||
* `offset` and `length` determine the part of the buffer to be written.
|
||||
*
|
||||
* `position` refers to the offset from the beginning of the file where this
|
||||
* data should be written. If `position` is `null`, the data will be written
|
||||
* at the current position. See pwrite(2).
|
||||
*
|
||||
* The callback will be given three arguments `(error, written, buffer)` where
|
||||
* written specifies how many bytes were written into buffer.
|
||||
*
|
||||
* Note that it is unsafe to use `fs.write` multiple times on the same file
|
||||
* without waiting for the callback.
|
||||
*/
|
||||
function write(fd, buffer, offset, length, position, callback) {
|
||||
if (!Buffer.isBuffer(buffer)) {
|
||||
// (fd, data, position, encoding, callback)
|
||||
let encoding = null;
|
||||
[ position, encoding, callback ] = Array.slice(arguments, 1);
|
||||
buffer = new Buffer(String(buffer), encoding);
|
||||
offset = 0;
|
||||
} else if (length + offset > buffer.length) {
|
||||
throw Error("Length is extends beyond buffer");
|
||||
} else if (length + offset !== buffer.length) {
|
||||
buffer = buffer.slice(offset, offset + length);
|
||||
}
|
||||
|
||||
let writeStream = new WriteStream(fd, { position: position,
|
||||
length: length });
|
||||
writeStream.on("error", callback);
|
||||
writeStream.write(buffer, function onEnd() {
|
||||
writeStream.destroy();
|
||||
if (callback)
|
||||
callback(null, buffer.length, buffer);
|
||||
});
|
||||
};
|
||||
exports.write = write;
|
||||
|
||||
/**
|
||||
* Synchronous version of string-based fs.read. Returns the number of
|
||||
* bytes read.
|
||||
*/
|
||||
function readSync(fd, buffer, offset, length, position) {
|
||||
let input = nsIFileInputStream(fd);
|
||||
// Setting a stream position, unless it"s `-1` which means current position.
|
||||
if (position >= 0)
|
||||
input.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position);
|
||||
// We use `nsIStreamTransportService` service to transform blocking
|
||||
// file input stream into a fully asynchronous stream that can be written
|
||||
// without blocking the main thread.
|
||||
let binaryInputStream = BinaryInputStream(input);
|
||||
let count = length === ALL ? binaryInputStream.available() : length;
|
||||
var bytes = binaryInputStream.readByteArray(count);
|
||||
buffer.copy.call(bytes, buffer, offset);
|
||||
|
||||
return bytes;
|
||||
};
|
||||
exports.readSync = readSync;
|
||||
|
||||
/**
|
||||
* Read data from the file specified by `fd`.
|
||||
*
|
||||
* `buffer` is the buffer that the data will be written to.
|
||||
* `offset` is offset within the buffer where writing will start.
|
||||
*
|
||||
* `length` is an integer specifying the number of bytes to read.
|
||||
*
|
||||
* `position` is an integer specifying where to begin reading from in the file.
|
||||
* If `position` is `null`, data will be read from the current file position.
|
||||
*
|
||||
* The callback is given the three arguments, `(error, bytesRead, buffer)`.
|
||||
*/
|
||||
function read(fd, buffer, offset, length, position, callback) {
|
||||
let bytesRead = 0;
|
||||
let readStream = new ReadStream(fd, { position: position, length: length });
|
||||
readStream.on("data", function onData(chunck) {
|
||||
chunck.copy(buffer, offset + bytesRead);
|
||||
bytesRead += chunck.length;
|
||||
});
|
||||
readStream.on("end", function onEnd() {
|
||||
callback(null, bytesRead, buffer);
|
||||
readStream.destroy();
|
||||
});
|
||||
};
|
||||
exports.read = read;
|
||||
|
||||
/**
|
||||
* Asynchronously reads the entire contents of a file.
|
||||
* The callback is passed two arguments `(error, data)`, where data is the
|
||||
* contents of the file.
|
||||
*/
|
||||
function readFile(path, encoding, callback) {
|
||||
if (isFunction(encoding)) {
|
||||
callback = encoding
|
||||
encoding = null
|
||||
}
|
||||
|
||||
let buffer = new Buffer();
|
||||
try {
|
||||
let readStream = new ReadStream(path);
|
||||
readStream.on("data", function(chunck) {
|
||||
chunck.copy(buffer, buffer.length);
|
||||
});
|
||||
readStream.on("error", function onError(error) {
|
||||
callback(error);
|
||||
readStream.destroy();
|
||||
});
|
||||
readStream.on("end", function onEnd() {
|
||||
callback(null, buffer);
|
||||
readStream.destroy();
|
||||
});
|
||||
} catch (error) {
|
||||
setTimeout(callback, 0, error);
|
||||
}
|
||||
};
|
||||
exports.readFile = readFile;
|
||||
|
||||
/**
|
||||
* Synchronous version of `fs.readFile`. Returns the contents of the path.
|
||||
* If encoding is specified then this function returns a string.
|
||||
* Otherwise it returns a buffer.
|
||||
*/
|
||||
function readFileSync(path, encoding) {
|
||||
let buffer = new Buffer();
|
||||
let fd = openSync(path, "r");
|
||||
try {
|
||||
readSync(fd, buffer, 0, ALL, 0);
|
||||
}
|
||||
finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
return buffer;
|
||||
};
|
||||
exports.readFileSync = readFileSync;
|
||||
|
||||
/**
|
||||
* Asynchronously writes data to a file, replacing the file if it already
|
||||
* exists. data can be a string or a buffer.
|
||||
*/
|
||||
function writeFile(path, content, encoding, callback) {
|
||||
try {
|
||||
if (isFunction(encoding)) {
|
||||
callback = encoding
|
||||
encoding = null
|
||||
}
|
||||
if (isString(content))
|
||||
content = new Buffer(content, encoding);
|
||||
|
||||
let writeStream = new WriteStream(path);
|
||||
writeStream.on("error", function onError(error) {
|
||||
callback(error);
|
||||
writeStream.destroy();
|
||||
});
|
||||
writeStream.write(content, function onDrain() {
|
||||
writeStream.destroy();
|
||||
callback(null);
|
||||
});
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
};
|
||||
exports.writeFile = writeFile;
|
||||
|
||||
/**
|
||||
* The synchronous version of `fs.writeFile`.
|
||||
*/
|
||||
function writeFileSync(filename, data, encoding) {
|
||||
throw Error("Not implemented");
|
||||
};
|
||||
exports.writeFileSync = writeFileSync;
|
||||
|
||||
|
||||
function utimesSync(path, atime, mtime) {
|
||||
throw Error("Not implemented");
|
||||
}
|
||||
exports.utimesSync = utimesSync;
|
||||
|
||||
let utimes = Async(utimesSync);
|
||||
exports.utimes = utimes;
|
||||
|
||||
function futimesSync(fd, atime, mtime, callback) {
|
||||
throw Error("Not implemented");
|
||||
}
|
||||
exports.futimesSync = futimesSync;
|
||||
|
||||
let futimes = Async(futimesSync);
|
||||
exports.futimes = futimes;
|
||||
|
||||
function fsyncSync(fd, atime, mtime, callback) {
|
||||
throw Error("Not implemented");
|
||||
}
|
||||
exports.fsyncSync = fsyncSync;
|
||||
|
||||
let fsync = Async(fsyncSync);
|
||||
exports.fsync = fsync;
|
||||
|
||||
|
||||
/**
|
||||
* Watch for changes on filename. The callback listener will be called each
|
||||
* time the file is accessed.
|
||||
*
|
||||
* The second argument is optional. The options if provided should be an object
|
||||
* containing two members a boolean, persistent, and interval, a polling value
|
||||
* in milliseconds. The default is { persistent: true, interval: 0 }.
|
||||
*/
|
||||
function watchFile(path, options, listener) {
|
||||
throw Error("Not implemented");
|
||||
};
|
||||
exports.watchFile = watchFile;
|
||||
|
||||
|
||||
function unwatchFile(path, listener) {
|
||||
throw Error("Not implemented");
|
||||
}
|
||||
exports.unwatchFile = unwatchFile;
|
||||
|
||||
function watch(path, options, listener) {
|
||||
throw Error("Not implemented");
|
||||
}
|
||||
exports.watch = watch;
|
324
addon-sdk/source/lib/sdk/io/stream.js
Normal file
324
addon-sdk/source/lib/sdk/io/stream.js
Normal file
@ -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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { EventTarget } = require("../event/target");
|
||||
const { emit } = require("../event/core");
|
||||
const { Buffer } = require("./buffer");
|
||||
const { Class } = require("../core/heritage");
|
||||
const { setTimeout } = require("../timers");
|
||||
const { ns } = require("../core/namespace");
|
||||
|
||||
function isFunction(value) typeof value === "function"
|
||||
|
||||
function accessor() {
|
||||
let map = new WeakMap();
|
||||
return function(fd, value) {
|
||||
if (value === null) map.delete(fd);
|
||||
if (value !== undefined) map.set(fd, value);
|
||||
return map.get(fd);
|
||||
}
|
||||
}
|
||||
|
||||
let nsIInputStreamPump = accessor();
|
||||
let nsIAsyncOutputStream = accessor();
|
||||
let nsIInputStream = accessor();
|
||||
let nsIOutputStream = accessor();
|
||||
|
||||
|
||||
/**
|
||||
* Utility function / hack that we use to figure if output stream is closed.
|
||||
*/
|
||||
function isClosed(stream) {
|
||||
// We assume that stream is not closed.
|
||||
let isClosed = false;
|
||||
stream.asyncWait({
|
||||
// If `onClose` callback is called before outer function returns
|
||||
// (synchronously) `isClosed` will be set to `true` identifying
|
||||
// that stream is closed.
|
||||
onOutputStreamReady: function onClose() isClosed = true
|
||||
|
||||
// `WAIT_CLOSURE_ONLY` flag overrides the default behavior, causing the
|
||||
// `onOutputStreamReady` notification to be suppressed until the stream
|
||||
// becomes closed.
|
||||
}, stream.WAIT_CLOSURE_ONLY, 0, null);
|
||||
return isClosed;
|
||||
}
|
||||
/**
|
||||
* Utility function takes output `stream`, `onDrain`, `onClose` callbacks and
|
||||
* calls one of this callbacks depending on stream state. It is guaranteed
|
||||
* that only one called will be called and it will be called asynchronously.
|
||||
* @param {nsIAsyncOutputStream} stream
|
||||
* @param {Function} onDrain
|
||||
* callback that is called when stream becomes writable.
|
||||
* @param {Function} onClose
|
||||
* callback that is called when stream becomes closed.
|
||||
*/
|
||||
function onStateChange(stream, target) {
|
||||
let isAsync = false;
|
||||
stream.asyncWait({
|
||||
onOutputStreamReady: function onOutputStreamReady() {
|
||||
// If `isAsync` was not yet set to `true` by the last line we know that
|
||||
// `onOutputStreamReady` was called synchronously. In such case we just
|
||||
// defer execution until next turn of event loop.
|
||||
if (!isAsync)
|
||||
return setTimeout(onOutputStreamReady, 0);
|
||||
|
||||
// As it"s not clear what is a state of the stream (TODO: Is there really
|
||||
// no better way ?) we employ hack (see details in `isClosed`) to verify
|
||||
// if stream is closed.
|
||||
emit(target, isClosed(stream) ? "close" : "drain");
|
||||
}
|
||||
}, 0, 0, null);
|
||||
isAsync = true;
|
||||
}
|
||||
|
||||
function pump(stream) {
|
||||
let input = nsIInputStream(stream);
|
||||
nsIInputStreamPump(stream).asyncRead({
|
||||
onStartRequest: function onStartRequest() {
|
||||
emit(stream, "start");
|
||||
},
|
||||
onDataAvailable: function onDataAvailable(req, c, is, offset, count) {
|
||||
try {
|
||||
let bytes = input.readByteArray(count);
|
||||
emit(stream, "data", new Buffer(bytes, stream.encoding));
|
||||
} catch (error) {
|
||||
emit(stream, "error", error);
|
||||
stream.readable = false;
|
||||
}
|
||||
},
|
||||
onStopRequest: function onStopRequest() {
|
||||
stream.readable = false;
|
||||
emit(stream, "end");
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
|
||||
const Stream = Class({
|
||||
extends: EventTarget,
|
||||
initialize: function() {
|
||||
this.readable = false;
|
||||
this.writable = false;
|
||||
this.encoding = null;
|
||||
},
|
||||
setEncoding: function setEncoding(encoding) {
|
||||
this.encoding = String(encoding).toUpperCase();
|
||||
},
|
||||
pipe: function pipe(target, options) {
|
||||
let source = this;
|
||||
function onData(chunk) {
|
||||
if (target.writable) {
|
||||
if (false === target.write(chunk))
|
||||
source.pause();
|
||||
}
|
||||
}
|
||||
function onDrain() {
|
||||
if (source.readable) source.resume();
|
||||
}
|
||||
function onEnd() {
|
||||
target.end();
|
||||
}
|
||||
function onPause() {
|
||||
source.pause();
|
||||
}
|
||||
function onResume() {
|
||||
if (source.readable)
|
||||
source.resume();
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
source.removeListener("data", onData);
|
||||
target.removeListener("drain", onDrain);
|
||||
source.removeListener("end", onEnd);
|
||||
|
||||
target.removeListener("pause", onPause);
|
||||
target.removeListener("resume", onResume);
|
||||
|
||||
source.removeListener("end", cleanup);
|
||||
source.removeListener("close", cleanup);
|
||||
|
||||
target.removeListener("end", cleanup);
|
||||
target.removeListener("close", cleanup);
|
||||
}
|
||||
|
||||
if (!options || options.end !== false)
|
||||
target.on("end", onEnd);
|
||||
|
||||
source.on("data", onData);
|
||||
target.on("drain", onDrain);
|
||||
target.on("resume", onResume);
|
||||
target.on("pause", onPause);
|
||||
|
||||
source.on("end", cleanup);
|
||||
source.on("close", cleanup);
|
||||
|
||||
target.on("end", cleanup);
|
||||
target.on("close", cleanup);
|
||||
|
||||
emit(target, "pipe", source);
|
||||
},
|
||||
pause: function pause() {
|
||||
emit(this, "pause");
|
||||
},
|
||||
resume: function resume() {
|
||||
emit(this, "resume");
|
||||
},
|
||||
destroySoon: function destroySoon() {
|
||||
this.destroy();
|
||||
}
|
||||
});
|
||||
exports.Stream = Stream;
|
||||
|
||||
const InputStream = Class({
|
||||
extends: Stream,
|
||||
initialize: function initialize(options) {
|
||||
let { input, pump } = options;
|
||||
|
||||
this.readable = true;
|
||||
this.paused = false;
|
||||
nsIInputStream(this, input);
|
||||
nsIInputStreamPump(this, pump);
|
||||
},
|
||||
get status() nsIInputStreamPump(this).status,
|
||||
read: function() pump(this),
|
||||
pause: function pause() {
|
||||
this.paused = true;
|
||||
nsIInputStreamPump(this).suspend();
|
||||
emit(this, "paused");
|
||||
},
|
||||
resume: function resume() {
|
||||
this.paused = false;
|
||||
nsIInputStreamPump(this).resume();
|
||||
emit(this, "resume");
|
||||
},
|
||||
destroy: function destroy() {
|
||||
this.readable = false;
|
||||
try {
|
||||
emit(this, "close", null);
|
||||
nsIInputStreamPump(this).cancel(null);
|
||||
nsIInputStreamPump(this, null);
|
||||
|
||||
nsIInputStream(this).close();
|
||||
nsIInputStream(this, null);
|
||||
} catch (error) {
|
||||
emit(this, "error", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
exports.InputStream = InputStream;
|
||||
|
||||
const OutputStream = Class({
|
||||
extends: Stream,
|
||||
initialize: function initialize(options) {
|
||||
let { output, asyncOutputStream } = options;
|
||||
|
||||
this.writable = true;
|
||||
nsIOutputStream(this, output);
|
||||
nsIAsyncOutputStream(this, asyncOutputStream);
|
||||
},
|
||||
write: function write(content, encoding, callback) {
|
||||
let output = nsIOutputStream(this);
|
||||
let asyncOutputStream = nsIAsyncOutputStream(this);
|
||||
|
||||
if (isFunction(encoding)) {
|
||||
callback = encoding;
|
||||
encoding = callback;
|
||||
}
|
||||
|
||||
// Flag indicating whether or not content has been flushed to the kernel
|
||||
// buffer.
|
||||
let isWritten = false;
|
||||
// If stream is not writable we throw an error.
|
||||
if (!this.writable)
|
||||
throw Error("stream not writable");
|
||||
|
||||
try {
|
||||
// If content is not a buffer then we create one out of it.
|
||||
if (!Buffer.isBuffer(content))
|
||||
content = new Buffer(content, encoding);
|
||||
|
||||
// We write content as a byte array as this will avoid any transcoding
|
||||
// if content was a buffer.
|
||||
output.writeByteArray(content.valueOf(), content.length);
|
||||
output.flush();
|
||||
|
||||
if (callback) this.once("drain", callback);
|
||||
onStateChange(asyncOutputStream, this);
|
||||
return true;
|
||||
} catch (error) {
|
||||
// If errors occur we emit appropriate event.
|
||||
emit(this, "error", error);
|
||||
}
|
||||
},
|
||||
flush: function flush() {
|
||||
nsIOutputStream(this).flush();
|
||||
},
|
||||
end: function end(content, encoding, callback) {
|
||||
if (isFunction(content)) {
|
||||
callback = content
|
||||
content = callback
|
||||
}
|
||||
if (isFunction(encoding)) {
|
||||
callback = encoding
|
||||
encoding = callback
|
||||
}
|
||||
|
||||
// Setting a listener to "close" event if passed.
|
||||
if (isFunction(callback))
|
||||
this.once("close", callback);
|
||||
|
||||
// If content is passed then we defer closing until we finish with writing.
|
||||
if (content)
|
||||
this.write(content, encoding, end.bind(this));
|
||||
// If we don"t write anything, then we close an outputStream.
|
||||
else
|
||||
nsIOutputStream(this).close();
|
||||
},
|
||||
destroy: function destroy(callback) {
|
||||
try {
|
||||
this.end(callback);
|
||||
nsIOutputStream(this, null);
|
||||
nsIAsyncOutputStream(this, null);
|
||||
} catch (error) {
|
||||
emit(this, "error", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
exports.OutputStream = OutputStream;
|
||||
|
||||
const DuplexStream = Class({
|
||||
extends: Stream,
|
||||
initialize: function initialize(options) {
|
||||
let { input, output, pump } = options;
|
||||
|
||||
this.writable = true;
|
||||
this.readable = true;
|
||||
this.encoding = null;
|
||||
|
||||
nsIInputStream(this, input);
|
||||
nsIOutputStream(this, output);
|
||||
nsIInputStreamPump(this, pump);
|
||||
},
|
||||
read: InputStream.prototype.read,
|
||||
pause: InputStream.prototype.pause,
|
||||
resume: InputStream.prototype.resume,
|
||||
|
||||
write: OutputStream.prototype.write,
|
||||
flush: OutputStream.prototype.flush,
|
||||
end: OutputStream.prototype.end,
|
||||
|
||||
destroy: function destroy(error) {
|
||||
if (error)
|
||||
emit(this, "error", error);
|
||||
InputStream.prototype.destroy.call(this);
|
||||
OutputStream.prototype.destroy.call(this);
|
||||
}
|
||||
});
|
||||
exports.DuplexStream = DuplexStream;
|
@ -1,3 +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/. */
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
"use strict";
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
@ -47,7 +55,13 @@ function iterator(target) {
|
||||
for (let ref of refs) {
|
||||
let value = ref.get();
|
||||
|
||||
if (has(target, value))
|
||||
// If `value` is already gc'ed, it would be `null`.
|
||||
// The `has` function is using a WeakMap as lookup table, so passing `null`
|
||||
// would raise an exception because WeakMap accepts as value only non-null
|
||||
// object.
|
||||
// Plus, if `value` is already gc'ed, we do not have to take it in account
|
||||
// during the iteration, and remove it from the references.
|
||||
if (value !== null && has(target, value))
|
||||
yield value;
|
||||
else
|
||||
refs.delete(ref);
|
||||
|
@ -12,10 +12,9 @@ module.metadata = {
|
||||
const observers = require('./deprecated/observer-service');
|
||||
const { Loader, validationAttributes } = require('./content/loader');
|
||||
const { Worker } = require('./content/worker');
|
||||
const { EventEmitter } = require('./deprecated/events');
|
||||
const { List } = require('./deprecated/list');
|
||||
const { Registry } = require('./util/registry');
|
||||
const { MatchPattern } = require('./page-mod/match-pattern');
|
||||
const { EventEmitter } = require('./deprecated/events');
|
||||
const { on, emit } = require('./event/core');
|
||||
const { validateOptions : validate } = require('./deprecated/api-utils');
|
||||
const { Cc, Ci } = require('chrome');
|
||||
const { merge } = require('./util/object');
|
||||
@ -24,14 +23,17 @@ const { windowIterator } = require('./deprecated/window-utils');
|
||||
const { isBrowser, getFrames } = require('./window/utils');
|
||||
const { getTabs, getTabContentWindow, getTabForContentWindow,
|
||||
getURI: getTabURI } = require('./tabs/utils');
|
||||
const { has, hasAny } = require('./util/array');
|
||||
const { ignoreWindow } = require('sdk/private-browsing/utils');
|
||||
const { Style } = require("./stylesheet/style");
|
||||
const { attach, detach } = require("./content/mod");
|
||||
const { has, hasAny } = require("./util/array");
|
||||
const { Rules } = require("./util/rules");
|
||||
|
||||
// Valid values for `attachTo` option
|
||||
const VALID_ATTACHTO_OPTIONS = ['existing', 'top', 'frame'];
|
||||
|
||||
const mods = new WeakMap();
|
||||
|
||||
// contentStyle* / contentScript* are sharing the same validation constraints,
|
||||
// so they can be mostly reused, except for the messages.
|
||||
const validStyleOptions = {
|
||||
@ -43,27 +45,6 @@ const validStyleOptions = {
|
||||
})
|
||||
};
|
||||
|
||||
// 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)),
|
||||
});
|
||||
|
||||
/**
|
||||
* PageMod constructor (exported below).
|
||||
* @constructor
|
||||
@ -121,13 +102,11 @@ const PageMod = Loader.compose(EventEmitter, {
|
||||
|
||||
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 (!include)
|
||||
throw new Error('The `include` option must always contain atleast one rule');
|
||||
|
||||
if (Array.isArray(include))
|
||||
rules.add.apply(null, include);
|
||||
else
|
||||
rules.add(include);
|
||||
rules.add.apply(rules, [].concat(include));
|
||||
|
||||
if (contentStyle || contentStyleFile) {
|
||||
this._style = Style({
|
||||
@ -138,6 +117,7 @@ const PageMod = Loader.compose(EventEmitter, {
|
||||
|
||||
this.on('error', this._onUncaughtError = this._onUncaughtError.bind(this));
|
||||
pageModManager.add(this._public);
|
||||
mods.set(this._public, this);
|
||||
|
||||
// `_applyOnExistingDocuments` has to be called after `pageModManager.add()`
|
||||
// otherwise its calls to `_onContent` method won't do anything.
|
||||
@ -146,26 +126,21 @@ const PageMod = Loader.compose(EventEmitter, {
|
||||
},
|
||||
|
||||
destroy: function destroy() {
|
||||
|
||||
if (this._style)
|
||||
detach(this._style);
|
||||
|
||||
for each (let rule in this.include)
|
||||
this.include.remove(rule);
|
||||
for (let i in this.include)
|
||||
this.include.remove(this.include[i]);
|
||||
|
||||
mods.delete(this._public);
|
||||
pageModManager.remove(this._public);
|
||||
},
|
||||
|
||||
_applyOnExistingDocuments: function _applyOnExistingDocuments() {
|
||||
let mod = this;
|
||||
// Returns true if the tab match one rule
|
||||
function isMatchingURI(uri) {
|
||||
// Use Array.some as `include` isn't a native array
|
||||
return Array.some(mod.include, function (rule) {
|
||||
return RULES[rule].test(uri);
|
||||
});
|
||||
}
|
||||
let tabs = getAllTabs().filter(function (tab) {
|
||||
return isMatchingURI(getTabURI(tab));
|
||||
return mod.include.matchesAny(getTabURI(tab));
|
||||
});
|
||||
|
||||
tabs.forEach(function (tab) {
|
||||
@ -230,12 +205,6 @@ const PageMod = Loader.compose(EventEmitter, {
|
||||
worker.destroy();
|
||||
});
|
||||
},
|
||||
_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);
|
||||
@ -258,9 +227,6 @@ const PageModManager = Registry.resolve({
|
||||
_destructor: function _destructor() {
|
||||
observers.remove('document-element-inserted', this._onContentWindow);
|
||||
this._removeAllListeners();
|
||||
for (let rule in RULES) {
|
||||
delete RULES[rule];
|
||||
}
|
||||
|
||||
// We need to do some cleaning er PageMods, like unregistering any
|
||||
// `contentStyle*`
|
||||
@ -285,14 +251,13 @@ const PageModManager = Registry.resolve({
|
||||
return;
|
||||
}
|
||||
|
||||
for (let rule in RULES)
|
||||
if (RULES[rule].test(document.URL))
|
||||
this._emit(rule, window);
|
||||
this._registry.forEach(function(mod) {
|
||||
if (mod.include.matchesAny(document.URL))
|
||||
mods.get(mod)._onContent(window);
|
||||
});
|
||||
},
|
||||
off: function off(topic, listener) {
|
||||
this.removeListener(topic, listener);
|
||||
if (!this._listeners(topic).length)
|
||||
delete RULES[topic];
|
||||
}
|
||||
});
|
||||
const pageModManager = PageModManager();
|
||||
|
@ -1,115 +1,5 @@
|
||||
/* -*- 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 { deprecateUsage } = require("../util/deprecate");
|
||||
|
||||
"use strict";
|
||||
deprecateUsage("Module 'sdk/page-mod/match-pattern' is deprecated use 'sdk/util/match-pattern' instead");
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
// Tests the urlStr against domain and check if
|
||||
// wildcard submitted (*.domain.com), it only allows
|
||||
// subdomains (sub.domain.com) or from the root (http://domain.com)
|
||||
// and reject non-matching domains (otherdomain.com)
|
||||
// bug 856913
|
||||
if (this.domain && url.host &&
|
||||
(url.host === this.domain ||
|
||||
url.host.slice(-this.domain.length - 1) === "." + this.domain))
|
||||
return true;
|
||||
if (this.urlPrefix && 0 == urlStr.indexOf(this.urlPrefix))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
};
|
||||
module.exports = require("../page-mod/match-pattern");
|
||||
|
@ -9,54 +9,161 @@ module.metadata = {
|
||||
"stability": "stable"
|
||||
};
|
||||
|
||||
const { Symbiont } = require("./content/symbiont");
|
||||
const { Trait } = require("./deprecated/traits");
|
||||
const { Class } = require('./core/heritage');
|
||||
const { on, emit, off, setListeners } = require('./event/core');
|
||||
const { filter, pipe, map, merge: streamMerge } = require('./event/utils');
|
||||
const { WorkerHost, Worker, detach, attach } = require('./worker/utils');
|
||||
const { Disposable } = require('./core/disposable');
|
||||
const { EventTarget } = require('./event/target');
|
||||
const { unload } = require('./system/unload');
|
||||
const { events, streamEventsFrom } = require('./content/events');
|
||||
const { getAttachEventType } = require('./content/utils');
|
||||
const { window } = require('./addon/window');
|
||||
const { getParentWindow } = require('./window/utils');
|
||||
const { create: makeFrame, getDocShell } = require('./frame/utils');
|
||||
const { contract } = require('./util/contract');
|
||||
const { contract: loaderContract } = require('./content/loader');
|
||||
const { has } = require('./util/array');
|
||||
const { Rules } = require('./util/rules');
|
||||
const { merge } = require('./util/object');
|
||||
|
||||
const Page = Trait.compose(
|
||||
Symbiont.resolve({
|
||||
constructor: '_initSymbiont'
|
||||
}),
|
||||
{
|
||||
_frame: Trait.required,
|
||||
_initFrame: Trait.required,
|
||||
postMessage: Symbiont.required,
|
||||
on: Symbiont.required,
|
||||
destroy: Symbiont.required,
|
||||
const views = WeakMap();
|
||||
const workers = WeakMap();
|
||||
const pages = WeakMap();
|
||||
|
||||
constructor: function Page(options) {
|
||||
options = options || {};
|
||||
const readyEventNames = [
|
||||
'DOMContentLoaded',
|
||||
'document-element-inserted',
|
||||
'load'
|
||||
];
|
||||
|
||||
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 ('contentScriptOptions' in options)
|
||||
this.contentScriptOptions = options.contentScriptOptions;
|
||||
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);
|
||||
function workerFor(page) workers.get(page)
|
||||
function pageFor(view) pages.get(view)
|
||||
function viewFor(page) views.get(page)
|
||||
function isDisposed (page) !views.get(page, false)
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
let pageContract = contract(merge({
|
||||
allow: {
|
||||
is: ['object', 'undefined', 'null'],
|
||||
map: function (allow) { return { script: !allow || allow.script !== false }}
|
||||
},
|
||||
onMessage: {
|
||||
is: ['function', 'undefined']
|
||||
},
|
||||
include: {
|
||||
is: ['string', 'array', 'undefined']
|
||||
},
|
||||
contentScriptWhen: {
|
||||
is: ['string', 'undefined']
|
||||
}
|
||||
);
|
||||
exports.Page = function(options) Page(options);
|
||||
exports.Page.prototype = Page.prototype;
|
||||
}, loaderContract.rules));
|
||||
|
||||
function enableScript (page) {
|
||||
getDocShell(viewFor(page)).allowJavascript = true;
|
||||
}
|
||||
|
||||
function disableScript (page) {
|
||||
getDocShell(viewFor(page)).allowJavascript = false;
|
||||
}
|
||||
|
||||
function Allow (page) {
|
||||
return {
|
||||
get script() getDocShell(viewFor(page)).allowJavascript,
|
||||
set script(value) value ? enableScript(page) : disableScript(page)
|
||||
};
|
||||
}
|
||||
|
||||
function injectWorker ({page}) {
|
||||
let worker = workerFor(page);
|
||||
let view = viewFor(page);
|
||||
if (isValidURL(page, view.contentDocument.URL))
|
||||
attach(worker, view.contentWindow);
|
||||
}
|
||||
|
||||
function isValidURL(page, url) !page.rules || page.rules.matchesAny(url)
|
||||
|
||||
const Page = Class({
|
||||
implements: [
|
||||
EventTarget,
|
||||
Disposable
|
||||
],
|
||||
extends: WorkerHost(workerFor),
|
||||
setup: function Page(options) {
|
||||
let page = this;
|
||||
options = pageContract(options);
|
||||
setListeners(this, options);
|
||||
let view = makeFrame(window.document, {
|
||||
nodeName: 'iframe',
|
||||
type: 'content',
|
||||
uri: options.contentURL,
|
||||
allowJavascript: options.allow.script,
|
||||
allowPlugins: true,
|
||||
allowAuth: true
|
||||
});
|
||||
|
||||
['contentScriptFile', 'contentScript', 'contentScriptWhen']
|
||||
.forEach(function (prop) page[prop] = options[prop]);
|
||||
|
||||
views.set(this, view);
|
||||
pages.set(view, this);
|
||||
|
||||
let worker = new Worker(options);
|
||||
workers.set(this, worker);
|
||||
pipe(worker, this);
|
||||
|
||||
if (this.include || options.include) {
|
||||
this.rules = Rules();
|
||||
this.rules.add.apply(this.rules, [].concat(this.include || options.include));
|
||||
}
|
||||
},
|
||||
get allow() Allow(this),
|
||||
set allow(value) {
|
||||
let allowJavascript = pageContract({ allow: value }).allow.script;
|
||||
return allowJavascript ? enableScript(this) : disableScript(this);
|
||||
},
|
||||
get contentURL() { return viewFor(this).getAttribute('src'); },
|
||||
set contentURL(value) {
|
||||
if (!isValidURL(this, value)) return;
|
||||
let view = viewFor(this);
|
||||
let contentURL = pageContract({ contentURL: value }).contentURL;
|
||||
view.setAttribute('src', contentURL);
|
||||
},
|
||||
dispose: function () {
|
||||
if (isDisposed(this)) return;
|
||||
let view = viewFor(this);
|
||||
if (view.parentNode) view.parentNode.removeChild(view);
|
||||
views.delete(this);
|
||||
detach(workers.get(this));
|
||||
},
|
||||
toString: function () '[object Page]'
|
||||
});
|
||||
|
||||
exports.Page = Page;
|
||||
|
||||
let pageEvents = streamMerge([events, streamEventsFrom(window)]);
|
||||
let readyEvents = filter(pageEvents, isReadyEvent);
|
||||
let formattedEvents = map(readyEvents, function({target, type}) {
|
||||
return { type: type, page: pageFromDoc(target) };
|
||||
});
|
||||
let pageReadyEvents = filter(formattedEvents, function({page, type}) {
|
||||
return getAttachEventType(page) === type});
|
||||
on(pageReadyEvents, 'data', injectWorker);
|
||||
|
||||
function isReadyEvent ({type}) {
|
||||
return has(readyEventNames, type);
|
||||
}
|
||||
|
||||
/*
|
||||
* Takes a document, finds its doc shell tree root and returns the
|
||||
* matching Page instance if found
|
||||
*/
|
||||
function pageFromDoc(doc) {
|
||||
let parentWindow = getParentWindow(doc.defaultView), page;
|
||||
if (!parentWindow) return;
|
||||
|
||||
let frames = parentWindow.document.getElementsByTagName('iframe');
|
||||
for (let i = frames.length; i--;)
|
||||
if (frames[i].contentDocument === doc && (page = pageFor(frames[i])))
|
||||
return page;
|
||||
return null;
|
||||
}
|
||||
|
@ -32,16 +32,7 @@ const systemEvents = require("./system/events");
|
||||
const { filter, pipe } = require("./event/utils");
|
||||
const { getNodeView, getActiveView } = require("./view/core");
|
||||
const { isNil, isObject } = require("./lang/type");
|
||||
|
||||
function getAttachEventType(model) {
|
||||
let when = model.contentScriptWhen;
|
||||
return requiresAddonGlobal(model) ? "sdk-panel-content-changed" :
|
||||
when === "start" ? "sdk-panel-content-changed" :
|
||||
when === "end" ? "sdk-panel-document-loaded" :
|
||||
when === "ready" ? "sdk-panel-content-loaded" :
|
||||
null;
|
||||
}
|
||||
|
||||
const { getAttachEventType } = require("./content/utils");
|
||||
|
||||
let number = { is: ['number', 'undefined', 'null'] };
|
||||
let boolean = { is: ['boolean', 'undefined', 'null'] };
|
||||
@ -94,14 +85,14 @@ let setupAutoHide = new function() {
|
||||
// It could be that listener is not GC-ed in the same cycle as
|
||||
// panel in such case we remove listener manually.
|
||||
let view = viewFor(panel);
|
||||
if (!view) systemEvents.off("sdk-panel-show", listener);
|
||||
if (!view) systemEvents.off("popupshowing", listener);
|
||||
else if (subject !== view) panel.hide();
|
||||
}
|
||||
|
||||
// system event listener is intentionally weak this way we'll allow GC
|
||||
// to claim panel if it's no longer referenced by an add-on code. This also
|
||||
// helps minimizing cleanup required on unload.
|
||||
systemEvents.on("sdk-panel-show", listener);
|
||||
systemEvents.on("popupshowing", listener);
|
||||
// To make sure listener is not claimed by GC earlier than necessary we
|
||||
// associate it with `panel` it's associated with. This way it won't be
|
||||
// GC-ed earlier than `panel` itself.
|
||||
@ -184,6 +175,18 @@ const Panel = Class({
|
||||
|
||||
/* Public API: Panel.show */
|
||||
show: function show(options, anchor) {
|
||||
if (options instanceof Ci.nsIDOMElement) {
|
||||
[anchor, options] = [options, null];
|
||||
}
|
||||
|
||||
if (anchor instanceof Ci.nsIDOMElement) {
|
||||
console.warn(
|
||||
"Passing a DOM node to Panel.show() method is an unsupported " +
|
||||
"feature that will be soon replaced. " +
|
||||
"See: https://bugzilla.mozilla.org/show_bug.cgi?id=878877"
|
||||
);
|
||||
}
|
||||
|
||||
let model = modelFor(this);
|
||||
let view = viewFor(this);
|
||||
let anchorView = getNodeView(anchor);
|
||||
@ -234,10 +237,10 @@ exports.Panel = Panel;
|
||||
let panelEvents = filter(events, function({target}) panelFor(target));
|
||||
|
||||
// Panel events emitted after panel has being shown.
|
||||
let shows = filter(panelEvents, function({type}) type === "sdk-panel-shown");
|
||||
let shows = filter(panelEvents, function({type}) type === "popupshown");
|
||||
|
||||
// Panel events emitted after panel became hidden.
|
||||
let hides = filter(panelEvents, function({type}) type === "sdk-panel-hidden");
|
||||
let hides = filter(panelEvents, function({type}) type === "popuphidden");
|
||||
|
||||
// Panel events emitted after content inside panel is ready. For different
|
||||
// panels ready may mean different state based on `contentScriptWhen` attribute.
|
||||
@ -248,7 +251,7 @@ let ready = filter(panelEvents, function({type, target})
|
||||
|
||||
// Panel events emitted after content document in the panel has changed.
|
||||
let change = filter(panelEvents, function({type})
|
||||
type === "sdk-panel-content-changed");
|
||||
type === "document-element-inserted");
|
||||
|
||||
// Forward panel show / hide events to panel's own event listeners.
|
||||
on(shows, "data", function({target}) emit(panelFor(target), "show"));
|
||||
|
@ -19,9 +19,8 @@ let channel = {};
|
||||
function forward({ subject, type, data })
|
||||
emit(channel, "data", { target: subject, type: type, data: data });
|
||||
|
||||
["sdk-panel-show", "sdk-panel-hide", "sdk-panel-shown",
|
||||
"sdk-panel-hidden", "sdk-panel-content-changed", "sdk-panel-content-loaded",
|
||||
"sdk-panel-document-loaded"
|
||||
["popupshowing", "popuphiding", "popupshown", "popuphidden",
|
||||
"document-element-inserted", "DOMContentLoaded", "load"
|
||||
].forEach(function(type) events.on(type, forward));
|
||||
|
||||
exports.events = channel;
|
||||
|
@ -205,16 +205,6 @@ function setupPanelFrame(frame) {
|
||||
}
|
||||
}
|
||||
|
||||
let EVENT_NAMES = {
|
||||
"popupshowing": "sdk-panel-show",
|
||||
"popuphiding": "sdk-panel-hide",
|
||||
"popupshown": "sdk-panel-shown",
|
||||
"popuphidden": "sdk-panel-hidden",
|
||||
"document-element-inserted": "sdk-panel-content-changed",
|
||||
"DOMContentLoaded": "sdk-panel-content-loaded",
|
||||
"load": "sdk-panel-document-loaded"
|
||||
};
|
||||
|
||||
function make(document) {
|
||||
document = document || getMostRecentBrowserWindow().document;
|
||||
let panel = document.createElementNS(XUL_NS, "panel");
|
||||
@ -249,29 +239,29 @@ function make(document) {
|
||||
function onDisplayChange({type}) {
|
||||
try { swapFrameLoaders(backgroundFrame, viewFrame); }
|
||||
catch(error) { console.exception(error); }
|
||||
events.emit(EVENT_NAMES[type], { subject: panel });
|
||||
events.emit(type, { subject: panel });
|
||||
}
|
||||
|
||||
function onContentReady({target, type}) {
|
||||
if (target === getContentDocument(panel)) {
|
||||
style(panel);
|
||||
events.emit(EVENT_NAMES[type], { subject: panel });
|
||||
events.emit(type, { subject: panel });
|
||||
}
|
||||
}
|
||||
|
||||
function onContentLoad({target, type}) {
|
||||
if (target === getContentDocument(panel))
|
||||
events.emit(EVENT_NAMES[type], { subject: panel });
|
||||
events.emit(type, { subject: panel });
|
||||
}
|
||||
|
||||
function onContentChange({subject, type}) {
|
||||
let document = subject;
|
||||
if (document === getContentDocument(panel) && document.defaultView)
|
||||
events.emit(EVENT_NAMES[type], { subject: panel });
|
||||
events.emit(type, { subject: panel });
|
||||
}
|
||||
|
||||
function onPanelStateChange({type}) {
|
||||
events.emit(EVENT_NAMES[type], { subject: panel })
|
||||
events.emit(type, { subject: panel })
|
||||
}
|
||||
|
||||
panel.addEventListener("popupshowing", onDisplayChange, false);
|
||||
|
394
addon-sdk/source/lib/sdk/places/bookmarks.js
Normal file
394
addon-sdk/source/lib/sdk/places/bookmarks.js
Normal file
@ -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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable",
|
||||
"engines": {
|
||||
"Firefox": "*"
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Requiring hosts so they can subscribe to client messages
|
||||
*/
|
||||
require('./host/host-bookmarks');
|
||||
require('./host/host-tags');
|
||||
require('./host/host-query');
|
||||
|
||||
const { Cc, Ci } = require('chrome');
|
||||
const { Class } = require('../core/heritage');
|
||||
const { send } = require('../addon/events');
|
||||
const { defer, reject, all, resolve, promised } = require('../core/promise');
|
||||
const { EventTarget } = require('../event/target');
|
||||
const { emit } = require('../event/core');
|
||||
const { identity, defer:async } = require('../lang/functional');
|
||||
const { extend, merge } = require('../util/object');
|
||||
const { fromIterator } = require('../util/array');
|
||||
const {
|
||||
constructTree, fetchItem, createQuery,
|
||||
isRootGroup, createQueryOptions
|
||||
} = require('./utils');
|
||||
const {
|
||||
bookmarkContract, groupContract, separatorContract
|
||||
} = require('./contract');
|
||||
const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
|
||||
getService(Ci.nsINavBookmarksService);
|
||||
|
||||
/*
|
||||
* Mapping of uncreated bookmarks with their created
|
||||
* counterparts
|
||||
*/
|
||||
const itemMap = new WeakMap();
|
||||
|
||||
/*
|
||||
* Constant used by nsIHistoryQuery; 1 is a bookmark query
|
||||
* https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
|
||||
*/
|
||||
const BOOKMARK_QUERY = 1;
|
||||
|
||||
/*
|
||||
* Bookmark Item classes
|
||||
*/
|
||||
|
||||
const Bookmark = Class({
|
||||
extends: [
|
||||
bookmarkContract.properties(identity)
|
||||
],
|
||||
initialize: function initialize (options) {
|
||||
merge(this, bookmarkContract(extend(defaults, options)));
|
||||
},
|
||||
type: 'bookmark',
|
||||
toString: function () '[object Bookmark]'
|
||||
});
|
||||
exports.Bookmark = Bookmark;
|
||||
|
||||
const Group = Class({
|
||||
extends: [
|
||||
groupContract.properties(identity)
|
||||
],
|
||||
initialize: function initialize (options) {
|
||||
// Don't validate if root group
|
||||
if (isRootGroup(options))
|
||||
merge(this, options);
|
||||
else
|
||||
merge(this, groupContract(extend(defaults, options)));
|
||||
},
|
||||
type: 'group',
|
||||
toString: function () '[object Group]'
|
||||
});
|
||||
exports.Group = Group;
|
||||
|
||||
const Separator = Class({
|
||||
extends: [
|
||||
separatorContract.properties(identity)
|
||||
],
|
||||
initialize: function initialize (options) {
|
||||
merge(this, separatorContract(extend(defaults, options)));
|
||||
},
|
||||
type: 'separator',
|
||||
toString: function () '[object Separator]'
|
||||
});
|
||||
exports.Separator = Separator;
|
||||
|
||||
/*
|
||||
* Functions
|
||||
*/
|
||||
|
||||
function save (items, options) {
|
||||
items = [].concat(items);
|
||||
options = options || {};
|
||||
let emitter = EventTarget();
|
||||
let results = [];
|
||||
let errors = [];
|
||||
let root = constructTree(items);
|
||||
let cache = new Map();
|
||||
|
||||
let isExplicitSave = item => !!~items.indexOf(item);
|
||||
// `walk` returns an aggregate promise indicating the completion
|
||||
// of the `commitItem` on each node, not whether or not that
|
||||
// commit was successful
|
||||
|
||||
// Force this to be async, as if a ducktype fails validation,
|
||||
// the promise implementation will fire an error event, which will
|
||||
// not trigger the handler as it's not yet bound
|
||||
//
|
||||
// Can remove after `Promise.jsm` is implemented in Bug 881047,
|
||||
// which will guarantee next tick execution
|
||||
async(() => root.walk(preCommitItem).then(commitComplete))();
|
||||
|
||||
function preCommitItem ({value:item}) {
|
||||
// Do nothing if tree root, default group (unsavable),
|
||||
// or if it's a dependency and not explicitly saved (in the list
|
||||
// of items to be saved), and not needed to be saved
|
||||
if (item === null || // node is the tree root
|
||||
isRootGroup(item) ||
|
||||
(getId(item) && !isExplicitSave(item)))
|
||||
return;
|
||||
|
||||
return promised(validate)(item)
|
||||
.then(() => commitItem(item, options))
|
||||
.then(data => construct(data, cache))
|
||||
.then(savedItem => {
|
||||
// If item was just created, make a map between
|
||||
// the creation object and created object,
|
||||
// so we can reference the item that doesn't have an id
|
||||
if (!getId(item))
|
||||
saveId(item, savedItem.id);
|
||||
|
||||
// Emit both the processed item, and original item
|
||||
// so a mapping can be understood in handler
|
||||
emit(emitter, 'data', savedItem, item);
|
||||
|
||||
// Push to results iff item was explicitly saved
|
||||
if (isExplicitSave(item))
|
||||
results[items.indexOf(item)] = savedItem;
|
||||
}, reason => {
|
||||
// Force reason to be a string for consistency
|
||||
reason = reason + '';
|
||||
// Emit both the reason, and original item
|
||||
// so a mapping can be understood in handler
|
||||
emit(emitter, 'error', reason + '', item);
|
||||
// Store unsaved item in results list
|
||||
results[items.indexOf(item)] = item;
|
||||
errors.push(reason);
|
||||
});
|
||||
}
|
||||
|
||||
// Called when traversal of the node tree is completed and all
|
||||
// items have been committed
|
||||
function commitComplete () {
|
||||
emit(emitter, 'end', results);
|
||||
}
|
||||
|
||||
return emitter;
|
||||
}
|
||||
exports.save = save;
|
||||
|
||||
function search (queries, options) {
|
||||
queries = [].concat(queries);
|
||||
let emitter = EventTarget();
|
||||
let cache = new Map();
|
||||
let queryObjs = queries.map(createQuery.bind(null, BOOKMARK_QUERY));
|
||||
let optionsObj = createQueryOptions(BOOKMARK_QUERY, options);
|
||||
|
||||
// Can remove after `Promise.jsm` is implemented in Bug 881047,
|
||||
// which will guarantee next tick execution
|
||||
async(() => {
|
||||
send('sdk-places-query', { queries: queryObjs, options: optionsObj })
|
||||
.then(handleQueryResponse);
|
||||
})();
|
||||
|
||||
function handleQueryResponse (data) {
|
||||
let deferreds = data.map(item => {
|
||||
return construct(item, cache).then(bookmark => {
|
||||
emit(emitter, 'data', bookmark);
|
||||
return bookmark;
|
||||
}, reason => {
|
||||
emit(emitter, 'error', reason);
|
||||
errors.push(reason);
|
||||
});
|
||||
});
|
||||
|
||||
all(deferreds).then(data => {
|
||||
emit(emitter, 'end', data);
|
||||
}, () => emit(emitter, 'end', []));
|
||||
}
|
||||
|
||||
return emitter;
|
||||
}
|
||||
exports.search = search;
|
||||
|
||||
function remove (items) {
|
||||
return [].concat(items).map(item => {
|
||||
item.remove = true;
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
exports.remove = remove;
|
||||
|
||||
/*
|
||||
* Internal Utilities
|
||||
*/
|
||||
|
||||
function commitItem (item, options) {
|
||||
// Get the item's ID, or getId it's saved version if it exists
|
||||
let id = getId(item);
|
||||
let data = normalize(item);
|
||||
let promise;
|
||||
|
||||
data.id = id;
|
||||
|
||||
if (!id) {
|
||||
promise = send('sdk-places-bookmarks-create', data);
|
||||
} else if (item.remove) {
|
||||
promise = send('sdk-places-bookmarks-remove', { id: id });
|
||||
} else {
|
||||
promise = send('sdk-places-bookmarks-last-updated', {
|
||||
id: id
|
||||
}).then(function (updated) {
|
||||
// If attempting to save an item that is not the
|
||||
// latest snapshot of a bookmark item, execute
|
||||
// the resolution function
|
||||
if (updated !== item.updated && options.resolve)
|
||||
return fetchItem(id)
|
||||
.then(options.resolve.bind(null, data));
|
||||
else
|
||||
return data;
|
||||
}).then(send.bind(null, 'sdk-places-bookmarks-save'));
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/*
|
||||
* Turns a bookmark item into a plain object,
|
||||
* converts `tags` from Set to Array, group instance to an id
|
||||
*/
|
||||
function normalize (item) {
|
||||
let data = merge({}, item);
|
||||
// Circumvent prototype property of `type`
|
||||
delete data.type;
|
||||
data.type = item.type;
|
||||
data.tags = [];
|
||||
if (item.tags) {
|
||||
data.tags = fromIterator(item.tags);
|
||||
}
|
||||
data.group = getId(data.group) || exports.UNSORTED.id;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/*
|
||||
* Takes a data object and constructs a BookmarkItem instance
|
||||
* of it, recursively generating parent instances as well.
|
||||
*
|
||||
* Pass in a `cache` Map to reuse instances of
|
||||
* bookmark items to reduce overhead;
|
||||
* The cache object is a map of id to a deferred with a
|
||||
* promise that resolves to the bookmark item.
|
||||
*/
|
||||
function construct (object, cache, forced) {
|
||||
let item = instantiate(object);
|
||||
let deferred = defer();
|
||||
|
||||
// Item could not be instantiated
|
||||
if (!item)
|
||||
return resolve(null);
|
||||
|
||||
// Return promise for item if found in the cache,
|
||||
// and not `forced`. `forced` indicates that this is the construct
|
||||
// call that should not read from cache, but should actually perform
|
||||
// the construction, as it was set before several async calls
|
||||
if (cache.has(item.id) && !forced)
|
||||
return cache.get(item.id).promise;
|
||||
else if (cache.has(item.id))
|
||||
deferred = cache.get(item.id);
|
||||
else
|
||||
cache.set(item.id, deferred);
|
||||
|
||||
// When parent group is found in cache, use
|
||||
// the same deferred value
|
||||
if (item.group && cache.has(item.group)) {
|
||||
cache.get(item.group).promise.then(group => {
|
||||
item.group = group;
|
||||
deferred.resolve(item);
|
||||
});
|
||||
|
||||
// If not in the cache, and a root group, return
|
||||
// the premade instance
|
||||
} else if (rootGroups.get(item.group)) {
|
||||
item.group = rootGroups.get(item.group);
|
||||
deferred.resolve(item);
|
||||
|
||||
// If not in the cache or a root group, fetch the parent
|
||||
} else {
|
||||
cache.set(item.group, defer());
|
||||
fetchItem(item.group).then(group => {
|
||||
return construct(group, cache, true);
|
||||
}).then(group => {
|
||||
item.group = group;
|
||||
deferred.resolve(item);
|
||||
}, deferred.reject);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function instantiate (object) {
|
||||
if (object.type === 'bookmark')
|
||||
return Bookmark(object);
|
||||
if (object.type === 'group')
|
||||
return Group(object);
|
||||
if (object.type === 'separator')
|
||||
return Separator(object);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a bookmark item; will throw an error if ininvalid,
|
||||
* to be used with `promised`. As bookmark items check on their class,
|
||||
* this only checks ducktypes
|
||||
*/
|
||||
function validate (object) {
|
||||
if (!isDuckType(object)) return true;
|
||||
let contract = object.type === 'bookmark' ? bookmarkContract :
|
||||
object.type === 'group' ? groupContract :
|
||||
object.type === 'separator' ? separatorContract :
|
||||
null;
|
||||
if (!contract) {
|
||||
throw Error('No type specified');
|
||||
}
|
||||
|
||||
// If object has a property set, and undefined,
|
||||
// manually override with default as it'll fail otherwise
|
||||
let withDefaults = Object.keys(defaults).reduce((obj, prop) => {
|
||||
if (obj[prop] == null) obj[prop] = defaults[prop];
|
||||
return obj;
|
||||
}, extend(object));
|
||||
|
||||
contract(withDefaults);
|
||||
}
|
||||
|
||||
function isDuckType (item) {
|
||||
return !(item instanceof Bookmark) &&
|
||||
!(item instanceof Group) &&
|
||||
!(item instanceof Separator);
|
||||
}
|
||||
|
||||
function saveId (unsaved, id) {
|
||||
itemMap.set(unsaved, id);
|
||||
}
|
||||
|
||||
// Fetches an item's ID from itself, or from the mapped items
|
||||
function getId (item) {
|
||||
return typeof item === 'number' ? item :
|
||||
item ? item.id || itemMap.get(item) :
|
||||
null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Set up the default, root groups
|
||||
*/
|
||||
|
||||
let defaultGroupMap = {
|
||||
MENU: bmsrv.bookmarksMenuFolder,
|
||||
TOOLBAR: bmsrv.toolbarFolder,
|
||||
UNSORTED: bmsrv.unfiledBookmarksFolder
|
||||
};
|
||||
|
||||
let rootGroups = new Map();
|
||||
|
||||
for (let i in defaultGroupMap) {
|
||||
let group = Object.freeze(Group({ title: i, id: defaultGroupMap[i] }));
|
||||
rootGroups.set(defaultGroupMap[i], group);
|
||||
exports[i] = group;
|
||||
}
|
||||
|
||||
let defaults = {
|
||||
group: exports.UNSORTED,
|
||||
index: -1
|
||||
};
|
77
addon-sdk/source/lib/sdk/places/contract.js
Normal file
77
addon-sdk/source/lib/sdk/places/contract.js
Normal file
@ -0,0 +1,77 @@
|
||||
/* -*- 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { Cc, Ci } = require('chrome');
|
||||
const { EventEmitter } = require('../deprecated/events');
|
||||
const { isValidURI, URL } = require('../url');
|
||||
const { contract } = require('../util/contract');
|
||||
const { extend } = require('../util/object');
|
||||
|
||||
// map of property validations
|
||||
const validItem = {
|
||||
id: {
|
||||
is: ['number', 'undefined', 'null'],
|
||||
},
|
||||
group: {
|
||||
is: ['object', 'number', 'undefined', 'null'],
|
||||
ok: function (value) {
|
||||
return value &&
|
||||
(value.toString && value.toString() === '[object Group]') ||
|
||||
typeof value === 'number' ||
|
||||
value.type === 'group';
|
||||
},
|
||||
msg: 'The `group` property must be a valid Group object'
|
||||
},
|
||||
index: {
|
||||
is: ['undefined', 'null', 'number'],
|
||||
map: function (value) value == null ? -1 : value,
|
||||
msg: 'The `index` property must be a number.'
|
||||
},
|
||||
updated: {
|
||||
is: ['number', 'undefined']
|
||||
}
|
||||
};
|
||||
|
||||
const validTitle = {
|
||||
title: {
|
||||
is: ['string'],
|
||||
msg: 'The `title` property must be defined.'
|
||||
}
|
||||
};
|
||||
|
||||
const validURL = {
|
||||
url: {
|
||||
is: ['string'],
|
||||
ok: isValidURI,
|
||||
msg: 'The `url` property must be a valid URL.'
|
||||
}
|
||||
};
|
||||
|
||||
const validTags = {
|
||||
tags: {
|
||||
is: ['object'],
|
||||
ok: function (tags) tags instanceof Set,
|
||||
map: function (tags) {
|
||||
if (Array.isArray(tags))
|
||||
return new Set(tags);
|
||||
if (tags == null)
|
||||
return new Set();
|
||||
return tags;
|
||||
},
|
||||
msg: 'The `tags` property must be a Set, or an array'
|
||||
}
|
||||
};
|
||||
|
||||
exports.bookmarkContract = contract(
|
||||
extend(validItem, validTitle, validURL, validTags));
|
||||
exports.separatorContract = contract(validItem);
|
||||
exports.groupContract = contract(extend(validItem, validTitle));
|
64
addon-sdk/source/lib/sdk/places/history.js
Normal file
64
addon-sdk/source/lib/sdk/places/history.js
Normal file
@ -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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable",
|
||||
"engines": {
|
||||
"Firefox": "*"
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Requiring hosts so they can subscribe to client messages
|
||||
*/
|
||||
require('./host/host-bookmarks');
|
||||
require('./host/host-tags');
|
||||
require('./host/host-query');
|
||||
|
||||
const { Cc, Ci } = require('chrome');
|
||||
const { Class } = require('../core/heritage');
|
||||
const { events, send } = require('../addon/events');
|
||||
const { defer, reject, all } = require('../core/promise');
|
||||
const { uuid } = require('../util/uuid');
|
||||
const { flatten } = require('../util/array');
|
||||
const { has, extend, merge, pick } = require('../util/object');
|
||||
const { emit } = require('../event/core');
|
||||
const { defer: async } = require('../lang/functional');
|
||||
const { EventTarget } = require('../event/target');
|
||||
const {
|
||||
urlQueryParser, createQuery, createQueryOptions
|
||||
} = require('./utils');
|
||||
|
||||
/*
|
||||
* Constant used by nsIHistoryQuery; 0 is a history query
|
||||
* https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
|
||||
*/
|
||||
const HISTORY_QUERY = 0;
|
||||
|
||||
let search = function query (queries, options) {
|
||||
queries = [].concat(queries);
|
||||
let emitter = EventTarget();
|
||||
let queryObjs = queries.map(createQuery.bind(null, HISTORY_QUERY));
|
||||
let optionsObj = createQueryOptions(HISTORY_QUERY, options);
|
||||
|
||||
// Can remove after `Promise.jsm` is implemented in Bug 881047,
|
||||
// which will guarantee next tick execution
|
||||
async(() => {
|
||||
send('sdk-places-query', {
|
||||
query: queryObjs,
|
||||
options: optionsObj
|
||||
}).then(results => {
|
||||
results.map(item => emit(emitter, 'data', item));
|
||||
emit(emitter, 'end', results);
|
||||
}, reason => {
|
||||
emit(emitter, 'error', reason);
|
||||
emit(emitter, 'end', []);
|
||||
});
|
||||
})();
|
||||
|
||||
return emitter;
|
||||
};
|
||||
exports.search = search;
|
236
addon-sdk/source/lib/sdk/places/host/host-bookmarks.js
Normal file
236
addon-sdk/source/lib/sdk/places/host/host-bookmarks.js
Normal file
@ -0,0 +1,236 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental",
|
||||
"engines": {
|
||||
"Firefox": "*"
|
||||
}
|
||||
};
|
||||
|
||||
const { Cc, Ci } = require('chrome');
|
||||
const browserHistory = Cc["@mozilla.org/browser/nav-history-service;1"].
|
||||
getService(Ci.nsIBrowserHistory);
|
||||
const asyncHistory = Cc["@mozilla.org/browser/history;1"].
|
||||
getService(Ci.mozIAsyncHistory);
|
||||
const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
|
||||
getService(Ci.nsINavBookmarksService);
|
||||
const taggingService = Cc["@mozilla.org/browser/tagging-service;1"].
|
||||
getService(Ci.nsITaggingService);
|
||||
const ios = Cc['@mozilla.org/network/io-service;1'].
|
||||
getService(Ci.nsIIOService);
|
||||
const { query } = require('./host-query');
|
||||
const {
|
||||
defer, all, resolve, promised, reject
|
||||
} = require('../../core/promise');
|
||||
const { request, response } = require('../../addon/host');
|
||||
const { send } = require('../../addon/events');
|
||||
const { on, emit } = require('../../event/core');
|
||||
const { filter } = require('../../event/utils');
|
||||
const { URL, isValidURI } = require('../../url');
|
||||
const { newURI } = require('../../url/utils');
|
||||
|
||||
const DEFAULT_INDEX = bmsrv.DEFAULT_INDEX;
|
||||
const UNSORTED_ID = bmsrv.unfiledBookmarksFolder;
|
||||
const ROOT_FOLDERS = [
|
||||
bmsrv.unfiledBookmarksFolder, bmsrv.toolbarFolder,
|
||||
bmsrv.tagsFolder, bmsrv.bookmarksMenuFolder
|
||||
];
|
||||
|
||||
const EVENT_MAP = {
|
||||
'sdk-places-bookmarks-create': createBookmarkItem,
|
||||
'sdk-places-bookmarks-save': saveBookmarkItem,
|
||||
'sdk-places-bookmarks-last-updated': getBookmarkLastUpdated,
|
||||
'sdk-places-bookmarks-get': getBookmarkItem,
|
||||
'sdk-places-bookmarks-remove': removeBookmarkItem,
|
||||
'sdk-places-bookmarks-get-all': getAllBookmarks,
|
||||
'sdk-places-bookmarks-get-children': getChildren
|
||||
};
|
||||
|
||||
function typeMap (type) {
|
||||
if (typeof type === 'number') {
|
||||
if (bmsrv.TYPE_BOOKMARK === type) return 'bookmark';
|
||||
if (bmsrv.TYPE_FOLDER === type) return 'group';
|
||||
if (bmsrv.TYPE_SEPARATOR === type) return 'separator';
|
||||
} else {
|
||||
if ('bookmark' === type) return bmsrv.TYPE_BOOKMARK;
|
||||
if ('group' === type) return bmsrv.TYPE_FOLDER;
|
||||
if ('separator' === type) return bmsrv.TYPE_SEPARATOR;
|
||||
}
|
||||
}
|
||||
|
||||
function getBookmarkLastUpdated ({id})
|
||||
resolve(bmsrv.getItemLastModified(id))
|
||||
exports.getBookmarkLastUpdated;
|
||||
|
||||
function createBookmarkItem (data) {
|
||||
let error;
|
||||
|
||||
if (data.group == null) data.group = UNSORTED_ID;
|
||||
if (data.index == null) data.index = DEFAULT_INDEX;
|
||||
|
||||
if (data.type === 'group')
|
||||
data.id = bmsrv.createFolder(
|
||||
data.group, data.title, data.index
|
||||
);
|
||||
else if (data.type === 'separator')
|
||||
data.id = bmsrv.insertSeparator(
|
||||
data.group, data.index
|
||||
);
|
||||
else
|
||||
data.id = bmsrv.insertBookmark(
|
||||
data.group, newURI(data.url), data.index, data.title
|
||||
);
|
||||
|
||||
// In the event where default or no index is provided (-1),
|
||||
// query the actual index for the response
|
||||
if (data.index === -1)
|
||||
data.index = bmsrv.getItemIndex(data.id);
|
||||
|
||||
data.updated = bmsrv.getItemLastModified(data.id);
|
||||
|
||||
return tag(data, true).then(() => data);
|
||||
}
|
||||
exports.createBookmarkItem = createBookmarkItem;
|
||||
|
||||
function saveBookmarkItem (data) {
|
||||
let id = data.id;
|
||||
if (!id)
|
||||
reject('Item is missing id');
|
||||
|
||||
let group = bmsrv.getFolderIdForItem(id);
|
||||
let index = bmsrv.getItemIndex(id);
|
||||
let type = bmsrv.getItemType(id);
|
||||
|
||||
if (data.url) {
|
||||
bmsrv.changeBookmarkURI(id, newURI(data.url));
|
||||
}
|
||||
else if (typeMap(type) === 'bookmark')
|
||||
data.url = bmsrv.getBookmarkURI(id).spec;
|
||||
|
||||
if (data.title)
|
||||
bmsrv.setItemTitle(id, data.title);
|
||||
else if (typeMap(type) !== 'separator')
|
||||
data.title = bmsrv.getItemTitle(id);
|
||||
|
||||
if (data.group && data.group !== group)
|
||||
bmsrv.moveItem(id, data.group, data.index || -1);
|
||||
else if (data.index != null && data.index !== index) {
|
||||
// We use moveItem here instead of setItemIndex
|
||||
// so we don't have to manage the indicies of the siblings
|
||||
bmsrv.moveItem(id, group, data.index);
|
||||
} else if (data.index == null)
|
||||
data.index = bmsrv.getItemIndex(id);
|
||||
|
||||
data.updated = bmsrv.getItemLastModified(data.id);
|
||||
|
||||
return tag(data).then(() => data);
|
||||
}
|
||||
exports.saveBookmarkItem = saveBookmarkItem;
|
||||
|
||||
function removeBookmarkItem (data) {
|
||||
let id = data.id;
|
||||
|
||||
if (!id)
|
||||
reject('Item is missing id');
|
||||
|
||||
bmsrv.removeItem(id);
|
||||
return resolve(null);
|
||||
}
|
||||
exports.removeBookmarkItem = removeBookmarkItem;
|
||||
|
||||
function getBookmarkItem (data) {
|
||||
let id = data.id;
|
||||
|
||||
if (!id)
|
||||
reject('Item is missing id');
|
||||
|
||||
let type = bmsrv.getItemType(id);
|
||||
|
||||
data.type = typeMap(type);
|
||||
|
||||
if (type === bmsrv.TYPE_BOOKMARK || type === bmsrv.TYPE_FOLDER)
|
||||
data.title = bmsrv.getItemTitle(id);
|
||||
|
||||
if (type === bmsrv.TYPE_BOOKMARK) {
|
||||
data.url = bmsrv.getBookmarkURI(id).spec;
|
||||
// Should be moved into host-tags as a method
|
||||
data.tags = taggingService.getTagsForURI(newURI(data.url), {});
|
||||
}
|
||||
|
||||
data.group = bmsrv.getFolderIdForItem(id);
|
||||
data.index = bmsrv.getItemIndex(id);
|
||||
data.updated = bmsrv.getItemLastModified(data.id);
|
||||
|
||||
return resolve(data);
|
||||
}
|
||||
exports.getBookmarkItem = getBookmarkItem;
|
||||
|
||||
function getAllBookmarks () {
|
||||
return query({}, { queryType: 1 }).then(bookmarks =>
|
||||
all(bookmarks.map(getBookmarkItem)));
|
||||
}
|
||||
exports.getAllBookmarks = getAllBookmarks;
|
||||
|
||||
function getChildren ({ id }) {
|
||||
if (typeMap(bmsrv.getItemType(id)) !== 'group') return [];
|
||||
let ids = [];
|
||||
for (let i = 0; ids[ids.length - 1] !== -1; i++)
|
||||
ids.push(bmsrv.getIdForItemAt(id, i));
|
||||
ids.pop();
|
||||
return all(ids.map(id => getBookmarkItem({ id: id })));
|
||||
}
|
||||
exports.getChildren = getChildren;
|
||||
|
||||
/*
|
||||
* Hook into host
|
||||
*/
|
||||
|
||||
let reqStream = filter(request, function (data) /sdk-places-bookmarks/.test(data.event));
|
||||
on(reqStream, 'data', function ({event, id, data}) {
|
||||
if (!EVENT_MAP[event]) return;
|
||||
|
||||
let resData = {
|
||||
id: id,
|
||||
event: event
|
||||
};
|
||||
|
||||
promised(EVENT_MAP[event])(data).then(res => {
|
||||
resData.data = res;
|
||||
respond(resData);
|
||||
}, reason => {
|
||||
resData.error = reason;
|
||||
respond(resData);
|
||||
});
|
||||
});
|
||||
|
||||
function respond (data) {
|
||||
emit(response, 'data', data);
|
||||
}
|
||||
|
||||
function tag (data, isNew) {
|
||||
// If a new item, we can skip checking what other tags
|
||||
// are on the item
|
||||
if (data.type !== 'bookmark') {
|
||||
return resolve();
|
||||
} else if (!isNew) {
|
||||
return send('sdk-places-tags-get-tags-by-url', { url: data.url })
|
||||
.then(tags => {
|
||||
return send('sdk-places-tags-untag', {
|
||||
tags: tags.filter(tag => !~data.tags.indexOf(tag)),
|
||||
url: data.url
|
||||
});
|
||||
}).then(() => send('sdk-places-tags-tag', {
|
||||
url: data.url, tags: data.tags
|
||||
}));
|
||||
}
|
||||
else if (data.tags && data.tags.length) {
|
||||
return send('sdk-places-tags-tag', { url: data.url, tags: data.tags });
|
||||
}
|
||||
else
|
||||
return resolve();
|
||||
}
|
||||
|
171
addon-sdk/source/lib/sdk/places/host/host-query.js
Normal file
171
addon-sdk/source/lib/sdk/places/host/host-query.js
Normal file
@ -0,0 +1,171 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental",
|
||||
"engines": {
|
||||
"Firefox": "*"
|
||||
}
|
||||
};
|
||||
|
||||
const { Cc, Ci } = require('chrome');
|
||||
const { defer, all, resolve } = require('../../core/promise');
|
||||
const { safeMerge, omit } = require('../../util/object');
|
||||
const historyService = Cc['@mozilla.org/browser/nav-history-service;1']
|
||||
.getService(Ci.nsINavHistoryService);
|
||||
const bookmarksService = Cc['@mozilla.org/browser/nav-bookmarks-service;1']
|
||||
.getService(Ci.nsINavBookmarksService);
|
||||
const { request, response } = require('../../addon/host');
|
||||
const { newURI } = require('../../url/utils');
|
||||
const { send } = require('../../addon/events');
|
||||
const { on, emit } = require('../../event/core');
|
||||
const { filter } = require('../../event/utils');
|
||||
|
||||
const ROOT_FOLDERS = [
|
||||
bookmarksService.unfiledBookmarksFolder, bookmarksService.toolbarFolder,
|
||||
bookmarksService.bookmarksMenuFolder
|
||||
];
|
||||
|
||||
const EVENT_MAP = {
|
||||
'sdk-places-query': queryReceiver
|
||||
};
|
||||
|
||||
// Properties that need to be manually
|
||||
// copied into a nsINavHistoryQuery object
|
||||
const MANUAL_QUERY_PROPERTIES = [
|
||||
'uri', 'folder', 'tags', 'url', 'folder'
|
||||
];
|
||||
|
||||
const PLACES_PROPERTIES = [
|
||||
'uri', 'title', 'accessCount', 'time'
|
||||
];
|
||||
|
||||
function execute (queries, options) {
|
||||
let deferred = defer();
|
||||
let root = historyService
|
||||
.executeQueries(queries, queries.length, options).root;
|
||||
|
||||
let items = collect([], root);
|
||||
deferred.resolve(items);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function collect (acc, node) {
|
||||
node.containerOpen = true;
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
let child = node.getChild(i);
|
||||
acc.push(child);
|
||||
if (child.type === child.RESULT_TYPE_FOLDER) {
|
||||
let container = child.QueryInterface(Ci.nsINavHistoryContainerResultNode);
|
||||
collect(acc, container);
|
||||
}
|
||||
}
|
||||
node.containerOpen = false;
|
||||
return acc;
|
||||
}
|
||||
|
||||
function query (queries, options) {
|
||||
queries = queries || [];
|
||||
options = options || {};
|
||||
let deferred = defer();
|
||||
let optionsObj, queryObjs;
|
||||
|
||||
try {
|
||||
optionsObj = historyService.getNewQueryOptions();
|
||||
queryObjs = [].concat(queries).map(createQuery);
|
||||
if (!queryObjs.length) {
|
||||
queryObjs = [historyService.getNewQuery()];
|
||||
}
|
||||
safeMerge(optionsObj, options);
|
||||
} catch (e) {
|
||||
deferred.reject(e);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/*
|
||||
* Currently `places:` queries are not supported
|
||||
*/
|
||||
optionsObj.excludeQueries = true;
|
||||
|
||||
execute(queryObjs, optionsObj).then(function (results) {
|
||||
if (optionsObj.queryType === 0) {
|
||||
return results.map(normalize);
|
||||
} else if (optionsObj.queryType === 1) {
|
||||
// Formats query results into more standard
|
||||
// data structures for returning
|
||||
return all(results.map(({itemId}) =>
|
||||
send('sdk-places-bookmarks-get', { id: itemId })));
|
||||
}
|
||||
}).then(deferred.resolve, deferred.reject);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
exports.query = query;
|
||||
|
||||
function createQuery (query) {
|
||||
query = query || {};
|
||||
let queryObj = historyService.getNewQuery();
|
||||
|
||||
safeMerge(queryObj, omit(query, MANUAL_QUERY_PROPERTIES));
|
||||
|
||||
if (query.tags && Array.isArray(query.tags))
|
||||
queryObj.tags = query.tags;
|
||||
if (query.uri || query.url)
|
||||
queryObj.uri = newURI(query.uri || query.url);
|
||||
if (query.folder)
|
||||
queryObj.setFolders([query.folder], 1);
|
||||
return queryObj;
|
||||
}
|
||||
|
||||
function queryReceiver (message) {
|
||||
let queries = message.data.queries || message.data.query;
|
||||
let options = message.data.options;
|
||||
let resData = {
|
||||
id: message.id,
|
||||
event: message.event
|
||||
};
|
||||
|
||||
query(queries, options).then(results => {
|
||||
resData.data = results;
|
||||
respond(resData);
|
||||
}, reason => {
|
||||
resData.error = reason;
|
||||
respond(resData);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Converts a nsINavHistoryResultNode into a plain object
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
|
||||
*/
|
||||
function normalize (historyObj) {
|
||||
return PLACES_PROPERTIES.reduce((obj, prop) => {
|
||||
if (prop === 'uri')
|
||||
obj.url = historyObj.uri;
|
||||
else if (prop === 'time') {
|
||||
// Cast from microseconds to milliseconds
|
||||
obj.time = Math.floor(historyObj.time / 1000)
|
||||
} else if (prop === 'accessCount')
|
||||
obj.visitCount = historyObj[prop];
|
||||
else
|
||||
obj[prop] = historyObj[prop];
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/*
|
||||
* Hook into host
|
||||
*/
|
||||
|
||||
let reqStream = filter(request, function (data) /sdk-places-query/.test(data.event));
|
||||
on(reqStream, 'data', function (e) {
|
||||
if (EVENT_MAP[e.event]) EVENT_MAP[e.event](e);
|
||||
});
|
||||
|
||||
function respond (data) {
|
||||
emit(response, 'data', data);
|
||||
}
|
91
addon-sdk/source/lib/sdk/places/host/host-tags.js
Normal file
91
addon-sdk/source/lib/sdk/places/host/host-tags.js
Normal file
@ -0,0 +1,91 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental",
|
||||
"engines": {
|
||||
"Firefox": "*"
|
||||
}
|
||||
};
|
||||
|
||||
const { Cc, Ci } = require('chrome');
|
||||
const taggingService = Cc["@mozilla.org/browser/tagging-service;1"].
|
||||
getService(Ci.nsITaggingService);
|
||||
const ios = Cc['@mozilla.org/network/io-service;1'].
|
||||
getService(Ci.nsIIOService);
|
||||
const { URL } = require('../../url');
|
||||
const { newURI } = require('../../url/utils');
|
||||
const { request, response } = require('../../addon/host');
|
||||
const { on, emit } = require('../../event/core');
|
||||
const { filter } = require('../../event/utils');
|
||||
|
||||
const EVENT_MAP = {
|
||||
'sdk-places-tags-tag': tag,
|
||||
'sdk-places-tags-untag': untag,
|
||||
'sdk-places-tags-get-tags-by-url': getTagsByURL,
|
||||
'sdk-places-tags-get-urls-by-tag': getURLsByTag
|
||||
};
|
||||
|
||||
function tag (message) {
|
||||
let data = message.data;
|
||||
let resData = {
|
||||
id: message.id,
|
||||
event: message.event
|
||||
};
|
||||
|
||||
resData.data = taggingService.tagURI(newURI(data.url), data.tags);
|
||||
respond(resData);
|
||||
}
|
||||
|
||||
function untag (message) {
|
||||
let data = message.data;
|
||||
let resData = {
|
||||
id: message.id,
|
||||
event: message.event
|
||||
};
|
||||
|
||||
resData.data = taggingService.untagURI(newURI(data.url), data.tags);
|
||||
respond(resData);
|
||||
}
|
||||
|
||||
function getURLsByTag (message) {
|
||||
let data = message.data;
|
||||
let resData = {
|
||||
id: message.id,
|
||||
event: message.event
|
||||
};
|
||||
|
||||
resData.data = taggingService
|
||||
.getURIsForTag(data.tag).map(function (uri) uri.spec);
|
||||
respond(resData);
|
||||
}
|
||||
|
||||
function getTagsByURL (message) {
|
||||
let data = message.data;
|
||||
let resData = {
|
||||
id: message.id,
|
||||
event: message.event
|
||||
};
|
||||
|
||||
resData.data = taggingService.getTagsForURI(newURI(data.url), {});
|
||||
respond(resData);
|
||||
}
|
||||
|
||||
/*
|
||||
* Hook into host
|
||||
*/
|
||||
|
||||
let reqStream = filter(request, function (data) {
|
||||
return /sdk-places-tags/.test(data.event);
|
||||
});
|
||||
|
||||
on(reqStream, 'data', function (e) {
|
||||
if (EVENT_MAP[e.event]) EVENT_MAP[e.event](e);
|
||||
});
|
||||
|
||||
function respond (data) {
|
||||
emit(response, 'data', data);
|
||||
}
|
237
addon-sdk/source/lib/sdk/places/utils.js
Normal file
237
addon-sdk/source/lib/sdk/places/utils.js
Normal file
@ -0,0 +1,237 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.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';
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental",
|
||||
"engines": {
|
||||
"Firefox": "*"
|
||||
}
|
||||
};
|
||||
|
||||
const { Cc, Ci } = require('chrome');
|
||||
const { Class } = require('../core/heritage');
|
||||
const { method } = require('../lang/functional');
|
||||
const { defer, promised, all } = require('../core/promise');
|
||||
const { send } = require('../addon/events');
|
||||
const { EventTarget } = require('../event/target');
|
||||
const { merge } = require('../util/object');
|
||||
const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
|
||||
getService(Ci.nsINavBookmarksService);
|
||||
|
||||
/*
|
||||
* TreeNodes are used to construct dependency trees
|
||||
* for BookmarkItems
|
||||
*/
|
||||
let TreeNode = Class({
|
||||
initialize: function (value) {
|
||||
this.value = value;
|
||||
this.children = [];
|
||||
},
|
||||
add: function (values) {
|
||||
[].concat(values).forEach(value => {
|
||||
this.children.push(value instanceof TreeNode ? value : TreeNode(value));
|
||||
});
|
||||
},
|
||||
get length () {
|
||||
let count = 0;
|
||||
this.walk(() => count++);
|
||||
// Do not count the current node
|
||||
return --count;
|
||||
},
|
||||
get: method(get),
|
||||
walk: method(walk),
|
||||
toString: function () '[object TreeNode]'
|
||||
});
|
||||
exports.TreeNode = TreeNode;
|
||||
|
||||
/*
|
||||
* Descends down from `node` applying `fn` to each in order.
|
||||
* Can be asynchronous if `fn` returns a promise. `fn` is passed
|
||||
* one argument, the current node, `curr`
|
||||
*/
|
||||
function walk (curr, fn) {
|
||||
return promised(fn)(curr).then(val => {
|
||||
return all(curr.children.map(child => walk(child, fn)));
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Descends from the TreeNode `node`, returning
|
||||
* the node with value `value` if found or `null`
|
||||
* otherwise
|
||||
*/
|
||||
function get (node, value) {
|
||||
if (node.value === value) return node;
|
||||
for (let child of node.children) {
|
||||
let found = get(child, value);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Constructs a tree of bookmark nodes
|
||||
* returning the root (value: null);
|
||||
*/
|
||||
|
||||
function constructTree (items) {
|
||||
let root = TreeNode(null);
|
||||
items.forEach(treeify.bind(null, root));
|
||||
|
||||
function treeify (root, item) {
|
||||
// If node already exists, skip
|
||||
let node = root.get(item);
|
||||
if (node) return node;
|
||||
node = TreeNode(item);
|
||||
|
||||
let parentNode = item.group ? treeify(root, item.group) : root;
|
||||
parentNode.add(node);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
exports.constructTree = constructTree;
|
||||
|
||||
/*
|
||||
* Shortcut for converting an id, or an object with an id, into
|
||||
* an object with corresponding bookmark data
|
||||
*/
|
||||
function fetchItem (item)
|
||||
send('sdk-places-bookmarks-get', { id: item.id || item })
|
||||
exports.fetchItem = fetchItem;
|
||||
|
||||
/*
|
||||
* Takes an ID or an object with ID and checks it against
|
||||
* the root bookmark folders
|
||||
*/
|
||||
function isRootGroup (id) {
|
||||
id = id && id.id;
|
||||
return ~[bmsrv.bookmarksMenuFolder, bmsrv.toolbarFolder,
|
||||
bmsrv.unfiledBookmarksFolder
|
||||
].indexOf(id);
|
||||
}
|
||||
exports.isRootGroup = isRootGroup;
|
||||
|
||||
/*
|
||||
* Merges appropriate options into query based off of url
|
||||
* 4 scenarios:
|
||||
*
|
||||
* 'moz.com' // domain: moz.com, domainIsHost: true
|
||||
* --> 'http://moz.com', 'http://moz.com/thunderbird'
|
||||
* '*.moz.com' // domain: moz.com, domainIsHost: false
|
||||
* --> 'http://moz.com', 'http://moz.com/index', 'http://ff.moz.com/test'
|
||||
* 'http://moz.com' // url: http://moz.com/, urlIsPrefix: false
|
||||
* --> 'http://moz.com/'
|
||||
* 'http://moz.com/*' // url: http://moz.com/, urlIsPrefix: true
|
||||
* --> 'http://moz.com/', 'http://moz.com/thunderbird'
|
||||
*/
|
||||
|
||||
function urlQueryParser (query, url) {
|
||||
if (!url) return;
|
||||
if (/^https?:\/\//.test(url)) {
|
||||
query.uri = url.charAt(url.length - 1) === '/' ? url : url + '/';
|
||||
if (/\*$/.test(url)) {
|
||||
query.uri = url.replace(/\*$/, '');
|
||||
query.uriIsPrefix = true;
|
||||
}
|
||||
} else {
|
||||
if (/^\*/.test(url)) {
|
||||
query.domain = url.replace(/^\*\./, '');
|
||||
query.domainIsHost = false;
|
||||
} else {
|
||||
query.domain = url;
|
||||
query.domainIsHost = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.urlQueryParser = urlQueryParser;
|
||||
|
||||
/*
|
||||
* Takes an EventEmitter and returns a promise that
|
||||
* aggregates results and handles a bulk resolve and reject
|
||||
*/
|
||||
|
||||
function promisedEmitter (emitter) {
|
||||
let { promise, resolve, reject } = defer();
|
||||
let errors = [];
|
||||
emitter.on('error', error => errors.push(error));
|
||||
emitter.on('end', (items) => {
|
||||
if (errors.length) reject(errors[0]);
|
||||
else resolve(items);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
exports.promisedEmitter = promisedEmitter;
|
||||
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
|
||||
function createQuery (type, query) {
|
||||
query = query || {};
|
||||
let qObj = {
|
||||
searchTerms: query.query
|
||||
};
|
||||
|
||||
urlQueryParser(qObj, query.url);
|
||||
|
||||
// 0 === history
|
||||
if (type === 0) {
|
||||
// PRTime used by query is in microseconds, not milliseconds
|
||||
qObj.beginTime = (query.from || 0) * 1000;
|
||||
qObj.endTime = (query.to || new Date()) * 1000;
|
||||
|
||||
// Set reference time to Epoch
|
||||
qObj.beginTimeReference = 0;
|
||||
qObj.endTimeReference = 0;
|
||||
}
|
||||
// 1 === bookmarks
|
||||
else if (type === 1) {
|
||||
qObj.tags = query.tags;
|
||||
qObj.folder = query.group && query.group.id;
|
||||
}
|
||||
// 2 === unified (not implemented on platform)
|
||||
else if (type === 2) {
|
||||
|
||||
}
|
||||
|
||||
return qObj;
|
||||
}
|
||||
exports.createQuery = createQuery;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
|
||||
|
||||
const SORT_MAP = {
|
||||
title: 1,
|
||||
date: 3, // sort by visit date
|
||||
url: 5,
|
||||
visitCount: 7,
|
||||
// keywords currently unsupported
|
||||
// keyword: 9,
|
||||
dateAdded: 11, // bookmarks only
|
||||
lastModified: 13 // bookmarks only
|
||||
};
|
||||
|
||||
function createQueryOptions (type, options) {
|
||||
options = options || {};
|
||||
let oObj = {};
|
||||
oObj.sortingMode = SORT_MAP[options.sort] || 0;
|
||||
if (options.descending && options.sort)
|
||||
oObj.sortingMode++;
|
||||
|
||||
// Resolve to default sort if ineligible based on query type
|
||||
if (type === 0 && // history
|
||||
(options.sort === 'dateAdded' || options.sort === 'lastModified'))
|
||||
oObj.sortingMode = 0;
|
||||
|
||||
oObj.maxResults = typeof options.count === 'number' ? options.count : 0;
|
||||
|
||||
oObj.queryType = type;
|
||||
|
||||
return oObj;
|
||||
}
|
||||
exports.createQueryOptions = createQueryOptions;
|
||||
|
@ -7,7 +7,7 @@ module.metadata = {
|
||||
"stability": "stable"
|
||||
};
|
||||
|
||||
const { setMode, getMode, on: onStateChange } = require('./private-browsing/utils');
|
||||
const { setMode, getMode, on: onStateChange, isPermanentPrivateBrowsing } = require('./private-browsing/utils');
|
||||
const { isWindowPrivate } = require('./window/utils');
|
||||
const { emit, on, once, off } = require('./event/core');
|
||||
const { when: unload } = require('./system/unload');
|
||||
@ -65,6 +65,10 @@ exports.isPrivate = function(thing) {
|
||||
return isWindowPrivate(window);
|
||||
}
|
||||
|
||||
// check if the post pwpb, global pb service is enabled.
|
||||
if (isPermanentPrivateBrowsing())
|
||||
return true;
|
||||
|
||||
// if we get here, and global private browsing
|
||||
// is available, and it is true, then return
|
||||
// true otherwise false is returned here
|
||||
|
@ -52,6 +52,10 @@ let isWindowPBSupported = exports.isWindowPBSupported =
|
||||
// checks that per-tab private browsing is implemented
|
||||
let isTabPBSupported = exports.isTabPBSupported =
|
||||
!pbService && !!PrivateBrowsingUtils && is('Fennec') && satisfiesVersion(version, '>=20.0*');
|
||||
|
||||
exports.isPermanentPrivateBrowsing = function() {
|
||||
return !!(PrivateBrowsingUtils && PrivateBrowsingUtils.permanentPrivateBrowsing);
|
||||
}
|
||||
|
||||
function ignoreWindow(window) {
|
||||
return !isPrivateBrowsingSupported && isWindowPrivate(window) && !isGlobalPBSupported;
|
||||
|
@ -85,7 +85,7 @@ exports.stdout = new function() {
|
||||
/**
|
||||
* 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
|
||||
* https://developer.mozilla.org/en-US/docs/Code_snippets/File_I_O#Getting_files_in_special_directories
|
||||
* http://mxr.mozilla.org/mozilla-central/source/xpcom/io/nsAppDirectoryServiceDefs.h
|
||||
* @example
|
||||
*
|
||||
|
@ -62,8 +62,16 @@ const Tab = Class({
|
||||
* @type {String}
|
||||
*/
|
||||
get favicon() {
|
||||
// TODO: provide the real favicon when it is available
|
||||
console.error(ERR_FENNEC_MSG);
|
||||
/*
|
||||
* Synchronous favicon services were never supported on Fennec,
|
||||
* and as of FF22, are now deprecated. When/if favicon services
|
||||
* are supported for Fennec, this getter should reference
|
||||
* `require('sdk/places/favicon').getFavicon`
|
||||
*/
|
||||
console.error(
|
||||
'tab.favicon is deprecated, and currently ' +
|
||||
'favicon helpers are not yet supported by Fennec'
|
||||
);
|
||||
|
||||
// return 16x16 blank default
|
||||
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAEklEQVQ4jWNgGAWjYBSMAggAAAQQAAF/TXiOAAAAAElFTkSuQmCC';
|
||||
|
@ -12,6 +12,8 @@ module.metadata = {
|
||||
const BaseAssert = require("sdk/test/assert").Assert;
|
||||
const { isFunction, isObject } = require("sdk/lang/type");
|
||||
|
||||
exports.Assert = BaseAssert;
|
||||
|
||||
function extend(target) {
|
||||
let descriptor = {}
|
||||
Array.slice(arguments, 1).forEach(function(source) {
|
||||
|
95
addon-sdk/source/lib/sdk/test/utils.js
Normal file
95
addon-sdk/source/lib/sdk/test/utils.js
Normal file
@ -0,0 +1,95 @@
|
||||
/* 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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'unstable'
|
||||
};
|
||||
|
||||
function getTestNames (exports)
|
||||
Object.keys(exports).filter(name => /^test/.test(name))
|
||||
|
||||
function isAsync (fn) fn.length > 1
|
||||
|
||||
/*
|
||||
* Takes an `exports` object of a test file and a function `beforeFn`
|
||||
* to be run before each test. `beforeFn` is called with a `name` string
|
||||
* as the first argument of the test name, and may specify a second
|
||||
* argument function `done` to indicate that this function should
|
||||
* resolve asynchronously
|
||||
*/
|
||||
function before (exports, beforeFn) {
|
||||
getTestNames(exports).map(name => {
|
||||
let testFn = exports[name];
|
||||
if (!isAsync(testFn) && !isAsync(beforeFn)) {
|
||||
exports[name] = function (assert) {
|
||||
beforeFn(name);
|
||||
testFn(assert);
|
||||
};
|
||||
}
|
||||
else if (isAsync(testFn) && !isAsync(beforeFn)) {
|
||||
exports[name] = function (assert, done) {
|
||||
beforeFn(name);
|
||||
testFn(assert, done);
|
||||
}
|
||||
}
|
||||
else if (!isAsync(testFn) && isAsync(beforeFn)) {
|
||||
exports[name] = function (assert, done) {
|
||||
beforeFn(name, () => {
|
||||
testFn(assert);
|
||||
done();
|
||||
});
|
||||
}
|
||||
} else if (isAsync(testFn) && isAsync(beforeFn)) {
|
||||
exports[name] = function (assert, done) {
|
||||
beforeFn(name, () => {
|
||||
testFn(assert, done);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
exports.before = before;
|
||||
|
||||
/*
|
||||
* Takes an `exports` object of a test file and a function `afterFn`
|
||||
* to be run after each test. `afterFn` is called with a `name` string
|
||||
* as the first argument of the test name, and may specify a second
|
||||
* argument function `done` to indicate that this function should
|
||||
* resolve asynchronously
|
||||
*/
|
||||
function after (exports, afterFn) {
|
||||
getTestNames(exports).map(name => {
|
||||
let testFn = exports[name];
|
||||
if (!isAsync(testFn) && !isAsync(afterFn)) {
|
||||
exports[name] = function (assert) {
|
||||
testFn(assert);
|
||||
afterFn(name);
|
||||
};
|
||||
}
|
||||
else if (isAsync(testFn) && !isAsync(afterFn)) {
|
||||
exports[name] = function (assert, done) {
|
||||
testFn(assert, () => {
|
||||
afterFn(name);
|
||||
done();
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (!isAsync(testFn) && isAsync(afterFn)) {
|
||||
exports[name] = function (assert, done) {
|
||||
testFn(assert);
|
||||
afterFn(name, done);
|
||||
}
|
||||
} else if (isAsync(testFn) && isAsync(afterFn)) {
|
||||
exports[name] = function (assert, done) {
|
||||
testFn(assert, () => {
|
||||
afterFn(name, done);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
exports.after = after;
|
@ -101,7 +101,6 @@ function fromIterator(iterator) {
|
||||
}
|
||||
exports.fromIterator = fromIterator;
|
||||
|
||||
|
||||
function find(array, predicate) {
|
||||
var index = 0;
|
||||
var count = array.length;
|
||||
|
122
addon-sdk/source/lib/sdk/util/match-pattern.js
Normal file
122
addon-sdk/source/lib/sdk/util/match-pattern.js
Normal file
@ -0,0 +1,122 @@
|
||||
/* -*- 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { URL } = require('../url');
|
||||
const cache = {};
|
||||
|
||||
function MatchPattern(pattern) {
|
||||
if (cache[pattern]) return cache[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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cache[pattern] = this;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Tests the urlStr against domain and check if
|
||||
// wildcard submitted (*.domain.com), it only allows
|
||||
// subdomains (sub.domain.com) or from the root (http://domain.com)
|
||||
// and reject non-matching domains (otherdomain.com)
|
||||
// bug 856913
|
||||
if (this.domain && url.host &&
|
||||
(url.host === this.domain ||
|
||||
url.host.slice(-this.domain.length - 1) === "." + this.domain))
|
||||
return true;
|
||||
if (this.urlPrefix && 0 == urlStr.indexOf(this.urlPrefix))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
toString: function () '[object MatchPattern]'
|
||||
|
||||
};
|
||||
|
||||
exports.MatchPattern = MatchPattern;
|
@ -8,6 +8,8 @@ module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { flatten } = require('./array');
|
||||
|
||||
/**
|
||||
* 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
|
||||
@ -29,6 +31,7 @@ module.metadata = {
|
||||
*/
|
||||
function merge(source) {
|
||||
let descriptor = {};
|
||||
|
||||
// `Boolean` converts the first parameter to a boolean value. Any object is
|
||||
// converted to `true` where `null` and `undefined` becames `false`. Therefore
|
||||
// the `filter` method will keep only objects that are defined and not null.
|
||||
@ -54,4 +57,36 @@ function extend(source) {
|
||||
}
|
||||
exports.extend = extend;
|
||||
|
||||
function has(obj, key) obj.hasOwnProperty(key);
|
||||
exports.has = has;
|
||||
|
||||
function each(obj, fn) {
|
||||
for (let key in obj) has(obj, key) && fn(obj[key], key, obj);
|
||||
}
|
||||
exports.each = each;
|
||||
|
||||
/**
|
||||
* Like `merge`, except no property descriptors are manipulated, for use
|
||||
* with platform objects. Identical to underscore's `extend`. Useful for
|
||||
* merging XPCOM objects
|
||||
*/
|
||||
function safeMerge(source) {
|
||||
Array.slice(arguments, 1).forEach(function onEach (obj) {
|
||||
for (let prop in obj) source[prop] = obj[prop];
|
||||
});
|
||||
return source;
|
||||
}
|
||||
exports.safeMerge = safeMerge;
|
||||
|
||||
/*
|
||||
* Returns a copy of the object without blacklisted properties
|
||||
*/
|
||||
function omit(source, ...values) {
|
||||
let copy = {};
|
||||
let keys = flatten(values);
|
||||
for (let prop in source)
|
||||
if (!~keys.indexOf(prop))
|
||||
copy[prop] = source[prop];
|
||||
return copy;
|
||||
}
|
||||
exports.omit = omit;
|
||||
|
52
addon-sdk/source/lib/sdk/util/rules.js
Normal file
52
addon-sdk/source/lib/sdk/util/rules.js
Normal file
@ -0,0 +1,52 @@
|
||||
/* -*- 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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { Class } = require('../core/heritage');
|
||||
const { MatchPattern } = require('./match-pattern');
|
||||
const { on, off, emit } = require('../event/core');
|
||||
const { method } = require('../lang/functional');
|
||||
const objectUtil = require('./object');
|
||||
const { EventTarget } = require('../event/target');
|
||||
const { List, addListItem, removeListItem } = require('./list');
|
||||
|
||||
// Should deprecate usage of EventEmitter/compose
|
||||
const Rules = Class({
|
||||
implements: [
|
||||
EventTarget,
|
||||
List
|
||||
],
|
||||
add: function(...rules) [].concat(rules).forEach(function onAdd(rule) {
|
||||
addListItem(this, rule);
|
||||
emit(this, 'add', rule);
|
||||
}, this),
|
||||
remove: function(...rules) [].concat(rules).forEach(function onRemove(rule) {
|
||||
removeListItem(this, rule);
|
||||
emit(this, 'remove', rule);
|
||||
}, this),
|
||||
get: function(rule) {
|
||||
let found = false;
|
||||
for (let i in this) if (this[i] === rule) found = true;
|
||||
return found;
|
||||
},
|
||||
// Returns true if uri matches atleast one stored rule
|
||||
matchesAny: function(uri) !!filterMatches(this, uri).length,
|
||||
toString: function() '[object Rules]'
|
||||
});
|
||||
exports.Rules = Rules;
|
||||
|
||||
function filterMatches(instance, uri) {
|
||||
let matches = [];
|
||||
for (let i in instance) {
|
||||
if (new MatchPattern(instance[i]).test(uri)) matches.push(instance[i]);
|
||||
}
|
||||
return matches;
|
||||
}
|
@ -604,6 +604,42 @@ BrowserWindow.prototype = {
|
||||
let palette = toolbox.palette;
|
||||
palette.appendChild(node);
|
||||
|
||||
if (this.window.CustomizableUI) {
|
||||
let placement = this.window.CustomizableUI.getPlacementOfWidget(node.id);
|
||||
if (!placement) {
|
||||
placement = {area: 'nav-bar', position: undefined};
|
||||
}
|
||||
this.window.CustomizableUI.addWidgetToArea(node.id, placement.area, placement.position);
|
||||
|
||||
// Depending on when this gets called, we might be in the right place now. In that case,
|
||||
// don't run the following code.
|
||||
if (node.parentNode != palette) {
|
||||
return;
|
||||
}
|
||||
// Otherwise, insert:
|
||||
let container = this.doc.getElementById(placement.area);
|
||||
if (container.customizationTarget) {
|
||||
container = container.customizationTarget;
|
||||
}
|
||||
|
||||
if (placement.position !== undefined) {
|
||||
// Find a position:
|
||||
let items = this.window.CustomizableUI.getWidgetIdsInArea(placement.area);
|
||||
let itemIndex = placement.position;
|
||||
for (let l = items.length; itemIndex < l; itemIndex++) {
|
||||
let realItems = container.getElementsByAttribute("id", items[itemIndex]);
|
||||
if (realItems[0]) {
|
||||
container.insertBefore(node, realItems[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node.parentNode != container) {
|
||||
container.appendChild(node);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Search for widget toolbar by reading toolbar's currentset attribute
|
||||
let container = null;
|
||||
let toolbars = this.doc.getElementsByTagName("toolbar");
|
||||
|
@ -389,3 +389,16 @@ function getOwnerBrowserWindow(node) {
|
||||
});
|
||||
}
|
||||
exports.getOwnerBrowserWindow = getOwnerBrowserWindow;
|
||||
|
||||
function getParentWindow(window) {
|
||||
try {
|
||||
return window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShellTreeItem).parent
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow);
|
||||
}
|
||||
catch (e) {}
|
||||
return null;
|
||||
}
|
||||
exports.getParentWindow = getParentWindow;
|
||||
|
@ -350,7 +350,7 @@ exports.Require = Require;
|
||||
const main = iced(function main(loader, id) {
|
||||
let uri = resolveURI(id, loader.mapping)
|
||||
let module = loader.main = loader.modules[uri] = Module(id, uri);
|
||||
return load(loader, module).exports;
|
||||
return loader.load(loader, module).exports;
|
||||
});
|
||||
exports.main = main;
|
||||
|
||||
|
@ -33,7 +33,7 @@
|
||||
"url": "sdk/url",
|
||||
"traceback": "sdk/console/traceback",
|
||||
"xhr": "sdk/net/xhr",
|
||||
"match-pattern": "sdk/page-mod/match-pattern",
|
||||
"match-pattern": "sdk/util/match-pattern",
|
||||
"tab-browser": "sdk/deprecated/tab-browser",
|
||||
"file": "sdk/io/file",
|
||||
"runtime": "sdk/system/runtime",
|
||||
|
@ -4,8 +4,7 @@
|
||||
|
||||
|
||||
This document describes the structure of the HTML generated by the renderapi.py
|
||||
tool, both for use in the API docs shown by "cfx docs" and as exported by
|
||||
"cfx sdocs". The particular HTML id and class attributes embedded in the files,
|
||||
tool. The particular HTML id and class attributes embedded in the files,
|
||||
as well as their organization, represent the interface between the tool and any
|
||||
front-end code wanting to style the docs in some particular way.
|
||||
|
||||
|
@ -223,7 +223,8 @@ class SmallXPI(unittest.TestCase):
|
||||
os.path.join("sdk", "self.js"),
|
||||
os.path.join("sdk", "core", "promise.js"),
|
||||
os.path.join("sdk", "net", "url.js"),
|
||||
os.path.join("sdk", "util", "object.js")
|
||||
os.path.join("sdk", "util", "object.js"),
|
||||
os.path.join("sdk", "util", "array.js")
|
||||
]])
|
||||
|
||||
missing = set(expected) - set(used_files)
|
||||
@ -265,6 +266,7 @@ class SmallXPI(unittest.TestCase):
|
||||
"resources/addon-sdk/lib/sdk/net/",
|
||||
"resources/addon-sdk/lib/sdk/core/promise.js",
|
||||
"resources/addon-sdk/lib/sdk/util/object.js",
|
||||
"resources/addon-sdk/lib/sdk/util/array.js",
|
||||
"resources/addon-sdk/lib/sdk/net/url.js",
|
||||
"resources/three/",
|
||||
"resources/three/lib/",
|
||||
|
@ -60,7 +60,7 @@ def welcome():
|
||||
print ("Your SDK may not work properly.")
|
||||
return
|
||||
|
||||
print ("Welcome to the Add-on SDK. Run 'cfx docs' for assistance.")
|
||||
print ("Welcome to the Add-on SDK. For the docs, visit https://addons.mozilla.org/en-US/developers/docs/sdk/latest/")
|
||||
|
||||
if __name__ == '__main__':
|
||||
welcome()
|
||||
|
@ -3,6 +3,9 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { LoaderWithHookedConsole } = require('sdk/test/loader');
|
||||
const { loader } = LoaderWithHookedConsole(module);
|
||||
const app = require("sdk/system/xul-app");
|
||||
|
||||
// This test makes sure that require statements used by all AMO hosted
|
||||
@ -104,7 +107,7 @@ exports["test compatibility"] = function(assert) {
|
||||
require("sdk/deprecated/events"), "sdk/deprecated/events -> events");
|
||||
|
||||
assert.equal(require("match-pattern"),
|
||||
require("sdk/page-mod/match-pattern"), "sdk/page-mod/match-pattern -> match-pattern");
|
||||
require("sdk/util/match-pattern"), "sdk/util/match-pattern -> match-pattern");
|
||||
|
||||
if (app.is("Firefox")) {
|
||||
assert.equal(require("tab-browser"),
|
||||
@ -141,8 +144,8 @@ exports["test compatibility"] = function(assert) {
|
||||
assert.equal(require("querystring"),
|
||||
require("sdk/querystring"), "sdk/querystring -> querystring");
|
||||
|
||||
assert.equal(require("addon-page"),
|
||||
require("sdk/addon-page"), "sdk/addon-page -> addon-page");
|
||||
assert.equal(loader.require("addon-page"),
|
||||
loader.require("sdk/addon-page"), "sdk/addon-page -> addon-page");
|
||||
|
||||
assert.equal(require("tabs/utils"),
|
||||
require("sdk/tabs/utils"), "sdk/tabs/utils -> tabs/utils");
|
||||
|
10
addon-sdk/source/test/fixtures/loader/unsupported/fennec.js
vendored
Normal file
10
addon-sdk/source/test/fixtures/loader/unsupported/fennec.js
vendored
Normal file
@ -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/. */
|
||||
|
||||
module.metadata = {
|
||||
"engines": {
|
||||
"Fennec": "*"
|
||||
}
|
||||
};
|
||||
module.exports = {};
|
10
addon-sdk/source/test/fixtures/loader/unsupported/firefox.js
vendored
Normal file
10
addon-sdk/source/test/fixtures/loader/unsupported/firefox.js
vendored
Normal file
@ -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/. */
|
||||
|
||||
module.metadata = {
|
||||
"engines": {
|
||||
"Firefox": "*"
|
||||
}
|
||||
};
|
||||
module.exports = {};
|
222
addon-sdk/source/test/places-helper.js
Normal file
222
addon-sdk/source/test/places-helper.js
Normal file
@ -0,0 +1,222 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.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 bmsrv = Cc['@mozilla.org/browser/nav-bookmarks-service;1'].
|
||||
getService(Ci.nsINavBookmarksService);
|
||||
const hsrv = Cc['@mozilla.org/browser/nav-history-service;1'].
|
||||
getService(Ci.nsINavHistoryService);
|
||||
const brsrv = Cc["@mozilla.org/browser/nav-history-service;1"]
|
||||
.getService(Ci.nsIBrowserHistory);
|
||||
const tagsrv = Cc['@mozilla.org/browser/tagging-service;1'].
|
||||
getService(Ci.nsITaggingService);
|
||||
const asyncHistory = Cc['@mozilla.org/browser/history;1'].
|
||||
getService(Ci.mozIAsyncHistory);
|
||||
const { send } = require('sdk/addon/events');
|
||||
const { setTimeout } = require('sdk/timers');
|
||||
const { newURI } = require('sdk/url/utils');
|
||||
const { defer, all } = require('sdk/core/promise');
|
||||
const { once } = require('sdk/system/events');
|
||||
const {
|
||||
Bookmark, Group, Separator,
|
||||
save, search,
|
||||
MENU, TOOLBAR, UNSORTED
|
||||
} = require('sdk/places/bookmarks');
|
||||
|
||||
function invalidResolve (assert) {
|
||||
return function (e) {
|
||||
assert.fail('Resolve state should not be called: ' + e);
|
||||
};
|
||||
}
|
||||
exports.invalidResolve = invalidResolve;
|
||||
|
||||
function invalidReject (assert) {
|
||||
return function (e) {
|
||||
assert.fail('Reject state should not be called: ' + e);
|
||||
};
|
||||
}
|
||||
exports.invalidReject = invalidReject;
|
||||
|
||||
// Removes all children of group
|
||||
function clearBookmarks (group) {
|
||||
group
|
||||
? bmsrv.removeFolderChildren(group.id)
|
||||
: clearAllBookmarks();
|
||||
}
|
||||
exports.clearBookmarks = clearBookmarks;
|
||||
|
||||
function clearAllBookmarks () {
|
||||
[MENU, TOOLBAR, UNSORTED].forEach(clearBookmarks);
|
||||
}
|
||||
exports.clearAllBookmarks = clearAllBookmarks;
|
||||
|
||||
function compareWithHost (assert, item) {
|
||||
let id = item.id;
|
||||
let type = item.type === 'group' ? bmsrv.TYPE_FOLDER : bmsrv['TYPE_' + item.type.toUpperCase()];
|
||||
let url = item.url && !item.url.endsWith('/') ? item.url + '/' : item.url;
|
||||
|
||||
if (type === bmsrv.TYPE_BOOKMARK) {
|
||||
assert.equal(url, bmsrv.getBookmarkURI(id).spec.toString(), 'Matches host url');
|
||||
let tags = tagsrv.getTagsForURI(newURI(item.url));
|
||||
for (let tag of tags) {
|
||||
// Handle both array for raw data and set for instances
|
||||
if (Array.isArray(item.tags))
|
||||
assert.ok(~item.tags.indexOf(tag), 'has correct tag');
|
||||
else
|
||||
assert.ok(item.tags.has(tag), 'has correct tag');
|
||||
}
|
||||
assert.equal(tags.length,
|
||||
Array.isArray(item.tags) ? item.tags.length : item.tags.size,
|
||||
'matches tag count');
|
||||
}
|
||||
if (type !== bmsrv.TYPE_SEPARATOR) {
|
||||
assert.equal(item.title, bmsrv.getItemTitle(id), 'Matches host title');
|
||||
}
|
||||
assert.equal(item.index, bmsrv.getItemIndex(id), 'Matches host index');
|
||||
assert.equal(item.group.id || item.group, bmsrv.getFolderIdForItem(id), 'Matches host group id');
|
||||
assert.equal(type, bmsrv.getItemType(id), 'Matches host type');
|
||||
}
|
||||
exports.compareWithHost = compareWithHost;
|
||||
|
||||
function addVisits (urls) {
|
||||
var deferred = defer();
|
||||
asyncHistory.updatePlaces([].concat(urls).map(createVisit), {
|
||||
handleResult: function () {},
|
||||
handleError: deferred.reject,
|
||||
handleCompletion: deferred.resolve
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
exports.addVisits = addVisits;
|
||||
|
||||
// Creates a mozIVisitInfo object
|
||||
function createVisit (url) {
|
||||
let place = {}
|
||||
place.uri = newURI(url);
|
||||
place.title = "Test visit for " + place.uri.spec;
|
||||
place.visits = [{
|
||||
transitionType: hsrv.TRANSITION_LINK,
|
||||
visitDate: +(new Date()) * 1000,
|
||||
referredURI: undefined
|
||||
}];
|
||||
return place;
|
||||
}
|
||||
|
||||
function clearHistory (done) {
|
||||
hsrv.removeAllPages();
|
||||
once('places-expiration-finished', done);
|
||||
}
|
||||
exports.clearHistory = clearHistory;
|
||||
|
||||
function createBookmark (data) {
|
||||
data = data || {};
|
||||
let item = {
|
||||
title: data.title || 'Moz',
|
||||
url: data.url || (!data.type || data.type === 'bookmark' ?
|
||||
'http://moz.com/' :
|
||||
undefined),
|
||||
tags: data.tags || (!data.type || data.type === 'bookmark' ?
|
||||
['firefox'] :
|
||||
undefined),
|
||||
type: data.type || 'bookmark',
|
||||
group: data.group
|
||||
};
|
||||
return send('sdk-places-bookmarks-create', item);
|
||||
}
|
||||
exports.createBookmark = createBookmark;
|
||||
|
||||
function createBookmarkItem (data) {
|
||||
let deferred = defer();
|
||||
data = data || {};
|
||||
save({
|
||||
title: data.title || 'Moz',
|
||||
url: data.url || 'http://moz.com/',
|
||||
tags: data.tags || (!data.type || data.type === 'bookmark' ?
|
||||
['firefox'] :
|
||||
undefined),
|
||||
type: data.type || 'bookmark',
|
||||
group: data.group
|
||||
}).on('end', function (bookmark) {
|
||||
deferred.resolve(bookmark[0]);
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
exports.createBookmarkItem = createBookmarkItem;
|
||||
|
||||
function createBookmarkTree () {
|
||||
let agg = [];
|
||||
return createBookmarkItem({ type: 'group', title: 'mozgroup' })
|
||||
.then(group => {
|
||||
agg.push(group);
|
||||
return all([createBookmarkItem({
|
||||
title: 'mozilla.com',
|
||||
url: 'http://mozilla.com/',
|
||||
group: group,
|
||||
tags: ['mozilla', 'firefox', 'thunderbird', 'rust']
|
||||
}), createBookmarkItem({
|
||||
title: 'mozilla.org',
|
||||
url: 'http://mozilla.org/',
|
||||
group: group,
|
||||
tags: ['mozilla', 'firefox', 'thunderbird', 'rust']
|
||||
}), createBookmarkItem({
|
||||
title: 'firefox',
|
||||
url: 'http://firefox.com/',
|
||||
group: group,
|
||||
tags: ['mozilla', 'firefox', 'browser']
|
||||
}), createBookmarkItem({
|
||||
title: 'thunderbird',
|
||||
url: 'http://mozilla.org/thunderbird/',
|
||||
group: group,
|
||||
tags: ['mozilla', 'thunderbird', 'email']
|
||||
}), createBookmarkItem({
|
||||
title: 'moz subfolder',
|
||||
group: group,
|
||||
type: 'group'
|
||||
})
|
||||
]);
|
||||
})
|
||||
.then(results => {
|
||||
agg = agg.concat(results);
|
||||
let subfolder = results.filter(item => item.type === 'group')[0];
|
||||
return createBookmarkItem({
|
||||
title: 'dark javascript secrets',
|
||||
url: 'http://w3schools.com',
|
||||
group: subfolder,
|
||||
tags: []
|
||||
});
|
||||
}).then(item => {
|
||||
agg.push(item);
|
||||
return createBookmarkItem(
|
||||
{ type: 'group', group: MENU, title: 'other stuff' }
|
||||
);
|
||||
}).then(newGroup => {
|
||||
agg.push(newGroup);
|
||||
return all([
|
||||
createBookmarkItem({
|
||||
title: 'mdn',
|
||||
url: 'http://developer.mozilla.org/en-US/',
|
||||
group: newGroup,
|
||||
tags: ['javascript']
|
||||
}),
|
||||
createBookmarkItem({
|
||||
title: 'web audio',
|
||||
url: 'http://webaud.io',
|
||||
group: newGroup,
|
||||
tags: ['javascript', 'web audio']
|
||||
}),
|
||||
createBookmarkItem({
|
||||
title: 'web audio components',
|
||||
url: 'http://component.fm',
|
||||
group: newGroup,
|
||||
tags: ['javascript', 'web audio', 'components']
|
||||
})
|
||||
]);
|
||||
}).then(results => {
|
||||
agg = agg.concat(results);
|
||||
return agg;
|
||||
});
|
||||
}
|
||||
exports.createBookmarkTree = createBookmarkTree;
|
@ -113,7 +113,9 @@ exports.testTabProperties = function(test) {
|
||||
// TODO: remove need for this test by implementing the favicon feature
|
||||
// Poors man deepEqual with JSON.stringify...
|
||||
test.assertEqual(JSON.stringify(messages),
|
||||
JSON.stringify([ERR_FENNEC_MSG]),
|
||||
JSON.stringify(['tab.favicon is deprecated, and ' +
|
||||
'currently favicon helpers are not yet supported ' +
|
||||
'by Fennec']),
|
||||
"favicon logs an error for now");
|
||||
test.assertEqual(tab.style, null, "style of the new tab matches");
|
||||
test.assertEqual(tab.index, tabsLen, "index of the new tab matches");
|
||||
@ -576,29 +578,6 @@ exports.testPerTabEvents = function(test) {
|
||||
});
|
||||
};
|
||||
|
||||
// TEST: tabs.activeTab getter
|
||||
exports.testActiveTab_getter_alt = function(test) {
|
||||
test.waitUntilDone();
|
||||
|
||||
let url = URL.replace("#title#", "foo");
|
||||
tabs.open({
|
||||
url: url,
|
||||
onActivate: function(tab) {
|
||||
test.assertEqual(tabs.activeTab.url, tab.url, 'the active tab is correct');
|
||||
|
||||
tab.once('ready', function() {
|
||||
test.assertEqual(tab.url, url);
|
||||
test.assertEqual(tab.title, "foo");
|
||||
|
||||
tab.close(function() {
|
||||
// end test
|
||||
test.done();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
exports.testUniqueTabIds = function(test) {
|
||||
test.waitUntilDone();
|
||||
var tabs = require('sdk/tabs');
|
||||
|
@ -6,60 +6,37 @@
|
||||
const { Cc, Ci } = require('chrome');
|
||||
const { Loader } = require('sdk/test/loader');
|
||||
const timer = require('sdk/timers');
|
||||
const { getOwnerWindow } = require('sdk/private-browsing/window/utils');
|
||||
const { windows, onFocus, getMostRecentBrowserWindow } = require('sdk/window/utils');
|
||||
const { open, focus } = require('sdk/window/helpers');
|
||||
const { StringBundle } = require('sdk/deprecated/app-strings');
|
||||
const tabs = require('sdk/tabs');
|
||||
|
||||
const base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA" +
|
||||
"AABzenr0AAAASUlEQVRYhe3O0QkAIAwD0eyqe3Q993AQ3cBSUKpygfsNTy" +
|
||||
"N5ugbQpK0BAADgP0BRDWXWlwEAAAAAgPsA3rzDaAAAAHgPcGrpgAnzQ2FG" +
|
||||
"bWRR9AAAAABJRU5ErkJggg%3D%3D";
|
||||
|
||||
// TEST: tabs.activeTab getter
|
||||
exports.testActiveTab_getter = function(test) {
|
||||
test.waitUntilDone();
|
||||
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
|
||||
let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head></html>";
|
||||
require("sdk/deprecated/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());
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Bug 682681 - tab.title should never be empty
|
||||
exports.testBug682681_aboutURI = function(test) {
|
||||
test.waitUntilDone();
|
||||
|
||||
let tabStrings = StringBundle('chrome://browser/locale/tabbrowser.properties');
|
||||
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
tabs.on('ready', function onReady(tab) {
|
||||
tabs.removeListener('ready', onReady);
|
||||
|
||||
tabs.on('ready', function onReady(tab) {
|
||||
tabs.removeListener('ready', onReady);
|
||||
test.assertEqual(tab.title,
|
||||
tabStrings.get('tabs.emptyTabTitle'),
|
||||
"title of about: tab is not blank");
|
||||
|
||||
test.assertEqual(tab.title,
|
||||
tabStrings.get('tabs.emptyTabTitle'),
|
||||
"title of about: tab is not blank");
|
||||
tab.close(function() test.done());
|
||||
});
|
||||
|
||||
// end of test
|
||||
closeBrowserWindow(window, function() test.done());
|
||||
});
|
||||
|
||||
// open a about: url
|
||||
tabs.open({
|
||||
url: "about:blank",
|
||||
inBackground: true
|
||||
});
|
||||
// open a about: url
|
||||
tabs.open({
|
||||
url: "about:blank",
|
||||
inBackground: true
|
||||
});
|
||||
};
|
||||
|
||||
@ -67,23 +44,13 @@ exports.testBug682681_aboutURI = function(test) {
|
||||
exports.testTitleForDataURI = function(test) {
|
||||
test.waitUntilDone();
|
||||
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
|
||||
tabs.on('ready', function onReady(tab) {
|
||||
tabs.removeListener('ready', onReady);
|
||||
|
||||
tabs.open({
|
||||
url: "data:text/html;charset=utf-8,<title>tab</title>",
|
||||
inBackground: true,
|
||||
onReady: function(tab) {
|
||||
test.assertEqual(tab.title, "tab", "data: title is not Connecting...");
|
||||
|
||||
// end of test
|
||||
closeBrowserWindow(window, function() test.done());
|
||||
});
|
||||
|
||||
// open a about: url
|
||||
tabs.open({
|
||||
url: "data:text/html;charset=utf-8,<title>tab</title>",
|
||||
inBackground: true
|
||||
});
|
||||
tab.close(function() test.done());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -93,8 +60,6 @@ exports.testBrowserWindowCreationOnActivate = function(test) {
|
||||
test.waitUntilDone();
|
||||
|
||||
let windows = require("sdk/windows").browserWindows;
|
||||
let tabs = require("sdk/tabs");
|
||||
|
||||
let gotActivate = false;
|
||||
|
||||
tabs.once('activate', function onActivate(eventTab) {
|
||||
@ -102,114 +67,80 @@ exports.testBrowserWindowCreationOnActivate = function(test) {
|
||||
gotActivate = true;
|
||||
});
|
||||
|
||||
openBrowserWindow(function(window, browser) {
|
||||
open().then(function(window) {
|
||||
test.assert(gotActivate, "Received activate event before openBrowserWindow's callback is called");
|
||||
closeBrowserWindow(window, function () test.done());
|
||||
});
|
||||
}
|
||||
|
||||
// TEST: tab.activate()
|
||||
exports.testActiveTab_setter = function(test) {
|
||||
test.waitUntilDone();
|
||||
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
let url = "data:text/html;charset=utf-8,<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
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// TEST: tab unloader
|
||||
exports.testAutomaticDestroy = function(test) {
|
||||
test.waitUntilDone();
|
||||
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
// Create a second tab instance that we will destroy
|
||||
let called = false;
|
||||
|
||||
// Create a second tab instance that we will destroy
|
||||
let called = false;
|
||||
|
||||
let loader = Loader(module);
|
||||
let tabs2 = loader.require("sdk/tabs");
|
||||
tabs2.on('open', function onOpen(tab) {
|
||||
called = true;
|
||||
});
|
||||
|
||||
loader.unload();
|
||||
|
||||
// Fire a tab event and ensure that the destroyed tab is inactive
|
||||
tabs.once('open', function () {
|
||||
timer.setTimeout(function () {
|
||||
test.assert(!called, "Unloaded tab module is destroyed and inactive");
|
||||
closeBrowserWindow(window, function() test.done());
|
||||
}, 0);
|
||||
});
|
||||
|
||||
tabs.open("data:text/html;charset=utf-8,foo");
|
||||
let loader = Loader(module);
|
||||
let tabs2 = loader.require("sdk/tabs");
|
||||
tabs2.on('open', function onOpen(tab) {
|
||||
called = true;
|
||||
});
|
||||
|
||||
loader.unload();
|
||||
|
||||
// Fire a tab event and ensure that the destroyed tab is inactive
|
||||
tabs.once('open', function (tab) {
|
||||
timer.setTimeout(function () {
|
||||
test.assert(!called, "Unloaded tab module is destroyed and inactive");
|
||||
tab.close(test.done.bind(test));
|
||||
}, 0);
|
||||
});
|
||||
|
||||
tabs.open("data:text/html;charset=utf-8,foo");
|
||||
};
|
||||
|
||||
// test tab properties
|
||||
exports.testTabProperties = function(test) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require('sdk/tabs');
|
||||
let url = "data:text/html;charset=utf-8,<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");
|
||||
test.assertNotEqual(tab.id, null, "a tab object always has an id property.");
|
||||
onReadyOrLoad(window);
|
||||
},
|
||||
onLoad: 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");
|
||||
test.assertNotEqual(tab.id, null, "a tab object always has an id property.");
|
||||
onReadyOrLoad(window);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
function onReadyOrLoad (window) {
|
||||
if (count++)
|
||||
closeBrowserWindow(window, function() test.done());
|
||||
function onReadyOrLoad (tab) {
|
||||
if (count++) {
|
||||
tab.close(test.done.bind(test));
|
||||
}
|
||||
}
|
||||
|
||||
let url = "data:text/html;charset=utf-8,<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");
|
||||
test.assertNotEqual(tab.id, null, "a tab object always has an id property.");
|
||||
onReadyOrLoad(tab);
|
||||
},
|
||||
onLoad: 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");
|
||||
test.assertNotEqual(tab.id, null, "a tab object always has an id property.");
|
||||
onReadyOrLoad(tab);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// TEST: tab properties
|
||||
exports.testTabContentTypeAndReload = function(test) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
|
||||
open().then(focus).then(function(window) {
|
||||
let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head><body>foo</body></html>";
|
||||
let urlXML = "data:text/xml;charset=utf-8,<foo>bar</foo>";
|
||||
tabs.open({
|
||||
@ -230,12 +161,13 @@ exports.testTabContentTypeAndReload = function(test) {
|
||||
// TEST: tabs iterator and length property
|
||||
exports.testTabsIteratorAndLength = function(test) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
|
||||
open(null, { features: { chrome: true, toolbar: true } }).then(focus).then(function(window) {
|
||||
let startCount = 0;
|
||||
for each (let t in tabs) startCount++;
|
||||
test.assertEqual(startCount, tabs.length, "length property is correct");
|
||||
let url = "data:text/html;charset=utf-8,default";
|
||||
|
||||
tabs.open(url);
|
||||
tabs.open(url);
|
||||
tabs.open({
|
||||
@ -245,6 +177,7 @@ exports.testTabsIteratorAndLength = function(test) {
|
||||
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());
|
||||
}
|
||||
});
|
||||
@ -254,8 +187,8 @@ exports.testTabsIteratorAndLength = function(test) {
|
||||
// TEST: tab.url setter
|
||||
exports.testTabLocation = function(test) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
|
||||
open().then(focus).then(function(window) {
|
||||
let url1 = "data:text/html;charset=utf-8,foo";
|
||||
let url2 = "data:text/html;charset=utf-8,bar";
|
||||
|
||||
@ -279,45 +212,39 @@ exports.testTabLocation = function(test) {
|
||||
// TEST: tab.close()
|
||||
exports.testTabClose = function(test) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
let url = "data:text/html;charset=utf-8,foo";
|
||||
|
||||
test.assertNotEqual(tabs.activeTab.url, url, "tab is not 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");
|
||||
let secondOnCloseCalled = false;
|
||||
tab.close(function() {
|
||||
closeBrowserWindow(window, function() {
|
||||
test.assert(secondOnCloseCalled,
|
||||
"The immediate second call to tab.close gots its callback fired");
|
||||
test.assertNotEqual(tabs.activeTab.url, url, "tab is no longer the active tab");
|
||||
test.done()
|
||||
});
|
||||
let url = "data:text/html;charset=utf-8,foo";
|
||||
|
||||
test.assertNotEqual(tabs.activeTab.url, url, "tab is not 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");
|
||||
let secondOnCloseCalled = false;
|
||||
|
||||
// Bug 699450: Multiple calls to tab.close should not throw
|
||||
tab.close(function() secondOnCloseCalled = true);
|
||||
try {
|
||||
tab.close(function () {
|
||||
test.assert(secondOnCloseCalled,
|
||||
"The immediate second call to tab.close gots its callback fired");
|
||||
test.assertNotEqual(tabs.activeTab.url, url, "tab is no longer the active tab");
|
||||
test.done();
|
||||
});
|
||||
|
||||
// Bug 699450: Multiple calls to tab should not throw
|
||||
try {
|
||||
tab.close(function () {
|
||||
secondOnCloseCalled = true;
|
||||
});
|
||||
}
|
||||
catch(e) {
|
||||
test.fail("second call to tab.close() thrown an exception: " + e);
|
||||
}
|
||||
test.assertNotEqual(tabs.activeTab.url, url, "tab is no longer the active tab");
|
||||
});
|
||||
|
||||
tabs.open(url);
|
||||
}
|
||||
catch(e) {
|
||||
test.fail("second call to tab.close() thrown an exception: " + e);
|
||||
}
|
||||
test.assertNotEqual(tabs.activeTab.url, url, "tab is no longer the active tab");
|
||||
});
|
||||
|
||||
tabs.open(url);
|
||||
};
|
||||
|
||||
// TEST: tab.move()
|
||||
exports.testTabMove = function(test) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
|
||||
open().then(function(window) {
|
||||
let url = "data:text/html;charset=utf-8,foo";
|
||||
|
||||
tabs.open({
|
||||
@ -335,168 +262,124 @@ exports.testTabMove = function(test) {
|
||||
// TEST: open tab with default options
|
||||
exports.testOpen = function(test) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
let url = "data:text/html;charset=utf-8,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");
|
||||
test.assertEqual(tab.isPinned, false, "The new tab is not pinned");
|
||||
|
||||
closeBrowserWindow(window, function() test.done());
|
||||
}
|
||||
});
|
||||
let url = "data:text/html;charset=utf-8,default";
|
||||
tabs.open({
|
||||
url: url,
|
||||
onReady: function(tab) {
|
||||
test.assertEqual(tab.url, url, "URL of the new tab matches");
|
||||
test.assertEqual(tab.isPinned, false, "The new tab is not pinned");
|
||||
|
||||
tab.close(function() test.done());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// TEST: open pinned tab
|
||||
// TEST: opening a pinned tab
|
||||
exports.testOpenPinned = function(test) {
|
||||
const xulApp = require("sdk/system/xul-app");
|
||||
if (xulApp.versionInRange(xulApp.platformVersion, "2.0b2", "*")) {
|
||||
// test tab pinning
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
let url = "data:text/html;charset=utf-8,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.");
|
||||
}
|
||||
test.waitUntilDone();
|
||||
|
||||
let url = "data:text/html;charset=utf-8,default";
|
||||
tabs.open({
|
||||
url: url,
|
||||
isPinned: true,
|
||||
onOpen: function(tab) {
|
||||
test.assertEqual(tab.isPinned, true, "The new tab is pinned");
|
||||
tab.close(test.done.bind(test));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// TEST: pin/unpin opened tab
|
||||
exports.testPinUnpin = function(test) {
|
||||
const xulApp = require("sdk/system/xul-app");
|
||||
if (xulApp.versionInRange(xulApp.platformVersion, "2.0b2", "*")) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
let url = "data:text/html;charset=utf-8,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.");
|
||||
}
|
||||
};
|
||||
test.waitUntilDone();
|
||||
|
||||
let url = "data:text/html;charset=utf-8,default";
|
||||
tabs.open({
|
||||
url: url,
|
||||
inBackground: true,
|
||||
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");
|
||||
tab.close(test.done.bind(test));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TEST: open tab in background
|
||||
exports.testInBackground = function(test) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
let activeUrl = tabs.activeTab.url;
|
||||
let url = "data:text/html;charset=utf-8,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
|
||||
});
|
||||
|
||||
let window = getMostRecentBrowserWindow();
|
||||
let activeUrl = tabs.activeTab.url;
|
||||
let url = "data:text/html;charset=utf-8,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");
|
||||
tab.close(test.done.bind(test));
|
||||
});
|
||||
};
|
||||
|
||||
tabs.open({
|
||||
url: url,
|
||||
inBackground: true
|
||||
});
|
||||
}
|
||||
|
||||
// TEST: open tab in new window
|
||||
exports.testOpenInNewWindow = function(test) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
|
||||
let cache = [];
|
||||
let windowUtils = require("sdk/deprecated/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 startWindowCount = windows().length;
|
||||
|
||||
let url = "data:text/html;charset=utf-8,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");
|
||||
let url = "data:text/html;charset=utf-8,testOpenInNewWindow";
|
||||
tabs.open({
|
||||
url: url,
|
||||
inNewWindow: true,
|
||||
onReady: function(tab) {
|
||||
let newWindow = getOwnerWindow(tab);
|
||||
test.assertEqual(windows().length, startWindowCount + 1, "a new window was opened");
|
||||
|
||||
onFocus(newWindow).then(function() {
|
||||
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 (let i in cache) cache[i] = null;
|
||||
wt.unload();
|
||||
closeBrowserWindow(newWindow, function() {
|
||||
closeBrowserWindow(window, function() test.done());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
closeBrowserWindow(newWindow, test.done.bind(test));
|
||||
}, test.fail).then(null, test.fail);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// Test tab.open inNewWindow + onOpen combination
|
||||
exports.testOpenInNewWindowOnOpen = function(test) {
|
||||
test.waitUntilDone();
|
||||
let tabs = require("sdk/tabs");
|
||||
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let cache = [];
|
||||
let windowUtils = require("sdk/deprecated/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 startWindowCount = windows().length;
|
||||
|
||||
let url = "data:text/html;charset=utf-8,newwindow";
|
||||
tabs.open({
|
||||
url: url,
|
||||
inNewWindow: true,
|
||||
onOpen: function(tab) {
|
||||
let newWindow = cache[cache.length - 1];
|
||||
test.assertEqual(cache.length, startWindowCount + 1, "a new window was opened");
|
||||
let url = "data:text/html;charset=utf-8,newwindow";
|
||||
tabs.open({
|
||||
url: url,
|
||||
inNewWindow: true,
|
||||
onOpen: function(tab) {
|
||||
let newWindow = getOwnerWindow(tab);
|
||||
|
||||
onFocus(newWindow).then(function() {
|
||||
test.assertEqual(windows().length, startWindowCount + 1, "a new window was opened");
|
||||
test.assertEqual(activeWindow, newWindow, "new window is active");
|
||||
|
||||
for (let i in cache) cache[i] = null;
|
||||
wt.unload();
|
||||
|
||||
closeBrowserWindow(newWindow, function() {
|
||||
closeBrowserWindow(window, function() test.done());
|
||||
test.done();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -504,7 +387,6 @@ exports.testOpenInNewWindowOnOpen = function(test) {
|
||||
exports.testTabsEvent_onOpen = function(test) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
var tabs = require("sdk/tabs");
|
||||
let url = "data:text/html;charset=utf-8,1";
|
||||
let eventCount = 0;
|
||||
|
||||
@ -530,7 +412,6 @@ exports.testTabsEvent_onOpen = function(test) {
|
||||
exports.testTabsEvent_onClose = function(test) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
var tabs = require("sdk/tabs");
|
||||
let url = "data:text/html;charset=utf-8,onclose";
|
||||
let eventCount = 0;
|
||||
|
||||
@ -562,8 +443,6 @@ exports.testTabsEvent_onCloseWindow = function(test) {
|
||||
test.waitUntilDone();
|
||||
|
||||
openBrowserWindow(function(window, browser) {
|
||||
var tabs = require("sdk/tabs");
|
||||
|
||||
let closeCount = 0, individualCloseCount = 0;
|
||||
function listener() {
|
||||
closeCount++;
|
||||
@ -616,7 +495,6 @@ exports.testTabsEvent_onCloseWindow = function(test) {
|
||||
exports.testTabsEvent_onReady = function(test) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
var tabs = require("sdk/tabs");
|
||||
let url = "data:text/html;charset=utf-8,onready";
|
||||
let eventCount = 0;
|
||||
|
||||
@ -642,7 +520,6 @@ exports.testTabsEvent_onReady = function(test) {
|
||||
exports.testTabsEvent_onActivate = function(test) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
var tabs = require("sdk/tabs");
|
||||
let url = "data:text/html;charset=utf-8,onactivate";
|
||||
let eventCount = 0;
|
||||
|
||||
@ -668,7 +545,6 @@ exports.testTabsEvent_onActivate = function(test) {
|
||||
exports.testTabsEvent_onDeactivate = function(test) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
var tabs = require("sdk/tabs");
|
||||
let url = "data:text/html;charset=utf-8,ondeactivate";
|
||||
let eventCount = 0;
|
||||
|
||||
@ -699,7 +575,6 @@ exports.testTabsEvent_onDeactivate = function(test) {
|
||||
exports.testTabsEvent_pinning = function(test) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
var tabs = require("sdk/tabs");
|
||||
let url = "data:text/html;charset=utf-8,1";
|
||||
|
||||
tabs.on('open', function onOpen(tab) {
|
||||
@ -727,7 +602,6 @@ exports.testTabsEvent_pinning = function(test) {
|
||||
exports.testPerTabEvents = function(test) {
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
var tabs = require("sdk/tabs");
|
||||
let eventCount = 0;
|
||||
|
||||
tabs.open({
|
||||
@ -755,8 +629,6 @@ 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("sdk/tabs");
|
||||
|
||||
tabs.open({
|
||||
url: "data:text/html;charset=utf-8,foobar",
|
||||
onOpen: function (tab) {
|
||||
@ -779,7 +651,6 @@ exports.testAttachOnMultipleDocuments = function (test) {
|
||||
// Example of attach that process multiple tab documents
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
let firstLocation = "data:text/html;charset=utf-8,foobar";
|
||||
let secondLocation = "data:text/html;charset=utf-8,bar";
|
||||
let thirdLocation = "data:text/html;charset=utf-8,fox";
|
||||
@ -861,7 +732,6 @@ exports.testAttachWrappers = function (test) {
|
||||
// Check that content script has access to wrapped values by default
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
let document = "data:text/html;charset=utf-8,<script>var globalJSVar = true; " +
|
||||
" document.getElementById = 3;</script>";
|
||||
let count = 0;
|
||||
@ -895,7 +765,6 @@ exports.testAttachUnwrapped = function (test) {
|
||||
// Check that content script has access to unwrapped values through unsafeWindow
|
||||
test.waitUntilDone();
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require("sdk/tabs");
|
||||
let document = "data:text/html;charset=utf-8,<script>var globalJSVar=true;</script>";
|
||||
let count = 0;
|
||||
|
||||
@ -924,7 +793,6 @@ exports['test window focus changes active tab'] = function(test) {
|
||||
test.waitUntilDone();
|
||||
let win1 = openBrowserWindow(function() {
|
||||
let win2 = openBrowserWindow(function() {
|
||||
let tabs = require("sdk/tabs");
|
||||
tabs.on("activate", function onActivate() {
|
||||
tabs.removeListener("activate", onActivate);
|
||||
test.pass("activate was called on windows focus change.");
|
||||
@ -993,7 +861,6 @@ exports.testOnLoadEventWithDOM = function(test) {
|
||||
test.waitUntilDone();
|
||||
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require('sdk/tabs');
|
||||
let count = 0;
|
||||
tabs.on('load', function onLoad(tab) {
|
||||
test.assertEqual(tab.title, 'tab', 'tab passed in as arg, load called');
|
||||
@ -1021,7 +888,6 @@ exports.testOnLoadEventWithImage = function(test) {
|
||||
test.waitUntilDone();
|
||||
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require('sdk/tabs');
|
||||
let count = 0;
|
||||
tabs.on('load', function onLoad(tab) {
|
||||
if (!count++) {
|
||||
@ -1050,8 +916,6 @@ exports.testOnPageShowEvent = function (test) {
|
||||
let secondUrl = 'data:text/html;charset=utf-8,Second';
|
||||
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let tabs = require('sdk/tabs');
|
||||
|
||||
let counter = 0;
|
||||
tabs.on('pageshow', function onPageShow(tab, persisted) {
|
||||
counter++;
|
||||
@ -1150,7 +1014,3 @@ function closeBrowserWindow(window, callback) {
|
||||
}, false);
|
||||
window.close();
|
||||
}
|
||||
|
||||
// Test disabled on Linux because of bug 882867
|
||||
if (require("sdk/system/runtime").OS == "Linux")
|
||||
module.exports = {};
|
||||
|
@ -55,6 +55,7 @@ exports['test that add-on page has no chrome'] = function(assert, done) {
|
||||
closeTab(tab);
|
||||
assert.ok(isChromeVisible(window), 'chrome is visible again');
|
||||
loader.unload();
|
||||
assert.ok(!isTabOpen(tab), 'add-on page tab is closed on unload');
|
||||
done();
|
||||
});
|
||||
};
|
||||
@ -79,6 +80,7 @@ exports['test that add-on page with hash has no chrome'] = function(assert, done
|
||||
closeTab(tab);
|
||||
assert.ok(isChromeVisible(window), 'chrome is visible again');
|
||||
loader.unload();
|
||||
assert.ok(!isTabOpen(tab), 'add-on page tab is closed on unload');
|
||||
done();
|
||||
});
|
||||
};
|
||||
@ -103,6 +105,7 @@ exports['test that add-on page with querystring has no chrome'] = function(asser
|
||||
closeTab(tab);
|
||||
assert.ok(isChromeVisible(window), 'chrome is visible again');
|
||||
loader.unload();
|
||||
assert.ok(!isTabOpen(tab), 'add-on page tab is closed on unload');
|
||||
done();
|
||||
});
|
||||
};
|
||||
@ -127,6 +130,7 @@ exports['test that add-on page with hash and querystring has no chrome'] = funct
|
||||
closeTab(tab);
|
||||
assert.ok(isChromeVisible(window), 'chrome is visible again');
|
||||
loader.unload();
|
||||
assert.ok(!isTabOpen(tab), 'add-on page tab is closed on unload');
|
||||
done();
|
||||
});
|
||||
};
|
||||
@ -151,19 +155,4 @@ exports['test that malformed uri is not an addon-page'] = function(assert, done)
|
||||
});
|
||||
};
|
||||
|
||||
exports['test that add-on pages are closed on unload'] = function(assert, done) {
|
||||
let { loader } = LoaderWithHookedConsole(module);
|
||||
loader.require('sdk/addon-page');
|
||||
|
||||
tabs.open({
|
||||
url: uri,
|
||||
onReady: function listener(tab) {
|
||||
loader.unload();
|
||||
assert.ok(!isTabOpen(tab), 'add-on page tabs are closed on unload');
|
||||
|
||||
done();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
require('test').run(exports);
|
||||
|
@ -2,7 +2,7 @@
|
||||
* License, v. 2.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";
|
||||
"use strict";
|
||||
|
||||
const { Cc, Ci } = require("chrome");
|
||||
const { setTimeout } = require("sdk/timers");
|
||||
|
462
addon-sdk/source/test/test-fs.js
Normal file
462
addon-sdk/source/test/test-fs.js
Normal file
@ -0,0 +1,462 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.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("sdk/system");
|
||||
const fs = require("sdk/io/fs");
|
||||
const url = require("sdk/url");
|
||||
const path = require("sdk/fs/path");
|
||||
const { Buffer } = require("sdk/io/buffer");
|
||||
|
||||
// Use profile directory to list / read / write files.
|
||||
const profilePath = pathFor("ProfD");
|
||||
const fileNameInProfile = "compatibility.ini";
|
||||
const dirNameInProfile = "extensions";
|
||||
const filePathInProfile = path.join(profilePath, fileNameInProfile);
|
||||
const dirPathInProfile = path.join(profilePath, dirNameInProfile);
|
||||
const mkdirPath = path.join(profilePath, "sdk-fixture-mkdir");
|
||||
const writePath = path.join(profilePath, "sdk-fixture-writeFile");
|
||||
const unlinkPath = path.join(profilePath, "sdk-fixture-unlink");
|
||||
const truncatePath = path.join(profilePath, "sdk-fixture-truncate");
|
||||
const renameFromPath = path.join(profilePath, "sdk-fixture-rename-from");
|
||||
const renameToPath = path.join(profilePath, "sdk-fixture-rename-to");
|
||||
|
||||
const profileEntries = [
|
||||
"compatibility.ini",
|
||||
"extensions",
|
||||
"extensions.ini",
|
||||
"prefs.js"
|
||||
// There are likely to be a lot more files but we can't really
|
||||
// on consistent list so we limit to this.
|
||||
];
|
||||
|
||||
exports["test readir"] = function(assert, end) {
|
||||
var async = false;
|
||||
fs.readdir(profilePath, function(error, entries) {
|
||||
assert.ok(async, "readdir is async");
|
||||
assert.ok(!error, "there is no error when reading directory");
|
||||
assert.ok(profileEntries.length <= entries.length,
|
||||
"got et least number of entries we expect");
|
||||
assert.ok(profileEntries.every(function(entry) {
|
||||
return entries.indexOf(entry) >= 0;
|
||||
}), "all profiles are present");
|
||||
end();
|
||||
});
|
||||
|
||||
async = true;
|
||||
};
|
||||
|
||||
exports["test readdir error"] = function(assert, end) {
|
||||
var async = false;
|
||||
var path = profilePath + "-does-not-exists";
|
||||
fs.readdir(path, function(error, entries) {
|
||||
assert.ok(async, "readdir is async");
|
||||
assert.equal(error.message, "ENOENT, readdir " + path);
|
||||
assert.equal(error.code, "ENOENT", "error has a code");
|
||||
assert.equal(error.path, path, "error has a path");
|
||||
assert.equal(error.errno, 34, "error has a errno");
|
||||
end();
|
||||
});
|
||||
|
||||
async = true;
|
||||
};
|
||||
|
||||
exports["test readdirSync"] = function(assert) {
|
||||
var async = false;
|
||||
var entries = fs.readdirSync(profilePath);
|
||||
assert.ok(profileEntries.length <= entries.length,
|
||||
"got et least number of entries we expect");
|
||||
assert.ok(profileEntries.every(function(entry) {
|
||||
return entries.indexOf(entry) >= 0;
|
||||
}), "all profiles are present");
|
||||
};
|
||||
|
||||
exports["test readirSync error"] = function(assert) {
|
||||
var async = false;
|
||||
var path = profilePath + "-does-not-exists";
|
||||
try {
|
||||
fs.readdirSync(path);
|
||||
assert.fail(Error("No error was thrown"));
|
||||
} catch (error) {
|
||||
assert.equal(error.message, "ENOENT, readdir " + path);
|
||||
assert.equal(error.code, "ENOENT", "error has a code");
|
||||
assert.equal(error.path, path, "error has a path");
|
||||
assert.equal(error.errno, 34, "error has a errno");
|
||||
}
|
||||
};
|
||||
|
||||
exports["test readFile"] = function(assert, end) {
|
||||
let async = false;
|
||||
fs.readFile(filePathInProfile, function(error, content) {
|
||||
assert.ok(async, "readFile is async");
|
||||
assert.ok(!error, "error is falsy");
|
||||
assert.ok(Buffer.isBuffer(content), "readFile returns buffer");
|
||||
assert.ok(typeof(content.length) === "number", "buffer has length");
|
||||
assert.ok(content.toString().indexOf("[Compatibility]") >= 0,
|
||||
"content contains expected data");
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
};
|
||||
|
||||
exports["test readFile error"] = function(assert, end) {
|
||||
let async = false;
|
||||
let path = filePathInProfile + "-does-not-exists";
|
||||
fs.readFile(path, function(error, content) {
|
||||
assert.ok(async, "readFile is async");
|
||||
assert.equal(error.message, "ENOENT, open " + path);
|
||||
assert.equal(error.code, "ENOENT", "error has a code");
|
||||
assert.equal(error.path, path, "error has a path");
|
||||
assert.equal(error.errno, 34, "error has a errno");
|
||||
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
};
|
||||
|
||||
exports["test readFileSync not implemented"] = function(assert) {
|
||||
let buffer = fs.readFileSync(filePathInProfile);
|
||||
assert.ok(buffer.toString().indexOf("[Compatibility]") >= 0,
|
||||
"read content");
|
||||
};
|
||||
|
||||
exports["test fs.stat file"] = function(assert, end) {
|
||||
let async = false;
|
||||
let path = filePathInProfile;
|
||||
fs.stat(path, function(error, stat) {
|
||||
assert.ok(async, "fs.stat is async");
|
||||
assert.ok(!error, "error is falsy");
|
||||
assert.ok(!stat.isDirectory(), "not a dir");
|
||||
assert.ok(stat.isFile(), "is a file");
|
||||
assert.ok(!stat.isSymbolicLink(), "isn't a symlink");
|
||||
assert.ok(typeof(stat.size) === "number", "size is a number");
|
||||
assert.ok(stat.exists === true, "file exists");
|
||||
assert.ok(typeof(stat.isBlockDevice()) === "boolean");
|
||||
assert.ok(typeof(stat.isCharacterDevice()) === "boolean");
|
||||
assert.ok(typeof(stat.isFIFO()) === "boolean");
|
||||
assert.ok(typeof(stat.isSocket()) === "boolean");
|
||||
assert.ok(typeof(stat.hidden) === "boolean");
|
||||
assert.ok(typeof(stat.writable) === "boolean")
|
||||
assert.ok(stat.readable === true);
|
||||
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
};
|
||||
|
||||
exports["test fs.stat dir"] = function(assert, end) {
|
||||
let async = false;
|
||||
let path = dirPathInProfile;
|
||||
fs.stat(path, function(error, stat) {
|
||||
assert.ok(async, "fs.stat is async");
|
||||
assert.ok(!error, "error is falsy");
|
||||
assert.ok(stat.isDirectory(), "is a dir");
|
||||
assert.ok(!stat.isFile(), "not a file");
|
||||
assert.ok(!stat.isSymbolicLink(), "isn't a symlink");
|
||||
assert.ok(typeof(stat.size) === "number", "size is a number");
|
||||
assert.ok(stat.exists === true, "file exists");
|
||||
assert.ok(typeof(stat.isBlockDevice()) === "boolean");
|
||||
assert.ok(typeof(stat.isCharacterDevice()) === "boolean");
|
||||
assert.ok(typeof(stat.isFIFO()) === "boolean");
|
||||
assert.ok(typeof(stat.isSocket()) === "boolean");
|
||||
assert.ok(typeof(stat.hidden) === "boolean");
|
||||
assert.ok(typeof(stat.writable) === "boolean")
|
||||
assert.ok(typeof(stat.readable) === "boolean");
|
||||
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
};
|
||||
|
||||
exports["test fs.stat error"] = function(assert, end) {
|
||||
let async = false;
|
||||
let path = filePathInProfile + "-does-not-exists";
|
||||
fs.stat(path, function(error, stat) {
|
||||
assert.ok(async, "fs.stat is async");
|
||||
assert.equal(error.message, "ENOENT, stat " + path);
|
||||
assert.equal(error.code, "ENOENT", "error has a code");
|
||||
assert.equal(error.path, path, "error has a path");
|
||||
assert.equal(error.errno, 34, "error has a errno");
|
||||
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
};
|
||||
|
||||
exports["test fs.exists NO"] = function(assert, end) {
|
||||
let async = false
|
||||
let path = filePathInProfile + "-does-not-exists";
|
||||
fs.exists(path, function(error, value) {
|
||||
assert.ok(async, "fs.exists is async");
|
||||
assert.ok(!error, "error is falsy");
|
||||
assert.ok(!value, "file does not exists");
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
};
|
||||
|
||||
exports["test fs.exists YES"] = function(assert, end) {
|
||||
let async = false
|
||||
let path = filePathInProfile
|
||||
fs.exists(path, function(error, value) {
|
||||
assert.ok(async, "fs.exists is async");
|
||||
assert.ok(!error, "error is falsy");
|
||||
assert.ok(value, "file exists");
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
};
|
||||
|
||||
exports["test fs.exists NO"] = function(assert, end) {
|
||||
let async = false
|
||||
let path = filePathInProfile + "-does-not-exists";
|
||||
fs.exists(path, function(error, value) {
|
||||
assert.ok(async, "fs.exists is async");
|
||||
assert.ok(!error, "error is falsy");
|
||||
assert.ok(!value, "file does not exists");
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
};
|
||||
|
||||
exports["test fs.existsSync"] = function(assert) {
|
||||
let path = filePathInProfile
|
||||
assert.equal(fs.existsSync(path), true, "exists");
|
||||
assert.equal(fs.existsSync(path + "-does-not-exists"), false, "exists");
|
||||
};
|
||||
|
||||
exports["test fs.mkdirSync fs.rmdirSync"] = function(assert) {
|
||||
let path = mkdirPath;
|
||||
|
||||
assert.equal(fs.existsSync(path), false, "does not exists");
|
||||
fs.mkdirSync(path);
|
||||
assert.equal(fs.existsSync(path), true, "dir was created");
|
||||
try {
|
||||
fs.mkdirSync(path);
|
||||
assert.fail(Error("mkdir on existing should throw"));
|
||||
} catch (error) {
|
||||
assert.equal(error.message, "EEXIST, mkdir " + path);
|
||||
assert.equal(error.code, "EEXIST", "error has a code");
|
||||
assert.equal(error.path, path, "error has a path");
|
||||
assert.equal(error.errno, 47, "error has a errno");
|
||||
}
|
||||
fs.rmdirSync(path);
|
||||
assert.equal(fs.existsSync(path), false, "dir was removed");
|
||||
};
|
||||
|
||||
exports["test fs.mkdir"] = function(assert, end) {
|
||||
let path = mkdirPath;
|
||||
|
||||
if (!fs.existsSync(path)) {
|
||||
let async = false;
|
||||
fs.mkdir(path, function(error) {
|
||||
assert.ok(async, "mkdir is async");
|
||||
assert.ok(!error, "no error");
|
||||
assert.equal(fs.existsSync(path), true, "dir was created");
|
||||
fs.rmdirSync(path);
|
||||
assert.equal(fs.existsSync(path), false, "dir was removed");
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
}
|
||||
};
|
||||
|
||||
exports["test fs.mkdir error"] = function(assert, end) {
|
||||
let path = mkdirPath;
|
||||
|
||||
if (!fs.existsSync(path)) {
|
||||
fs.mkdirSync(path);
|
||||
let async = false;
|
||||
fs.mkdir(path, function(error) {
|
||||
assert.ok(async, "mkdir is async");
|
||||
assert.equal(error.message, "EEXIST, mkdir " + path);
|
||||
assert.equal(error.code, "EEXIST", "error has a code");
|
||||
assert.equal(error.path, path, "error has a path");
|
||||
assert.equal(error.errno, 47, "error has a errno");
|
||||
fs.rmdirSync(path);
|
||||
assert.equal(fs.existsSync(path), false, "dir was removed");
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
}
|
||||
};
|
||||
|
||||
exports["test fs.rmdir"] = function(assert, end) {
|
||||
let path = mkdirPath;
|
||||
|
||||
if (!fs.existsSync(path)) {
|
||||
fs.mkdirSync(path);
|
||||
assert.equal(fs.existsSync(path), true, "dir exists");
|
||||
let async = false;
|
||||
fs.rmdir(path, function(error) {
|
||||
assert.ok(async, "mkdir is async");
|
||||
assert.ok(!error, "no error");
|
||||
assert.equal(fs.existsSync(path), false, "dir was removed");
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
exports["test fs.rmdir error"] = function(assert, end) {
|
||||
let path = mkdirPath;
|
||||
|
||||
if (!fs.existsSync(path)) {
|
||||
assert.equal(fs.existsSync(path), false, "dir doesn't exists");
|
||||
let async = false;
|
||||
fs.rmdir(path, function(error) {
|
||||
assert.ok(async, "mkdir is async");
|
||||
assert.equal(error.message, "ENOENT, remove " + path);
|
||||
assert.equal(error.code, "ENOENT", "error has a code");
|
||||
assert.equal(error.path, path, "error has a path");
|
||||
assert.equal(error.errno, 34, "error has a errno");
|
||||
assert.equal(fs.existsSync(path), false, "dir is removed");
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
}
|
||||
};
|
||||
|
||||
exports["test fs.truncateSync fs.unlinkSync"] = function(assert) {
|
||||
let path = truncatePath;
|
||||
|
||||
assert.equal(fs.existsSync(path), false, "does not exists");
|
||||
fs.truncateSync(path);
|
||||
assert.equal(fs.existsSync(path), true, "file was created");
|
||||
fs.truncateSync(path);
|
||||
fs.unlinkSync(path);
|
||||
assert.equal(fs.existsSync(path), false, "file was removed");
|
||||
};
|
||||
|
||||
|
||||
exports["test fs.truncate"] = function(assert, end) {
|
||||
let path = truncatePath;
|
||||
if (!fs.existsSync(path)) {
|
||||
let async = false;
|
||||
fs.truncate(path, 0, function(error) {
|
||||
assert.ok(async, "truncate is async");
|
||||
console.log(error);
|
||||
assert.ok(!error, "no error");
|
||||
assert.equal(fs.existsSync(path), true, "file was created");
|
||||
fs.unlinkSync(path);
|
||||
assert.equal(fs.existsSync(path), false, "file was removed");
|
||||
end();
|
||||
})
|
||||
async = true;
|
||||
}
|
||||
};
|
||||
|
||||
exports["test fs.unlink"] = function(assert, end) {
|
||||
let path = unlinkPath;
|
||||
let async = false;
|
||||
assert.ok(!fs.existsSync(path), "file doesn't exists yet");
|
||||
fs.truncateSync(path, 0);
|
||||
assert.ok(fs.existsSync(path), "file was created");
|
||||
fs.unlink(path, function(error) {
|
||||
assert.ok(async, "fs.unlink is async");
|
||||
assert.ok(!error, "error is falsy");
|
||||
assert.ok(!fs.existsSync(path), "file was removed");
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
};
|
||||
|
||||
exports["test fs.unlink error"] = function(assert, end) {
|
||||
let path = unlinkPath;
|
||||
let async = false;
|
||||
assert.ok(!fs.existsSync(path), "file doesn't exists yet");
|
||||
fs.unlink(path, function(error) {
|
||||
assert.ok(async, "fs.unlink is async");
|
||||
assert.equal(error.message, "ENOENT, remove " + path);
|
||||
assert.equal(error.code, "ENOENT", "error has a code");
|
||||
assert.equal(error.path, path, "error has a path");
|
||||
assert.equal(error.errno, 34, "error has a errno");
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
};
|
||||
|
||||
exports["test fs.rename"] = function(assert, end) {
|
||||
let fromPath = renameFromPath;
|
||||
let toPath = renameToPath;
|
||||
|
||||
fs.truncateSync(fromPath);
|
||||
assert.ok(fs.existsSync(fromPath), "source file exists");
|
||||
assert.ok(!fs.existsSync(toPath), "destination doesn't exists");
|
||||
var async = false;
|
||||
fs.rename(fromPath, toPath, function(error) {
|
||||
assert.ok(async, "fs.rename is async");
|
||||
assert.ok(!error, "error is falsy");
|
||||
assert.ok(!fs.existsSync(fromPath), "source path no longer exists");
|
||||
assert.ok(fs.existsSync(toPath), "destination file exists");
|
||||
fs.unlinkSync(toPath);
|
||||
assert.ok(!fs.existsSync(toPath), "cleaned up properly");
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
};
|
||||
|
||||
exports["test fs.rename (missing source file)"] = function(assert, end) {
|
||||
let fromPath = renameFromPath;
|
||||
let toPath = renameToPath;
|
||||
|
||||
assert.ok(!fs.existsSync(fromPath), "source file doesn't exists");
|
||||
assert.ok(!fs.existsSync(toPath), "destination doesn't exists");
|
||||
var async = false;
|
||||
fs.rename(fromPath, toPath, function(error) {
|
||||
assert.ok(async, "fs.rename is async");
|
||||
assert.equal(error.message, "ENOENT, rename " + fromPath);
|
||||
assert.equal(error.code, "ENOENT", "error has a code");
|
||||
assert.equal(error.path, fromPath, "error has a path");
|
||||
assert.equal(error.errno, 34, "error has a errno");
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
};
|
||||
|
||||
exports["test fs.rename (existing target file)"] = function(assert, end) {
|
||||
let fromPath = renameFromPath;
|
||||
let toPath = renameToPath;
|
||||
|
||||
fs.truncateSync(fromPath);
|
||||
fs.truncateSync(toPath);
|
||||
assert.ok(fs.existsSync(fromPath), "source file exists");
|
||||
assert.ok(fs.existsSync(toPath), "destination file exists");
|
||||
var async = false;
|
||||
fs.rename(fromPath, toPath, function(error) {
|
||||
assert.ok(async, "fs.rename is async");
|
||||
assert.ok(!error, "error is falsy");
|
||||
assert.ok(!fs.existsSync(fromPath), "source path no longer exists");
|
||||
assert.ok(fs.existsSync(toPath), "destination file exists");
|
||||
fs.unlinkSync(toPath);
|
||||
assert.ok(!fs.existsSync(toPath), "cleaned up properly");
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
};
|
||||
|
||||
exports["test fs.writeFile"] = function(assert, end) {
|
||||
let path = writePath;
|
||||
let content = ["hello world",
|
||||
"this is some text"].join("\n");
|
||||
|
||||
var async = false;
|
||||
fs.writeFile(path, content, function(error) {
|
||||
assert.ok(async, "fs write is async");
|
||||
assert.ok(!error, "error is falsy");
|
||||
assert.ok(fs.existsSync(path), "file was created");
|
||||
assert.equal(fs.readFileSync(path).toString(),
|
||||
content,
|
||||
"contet was written");
|
||||
fs.unlinkSync(path);
|
||||
assert.ok(!fs.exists(path), "file was removed");
|
||||
|
||||
end();
|
||||
});
|
||||
async = true;
|
||||
};
|
||||
|
||||
require("test").run(exports);
|
99
addon-sdk/source/test/test-host-events.js
Normal file
99
addon-sdk/source/test/test-host-events.js
Normal file
@ -0,0 +1,99 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.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 { defer, all } = require('sdk/core/promise');
|
||||
const { setTimeout } = require('sdk/timers');
|
||||
const { request, response } = require('sdk/addon/host');
|
||||
const { send } = require('sdk/addon/events');
|
||||
const { filter } = require('sdk/event/utils');
|
||||
const { on, emit, off } = require('sdk/event/core');
|
||||
|
||||
let stream = filter(request, (data) => /sdk-x-test/.test(data.event));
|
||||
|
||||
exports.testSend = function (assert, done) {
|
||||
on(stream, 'data', handle);
|
||||
send('sdk-x-test-simple', { title: 'my test data' }).then((data) => {
|
||||
assert.equal(data.title, 'my response', 'response is handled');
|
||||
off(stream, 'data', handle);
|
||||
done();
|
||||
}, (reason) => {
|
||||
assert.fail('should not call reject');
|
||||
});
|
||||
function handle (e) {
|
||||
assert.equal(e.event, 'sdk-x-test-simple', 'correct event name');
|
||||
assert.ok(e.id != null, 'message has an ID');
|
||||
assert.equal(e.data.title, 'my test data', 'serialized data passes');
|
||||
e.data.title = 'my response';
|
||||
emit(response, 'data', e);
|
||||
}
|
||||
};
|
||||
|
||||
exports.testSendError = function (assert, done) {
|
||||
on(stream, 'data', handle);
|
||||
send('sdk-x-test-error', { title: 'my test data' }).then((data) => {
|
||||
assert.fail('should not call success');
|
||||
}, (reason) => {
|
||||
assert.equal(reason, 'ErrorInfo', 'should reject with error/reason');
|
||||
off(stream, 'data', handle);
|
||||
done();
|
||||
});
|
||||
function handle (e) {
|
||||
e.error = 'ErrorInfo';
|
||||
emit(response, 'data', e);
|
||||
}
|
||||
};
|
||||
|
||||
exports.testMultipleSends = function (assert, done) {
|
||||
let count = 0;
|
||||
let ids = [];
|
||||
on(stream, 'data', handle);
|
||||
['firefox', 'thunderbird', 'rust'].map(data =>
|
||||
send('sdk-x-test-multi', { data: data }).then(val => {
|
||||
assert.ok(val === 'firefox' || val === 'rust', 'successful calls resolve correctly');
|
||||
if (++count === 3) {
|
||||
off(stream, 'data', handle);
|
||||
done();
|
||||
}
|
||||
}, reason => {
|
||||
assert.equal(reason.error, 'too blue', 'rejected calls are rejected');
|
||||
if (++count === 3) {
|
||||
off(stream, 'data', handle);
|
||||
done();
|
||||
}
|
||||
}));
|
||||
|
||||
function handle (e) {
|
||||
if (e.data !== 'firefox' || e.data !== 'rust')
|
||||
e.error = { data: e.data, error: 'too blue' };
|
||||
assert.ok(!~ids.indexOf(e.id), 'ID should be unique');
|
||||
assert.equal(e.event, 'sdk-x-test-multi', 'has event name');
|
||||
ids.push(e.id);
|
||||
emit(response, 'data', e);
|
||||
}
|
||||
};
|
||||
|
||||
exports.testSerialization = function (assert, done) {
|
||||
on(stream, 'data', handle);
|
||||
let object = { title: 'my test data' };
|
||||
let resObject;
|
||||
send('sdk-x-test-serialize', object).then(data => {
|
||||
data.title = 'another title';
|
||||
assert.equal(object.title, 'my test data', 'original object not modified');
|
||||
assert.equal(resObject.title, 'new title', 'object passed by value from host');
|
||||
off(stream, 'data', handle);
|
||||
done();
|
||||
}, (reason) => {
|
||||
assert.fail('should not call reject');
|
||||
});
|
||||
function handle (e) {
|
||||
e.data.title = 'new title';
|
||||
assert.equal(object.title, 'my test data', 'object passed by value to host');
|
||||
resObject = e.data;
|
||||
emit(response, 'data', e);
|
||||
}
|
||||
};
|
||||
|
||||
require('test').run(exports);
|
@ -5,7 +5,7 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { MatchPattern } = require("sdk/page-mod/match-pattern");
|
||||
const { MatchPattern } = require("sdk/util/match-pattern");
|
||||
|
||||
exports.testMatchPatternTestTrue = function(test) {
|
||||
function ok(pattern, url) {
|
||||
@ -35,6 +35,8 @@ exports.testMatchPatternTestTrue = function(test) {
|
||||
ok(/https:.*zilla.*/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753");
|
||||
ok('*.sample.com', 'http://ex.sample.com/foo.html');
|
||||
ok('*.amp.le.com', 'http://ex.amp.le.com');
|
||||
|
||||
ok('data:*', 'data:text/html;charset=utf-8,');
|
||||
};
|
||||
|
||||
exports.testMatchPatternTestFalse = function(test) {
|
||||
|
35
addon-sdk/source/test/test-object.js
Normal file
35
addon-sdk/source/test/test-object.js
Normal file
@ -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/. */
|
||||
'use strict';
|
||||
|
||||
const { merge, extend, has, each } = require('sdk/util/object');
|
||||
|
||||
let o = {
|
||||
'paper': 0,
|
||||
'rock': 1,
|
||||
'scissors': 2
|
||||
}
|
||||
|
||||
//exports.testMerge = function(test) {}
|
||||
//exports.testExtend = function(test) {}
|
||||
|
||||
exports.testHas = function(test) {
|
||||
test.assertEqual(has(o, 'paper'), true, 'has correctly finds key');
|
||||
test.assertEqual(has(o, 'rock'), true, 'has correctly finds key');
|
||||
test.assertEqual(has(o, 'scissors'), true, 'has correctly finds key');
|
||||
test.assertEqual(has(o, 'nope'), false, 'has correctly does not find key');
|
||||
test.assertEqual(has(o, '__proto__'), false, 'has correctly does not find key');
|
||||
test.assertEqual(has(o, 'isPrototypeOf'), false, 'has correctly does not find key');
|
||||
};
|
||||
|
||||
exports.testEach = function(test) {
|
||||
var count = 0;
|
||||
var keys = new Set();
|
||||
each(o, function (value, key, object) {
|
||||
keys.add(key);
|
||||
test.assertEqual(o[key], value, 'Key and value pairs passed in');
|
||||
test.assertEqual(o, object, 'Object passed in');
|
||||
});
|
||||
test.assertEqual(keys.size, 3, 'All keys have been iterated upon');
|
||||
}
|
@ -143,7 +143,7 @@ exports.testPageModErrorHandling = function(test) {
|
||||
test.assertRaises(function() {
|
||||
new PageMod();
|
||||
},
|
||||
'pattern is undefined',
|
||||
'The `include` option must always contain atleast one rule',
|
||||
"PageMod() throws when 'include' option is not specified.");
|
||||
};
|
||||
|
||||
|
@ -147,7 +147,7 @@ exports.testValidateOptions = function(assert) {
|
||||
|
||||
assert.throws(
|
||||
function () Page({ onMessage: "This is not a function."}),
|
||||
/The event listener must be a function\./,
|
||||
/The option "onMessage" must be one of the following types: function/,
|
||||
"Validation correctly denied a non-function onMessage."
|
||||
);
|
||||
|
||||
@ -302,6 +302,108 @@ exports.testPingPong = function(assert, done) {
|
||||
});
|
||||
};
|
||||
|
||||
exports.testRedirect = function (assert, done) {
|
||||
let page = Page({
|
||||
contentURL: 'data:text/html;charset=utf-8,first-page',
|
||||
contentScript: '(function () {' +
|
||||
'if (/first-page/.test(document.location.href)) ' +
|
||||
' document.location.href = "data:text/html;charset=utf-8,redirect";' +
|
||||
'else ' +
|
||||
' self.port.emit("redirect", document.location.href);' +
|
||||
'})();'
|
||||
});
|
||||
|
||||
page.port.on('redirect', function (url) {
|
||||
assert.equal(url, 'data:text/html;charset=utf-8,redirect', 'Reinjects contentScript on reload');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testRedirectIncludeArrays = function (assert, done) {
|
||||
let firstURL = 'data:text/html;charset=utf-8,first-page';
|
||||
let page = Page({
|
||||
contentURL: firstURL,
|
||||
contentScript: '(function () {' +
|
||||
'self.port.emit("load", document.location.href);' +
|
||||
' self.port.on("redirect", function (url) {' +
|
||||
' document.location.href = url;' +
|
||||
' })' +
|
||||
'})();',
|
||||
include: ['about:blank', 'data:*']
|
||||
});
|
||||
|
||||
page.port.on('load', function (url) {
|
||||
if (url === firstURL) {
|
||||
page.port.emit('redirect', 'about:blank');
|
||||
} else if (url === 'about:blank') {
|
||||
page.port.emit('redirect', 'about:home');
|
||||
assert.ok('`include` property handles arrays');
|
||||
assert.equal(url, 'about:blank', 'Redirects work with accepted domains');
|
||||
done();
|
||||
} else if (url === 'about:home') {
|
||||
assert.fail('Should not redirect to restricted domain');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
exports.testRedirectFromWorker = function (assert, done) {
|
||||
let firstURL = 'data:text/html;charset=utf-8,first-page';
|
||||
let secondURL = 'data:text/html;charset=utf-8,second-page';
|
||||
let thirdURL = 'data:text/html;charset=utf-8,third-page';
|
||||
let page = Page({
|
||||
contentURL: firstURL,
|
||||
contentScript: '(function () {' +
|
||||
'self.port.emit("load", document.location.href);' +
|
||||
' self.port.on("redirect", function (url) {' +
|
||||
' document.location.href = url;' +
|
||||
' })' +
|
||||
'})();',
|
||||
include: 'data:*'
|
||||
});
|
||||
|
||||
page.port.on('load', function (url) {
|
||||
if (url === firstURL) {
|
||||
page.port.emit('redirect', secondURL);
|
||||
} else if (url === secondURL) {
|
||||
page.port.emit('redirect', thirdURL);
|
||||
} else if (url === thirdURL) {
|
||||
page.port.emit('redirect', 'about:home');
|
||||
assert.equal(url, thirdURL, 'Redirects work with accepted domains on include strings');
|
||||
done();
|
||||
} else {
|
||||
assert.fail('Should not redirect to unauthorized domains');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
exports.testRedirectWithContentURL = function (assert, done) {
|
||||
let firstURL = 'data:text/html;charset=utf-8,first-page';
|
||||
let secondURL = 'data:text/html;charset=utf-8,second-page';
|
||||
let thirdURL = 'data:text/html;charset=utf-8,third-page';
|
||||
let page = Page({
|
||||
contentURL: firstURL,
|
||||
contentScript: '(function () {' +
|
||||
'self.port.emit("load", document.location.href);' +
|
||||
'})();',
|
||||
include: 'data:*'
|
||||
});
|
||||
|
||||
page.port.on('load', function (url) {
|
||||
if (url === firstURL) {
|
||||
page.contentURL = secondURL;
|
||||
} else if (url === secondURL) {
|
||||
page.contentURL = thirdURL;
|
||||
} else if (url === thirdURL) {
|
||||
page.contentURL = 'about:home';
|
||||
assert.equal(url, thirdURL, 'Redirects work with accepted domains on include strings');
|
||||
done();
|
||||
} else {
|
||||
assert.fail('Should not redirect to unauthorized domains');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
exports.testMultipleDestroys = function(assert) {
|
||||
let page = Page();
|
||||
page.destroy();
|
||||
|
@ -11,7 +11,7 @@ const self = require('sdk/self');
|
||||
const { open, close, focus } = require('sdk/window/helpers');
|
||||
const { isPrivate } = require('sdk/private-browsing');
|
||||
const { isWindowPBSupported, isGlobalPBSupported } = require('sdk/private-browsing/utils');
|
||||
const { defer } = require('sdk/core/promise');
|
||||
const { defer, all } = require('sdk/core/promise');
|
||||
const { getMostRecentBrowserWindow } = require('sdk/window/utils');
|
||||
const { getWindow } = require('sdk/panel/window');
|
||||
const { pb } = require('./private-browsing/helper');
|
||||
@ -19,6 +19,11 @@ const { URL } = require('sdk/url');
|
||||
|
||||
const SVG_URL = self.data.url('mofo_logo.SVG');
|
||||
|
||||
function ignorePassingDOMNodeWarning(type, message) {
|
||||
if (type !== 'warn' || !message.startsWith('Passing a DOM node'))
|
||||
console[type](message);
|
||||
}
|
||||
|
||||
function makeEmptyPrivateBrowserWindow(options) {
|
||||
options = options || {};
|
||||
return open('chrome://browser/content/browser.xul', {
|
||||
@ -291,7 +296,8 @@ exports["test Several Show Hides"] = function(assert, done) {
|
||||
};
|
||||
|
||||
exports["test Anchor And Arrow"] = function(assert, done) {
|
||||
const { Panel } = require('sdk/panel');
|
||||
let { loader } = LoaderWithHookedConsole(module, ignorePassingDOMNodeWarning);
|
||||
let { Panel } = loader.require('sdk/panel');
|
||||
|
||||
let count = 0;
|
||||
let queue = [];
|
||||
@ -457,6 +463,24 @@ exports["test Panel Text Color"] = function(assert, done) {
|
||||
});
|
||||
};
|
||||
|
||||
// Bug 866333
|
||||
exports["test watch event name"] = function(assert, done) {
|
||||
const { Panel } = require('sdk/panel');
|
||||
|
||||
let html = "<html><head><style>body {color: yellow}</style></head>" +
|
||||
"<body><p>Foo</p></body></html>";
|
||||
|
||||
let panel = Panel({
|
||||
contentURL: "data:text/html;charset=utf-8," + encodeURI(html),
|
||||
contentScript: "self.port.emit('watch', 'test');"
|
||||
});
|
||||
panel.port.on("watch", function (msg) {
|
||||
assert.equal(msg, "test", 'watch event name works');
|
||||
panel.destroy();
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
// Bug 696552: Ensure panel.contentURL modification support
|
||||
exports["test Change Content URL"] = function(assert, done) {
|
||||
const { Panel } = require('sdk/panel');
|
||||
@ -620,7 +644,7 @@ exports["test console.log in Panel"] = function(assert, done) {
|
||||
|
||||
if (isWindowPBSupported) {
|
||||
exports.testPanelDoesNotShowInPrivateWindowNoAnchor = function(assert, done) {
|
||||
let loader = Loader(module);
|
||||
let { loader } = LoaderWithHookedConsole(module, ignorePassingDOMNodeWarning);
|
||||
let { Panel } = loader.require("sdk/panel");
|
||||
let browserWindow = getMostRecentBrowserWindow();
|
||||
|
||||
@ -674,7 +698,7 @@ if (isWindowPBSupported) {
|
||||
}
|
||||
|
||||
exports.testPanelDoesNotShowInPrivateWindowWithAnchor = function(assert, done) {
|
||||
let loader = Loader(module);
|
||||
let { loader } = LoaderWithHookedConsole(module, ignorePassingDOMNodeWarning);
|
||||
let { Panel } = loader.require("sdk/panel");
|
||||
let browserWindow = getMostRecentBrowserWindow();
|
||||
|
||||
@ -810,6 +834,56 @@ exports['test Only One Panel Open Concurrently'] = function (assert, done) {
|
||||
panelB.show();
|
||||
};
|
||||
|
||||
exports['test passing DOM node as first argument'] = function (assert, done) {
|
||||
let warned = defer();
|
||||
let shown = defer();
|
||||
|
||||
function onMessage(type, message) {
|
||||
let warning = 'Passing a DOM node to Panel.show() method is an unsupported ' +
|
||||
'feature that will be soon replaced. ' +
|
||||
'See: https://bugzilla.mozilla.org/show_bug.cgi?id=878877';
|
||||
|
||||
assert.equal(type, 'warn',
|
||||
'the message logged is a warning');
|
||||
|
||||
assert.equal(message, warning,
|
||||
'the warning content is correct');
|
||||
|
||||
warned.resolve();
|
||||
}
|
||||
|
||||
let { loader } = LoaderWithHookedConsole(module, onMessage);
|
||||
let { Panel } = loader.require('sdk/panel');
|
||||
let { Widget } = loader.require('sdk/widget');
|
||||
let { document } = getMostRecentBrowserWindow();
|
||||
let widgetId = 'widget:' + self.id + '-panel-widget';
|
||||
|
||||
let panel = Panel({
|
||||
onShow: function() {
|
||||
let panelNode = document.getElementById('mainPopupSet').lastChild;
|
||||
|
||||
assert.equal(panelNode.anchorNode, widgetNode,
|
||||
'the panel is properly anchored to the widget');
|
||||
|
||||
shown.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
let widget = Widget({
|
||||
id: 'panel-widget',
|
||||
label: 'panel widget',
|
||||
content: '<i></i>',
|
||||
});
|
||||
|
||||
let widgetNode = document.getElementById(widgetId);
|
||||
|
||||
all(warned.promise, shown.promise).
|
||||
then(loader.unload).
|
||||
then(done, assert.fail)
|
||||
|
||||
panel.show(widgetNode);
|
||||
};
|
||||
|
||||
if (isWindowPBSupported) {
|
||||
exports.testGetWindow = function(assert, done) {
|
||||
let activeWindow = getMostRecentBrowserWindow();
|
||||
|
428
addon-sdk/source/test/test-path.js
Normal file
428
addon-sdk/source/test/test-path.js
Normal file
@ -0,0 +1,428 @@
|
||||
// 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.
|
||||
|
||||
|
||||
// Adapted version of:
|
||||
// https://github.com/joyent/node/blob/v0.9.1/test/simple/test-path.js
|
||||
|
||||
exports['test path'] = function(assert) {
|
||||
|
||||
var system = require('sdk/system');
|
||||
var path = require('sdk/fs/path');
|
||||
|
||||
// Shim process global from node.
|
||||
var process = Object.create(require('sdk/system'));
|
||||
process.cwd = process.pathFor.bind(process, 'CurProcD');
|
||||
|
||||
var isWindows = require('sdk/system').platform.indexOf('win') === 0;
|
||||
|
||||
assert.equal(path.basename(''), '');
|
||||
assert.equal(path.basename('/dir/basename.ext'), 'basename.ext');
|
||||
assert.equal(path.basename('/basename.ext'), 'basename.ext');
|
||||
assert.equal(path.basename('basename.ext'), 'basename.ext');
|
||||
assert.equal(path.basename('basename.ext/'), 'basename.ext');
|
||||
assert.equal(path.basename('basename.ext//'), 'basename.ext');
|
||||
|
||||
if (isWindows) {
|
||||
// On Windows a backslash acts as a path separator.
|
||||
assert.equal(path.basename('\\dir\\basename.ext'), 'basename.ext');
|
||||
assert.equal(path.basename('\\basename.ext'), 'basename.ext');
|
||||
assert.equal(path.basename('basename.ext'), 'basename.ext');
|
||||
assert.equal(path.basename('basename.ext\\'), 'basename.ext');
|
||||
assert.equal(path.basename('basename.ext\\\\'), 'basename.ext');
|
||||
|
||||
} else {
|
||||
// On unix a backslash is just treated as any other character.
|
||||
assert.equal(path.basename('\\dir\\basename.ext'), '\\dir\\basename.ext');
|
||||
assert.equal(path.basename('\\basename.ext'), '\\basename.ext');
|
||||
assert.equal(path.basename('basename.ext'), 'basename.ext');
|
||||
assert.equal(path.basename('basename.ext\\'), 'basename.ext\\');
|
||||
assert.equal(path.basename('basename.ext\\\\'), 'basename.ext\\\\');
|
||||
}
|
||||
|
||||
// POSIX filenames may include control characters
|
||||
// c.f. http://www.dwheeler.com/essays/fixing-unix-linux-filenames.html
|
||||
if (!isWindows) {
|
||||
var controlCharFilename = 'Icon' + String.fromCharCode(13);
|
||||
assert.equal(path.basename('/a/b/' + controlCharFilename),
|
||||
controlCharFilename);
|
||||
}
|
||||
|
||||
assert.equal(path.dirname('/a/b/'), '/a');
|
||||
assert.equal(path.dirname('/a/b'), '/a');
|
||||
assert.equal(path.dirname('/a'), '/');
|
||||
assert.equal(path.dirname(''), '.');
|
||||
assert.equal(path.dirname('/'), '/');
|
||||
assert.equal(path.dirname('////'), '/');
|
||||
|
||||
if (isWindows) {
|
||||
assert.equal(path.dirname('c:\\'), 'c:\\');
|
||||
assert.equal(path.dirname('c:\\foo'), 'c:\\');
|
||||
assert.equal(path.dirname('c:\\foo\\'), 'c:\\');
|
||||
assert.equal(path.dirname('c:\\foo\\bar'), 'c:\\foo');
|
||||
assert.equal(path.dirname('c:\\foo\\bar\\'), 'c:\\foo');
|
||||
assert.equal(path.dirname('c:\\foo\\bar\\baz'), 'c:\\foo\\bar');
|
||||
assert.equal(path.dirname('\\'), '\\');
|
||||
assert.equal(path.dirname('\\foo'), '\\');
|
||||
assert.equal(path.dirname('\\foo\\'), '\\');
|
||||
assert.equal(path.dirname('\\foo\\bar'), '\\foo');
|
||||
assert.equal(path.dirname('\\foo\\bar\\'), '\\foo');
|
||||
assert.equal(path.dirname('\\foo\\bar\\baz'), '\\foo\\bar');
|
||||
assert.equal(path.dirname('c:'), 'c:');
|
||||
assert.equal(path.dirname('c:foo'), 'c:');
|
||||
assert.equal(path.dirname('c:foo\\'), 'c:');
|
||||
assert.equal(path.dirname('c:foo\\bar'), 'c:foo');
|
||||
assert.equal(path.dirname('c:foo\\bar\\'), 'c:foo');
|
||||
assert.equal(path.dirname('c:foo\\bar\\baz'), 'c:foo\\bar');
|
||||
assert.equal(path.dirname('\\\\unc\\share'), '\\\\unc\\share');
|
||||
assert.equal(path.dirname('\\\\unc\\share\\foo'), '\\\\unc\\share\\');
|
||||
assert.equal(path.dirname('\\\\unc\\share\\foo\\'), '\\\\unc\\share\\');
|
||||
assert.equal(path.dirname('\\\\unc\\share\\foo\\bar'),
|
||||
'\\\\unc\\share\\foo');
|
||||
assert.equal(path.dirname('\\\\unc\\share\\foo\\bar\\'),
|
||||
'\\\\unc\\share\\foo');
|
||||
assert.equal(path.dirname('\\\\unc\\share\\foo\\bar\\baz'),
|
||||
'\\\\unc\\share\\foo\\bar');
|
||||
}
|
||||
|
||||
|
||||
assert.equal(path.extname(''), '');
|
||||
assert.equal(path.extname('/path/to/file'), '');
|
||||
assert.equal(path.extname('/path/to/file.ext'), '.ext');
|
||||
assert.equal(path.extname('/path.to/file.ext'), '.ext');
|
||||
assert.equal(path.extname('/path.to/file'), '');
|
||||
assert.equal(path.extname('/path.to/.file'), '');
|
||||
assert.equal(path.extname('/path.to/.file.ext'), '.ext');
|
||||
assert.equal(path.extname('/path/to/f.ext'), '.ext');
|
||||
assert.equal(path.extname('/path/to/..ext'), '.ext');
|
||||
assert.equal(path.extname('file'), '');
|
||||
assert.equal(path.extname('file.ext'), '.ext');
|
||||
assert.equal(path.extname('.file'), '');
|
||||
assert.equal(path.extname('.file.ext'), '.ext');
|
||||
assert.equal(path.extname('/file'), '');
|
||||
assert.equal(path.extname('/file.ext'), '.ext');
|
||||
assert.equal(path.extname('/.file'), '');
|
||||
assert.equal(path.extname('/.file.ext'), '.ext');
|
||||
assert.equal(path.extname('.path/file.ext'), '.ext');
|
||||
assert.equal(path.extname('file.ext.ext'), '.ext');
|
||||
assert.equal(path.extname('file.'), '.');
|
||||
assert.equal(path.extname('.'), '');
|
||||
assert.equal(path.extname('./'), '');
|
||||
assert.equal(path.extname('.file.ext'), '.ext');
|
||||
assert.equal(path.extname('.file'), '');
|
||||
assert.equal(path.extname('.file.'), '.');
|
||||
assert.equal(path.extname('.file..'), '.');
|
||||
assert.equal(path.extname('..'), '');
|
||||
assert.equal(path.extname('../'), '');
|
||||
assert.equal(path.extname('..file.ext'), '.ext');
|
||||
assert.equal(path.extname('..file'), '.file');
|
||||
assert.equal(path.extname('..file.'), '.');
|
||||
assert.equal(path.extname('..file..'), '.');
|
||||
assert.equal(path.extname('...'), '.');
|
||||
assert.equal(path.extname('...ext'), '.ext');
|
||||
assert.equal(path.extname('....'), '.');
|
||||
assert.equal(path.extname('file.ext/'), '.ext');
|
||||
assert.equal(path.extname('file.ext//'), '.ext');
|
||||
assert.equal(path.extname('file/'), '');
|
||||
assert.equal(path.extname('file//'), '');
|
||||
assert.equal(path.extname('file./'), '.');
|
||||
assert.equal(path.extname('file.//'), '.');
|
||||
|
||||
if (isWindows) {
|
||||
// On windows, backspace is a path separator.
|
||||
assert.equal(path.extname('.\\'), '');
|
||||
assert.equal(path.extname('..\\'), '');
|
||||
assert.equal(path.extname('file.ext\\'), '.ext');
|
||||
assert.equal(path.extname('file.ext\\\\'), '.ext');
|
||||
assert.equal(path.extname('file\\'), '');
|
||||
assert.equal(path.extname('file\\\\'), '');
|
||||
assert.equal(path.extname('file.\\'), '.');
|
||||
assert.equal(path.extname('file.\\\\'), '.');
|
||||
|
||||
} else {
|
||||
// On unix, backspace is a valid name component like any other character.
|
||||
assert.equal(path.extname('.\\'), '');
|
||||
assert.equal(path.extname('..\\'), '.\\');
|
||||
assert.equal(path.extname('file.ext\\'), '.ext\\');
|
||||
assert.equal(path.extname('file.ext\\\\'), '.ext\\\\');
|
||||
assert.equal(path.extname('file\\'), '');
|
||||
assert.equal(path.extname('file\\\\'), '');
|
||||
assert.equal(path.extname('file.\\'), '.\\');
|
||||
assert.equal(path.extname('file.\\\\'), '.\\\\');
|
||||
}
|
||||
|
||||
// path.join tests
|
||||
var failures = [];
|
||||
var joinTests =
|
||||
// arguments result
|
||||
[[['.', 'x/b', '..', '/b/c.js'], 'x/b/c.js'],
|
||||
[['/.', 'x/b', '..', '/b/c.js'], '/x/b/c.js'],
|
||||
[['/foo', '../../../bar'], '/bar'],
|
||||
[['foo', '../../../bar'], '../../bar'],
|
||||
[['foo/', '../../../bar'], '../../bar'],
|
||||
[['foo/x', '../../../bar'], '../bar'],
|
||||
[['foo/x', './bar'], 'foo/x/bar'],
|
||||
[['foo/x/', './bar'], 'foo/x/bar'],
|
||||
[['foo/x/', '.', 'bar'], 'foo/x/bar'],
|
||||
[['./'], './'],
|
||||
[['.', './'], './'],
|
||||
[['.', '.', '.'], '.'],
|
||||
[['.', './', '.'], '.'],
|
||||
[['.', '/./', '.'], '.'],
|
||||
[['.', '/////./', '.'], '.'],
|
||||
[['.'], '.'],
|
||||
[['', '.'], '.'],
|
||||
[['', 'foo'], 'foo'],
|
||||
[['foo', '/bar'], 'foo/bar'],
|
||||
[['', '/foo'], '/foo'],
|
||||
[['', '', '/foo'], '/foo'],
|
||||
[['', '', 'foo'], 'foo'],
|
||||
[['foo', ''], 'foo'],
|
||||
[['foo/', ''], 'foo/'],
|
||||
[['foo', '', '/bar'], 'foo/bar'],
|
||||
[['./', '..', '/foo'], '../foo'],
|
||||
[['./', '..', '..', '/foo'], '../../foo'],
|
||||
[['.', '..', '..', '/foo'], '../../foo'],
|
||||
[['', '..', '..', '/foo'], '../../foo'],
|
||||
[['/'], '/'],
|
||||
[['/', '.'], '/'],
|
||||
[['/', '..'], '/'],
|
||||
[['/', '..', '..'], '/'],
|
||||
[[''], '.'],
|
||||
[['', ''], '.'],
|
||||
[[' /foo'], ' /foo'],
|
||||
[[' ', 'foo'], ' /foo'],
|
||||
[[' ', '.'], ' '],
|
||||
[[' ', '/'], ' /'],
|
||||
[[' ', ''], ' '],
|
||||
[['/', 'foo'], '/foo'],
|
||||
[['/', '/foo'], '/foo'],
|
||||
[['/', '//foo'], '/foo'],
|
||||
[['/', '', '/foo'], '/foo'],
|
||||
[['', '/', 'foo'], '/foo'],
|
||||
[['', '/', '/foo'], '/foo']
|
||||
];
|
||||
|
||||
// Windows-specific join tests
|
||||
if (isWindows) {
|
||||
joinTests = joinTests.concat(
|
||||
[// UNC path expected
|
||||
[['//foo/bar'], '//foo/bar/'],
|
||||
[['\\/foo/bar'], '//foo/bar/'],
|
||||
[['\\\\foo/bar'], '//foo/bar/'],
|
||||
// UNC path expected - server and share separate
|
||||
[['//foo', 'bar'], '//foo/bar/'],
|
||||
[['//foo/', 'bar'], '//foo/bar/'],
|
||||
[['//foo', '/bar'], '//foo/bar/'],
|
||||
// UNC path expected - questionable
|
||||
[['//foo', '', 'bar'], '//foo/bar/'],
|
||||
[['//foo/', '', 'bar'], '//foo/bar/'],
|
||||
[['//foo/', '', '/bar'], '//foo/bar/'],
|
||||
// UNC path expected - even more questionable
|
||||
[['', '//foo', 'bar'], '//foo/bar/'],
|
||||
[['', '//foo/', 'bar'], '//foo/bar/'],
|
||||
[['', '//foo/', '/bar'], '//foo/bar/'],
|
||||
// No UNC path expected (no double slash in first component)
|
||||
[['\\', 'foo/bar'], '/foo/bar'],
|
||||
[['\\', '/foo/bar'], '/foo/bar'],
|
||||
[['', '/', '/foo/bar'], '/foo/bar'],
|
||||
// No UNC path expected (no non-slashes in first component - questionable)
|
||||
[['//', 'foo/bar'], '/foo/bar'],
|
||||
[['//', '/foo/bar'], '/foo/bar'],
|
||||
[['\\\\', '/', '/foo/bar'], '/foo/bar'],
|
||||
[['//'], '/'],
|
||||
// No UNC path expected (share name missing - questionable).
|
||||
[['//foo'], '/foo'],
|
||||
[['//foo/'], '/foo/'],
|
||||
[['//foo', '/'], '/foo/'],
|
||||
[['//foo', '', '/'], '/foo/'],
|
||||
// No UNC path expected (too many leading slashes - questionable)
|
||||
[['///foo/bar'], '/foo/bar'],
|
||||
[['////foo', 'bar'], '/foo/bar'],
|
||||
[['\\\\\\/foo/bar'], '/foo/bar'],
|
||||
// Drive-relative vs drive-absolute paths. This merely describes the
|
||||
// status quo, rather than being obviously right
|
||||
[['c:'], 'c:.'],
|
||||
[['c:.'], 'c:.'],
|
||||
[['c:', ''], 'c:.'],
|
||||
[['', 'c:'], 'c:.'],
|
||||
[['c:.', '/'], 'c:./'],
|
||||
[['c:.', 'file'], 'c:file'],
|
||||
[['c:', '/'], 'c:/'],
|
||||
[['c:', 'file'], 'c:/file']
|
||||
]);
|
||||
}
|
||||
|
||||
// Run the join tests.
|
||||
joinTests.forEach(function(test) {
|
||||
var actual = path.join.apply(path, test[0]);
|
||||
var expected = isWindows ? test[1].replace(/\//g, '\\') : test[1];
|
||||
var message = 'path.join(' + test[0].map(JSON.stringify).join(',') + ')' +
|
||||
'\n expect=' + JSON.stringify(expected) +
|
||||
'\n actual=' + JSON.stringify(actual);
|
||||
if (actual !== expected) failures.push('\n' + message);
|
||||
// assert.equal(actual, expected, message);
|
||||
});
|
||||
assert.equal(failures.length, 0, failures.join(''));
|
||||
var joinThrowTests = [true, false, 7, null, {}, undefined, [], NaN];
|
||||
joinThrowTests.forEach(function(test) {
|
||||
assert.throws(function() {
|
||||
path.join(test);
|
||||
}, TypeError);
|
||||
assert.throws(function() {
|
||||
path.resolve(test);
|
||||
}, TypeError);
|
||||
});
|
||||
|
||||
|
||||
// path normalize tests
|
||||
if (isWindows) {
|
||||
assert.equal(path.normalize('./fixtures///b/../b/c.js'),
|
||||
'fixtures\\b\\c.js');
|
||||
assert.equal(path.normalize('/foo/../../../bar'), '\\bar');
|
||||
assert.equal(path.normalize('a//b//../b'), 'a\\b');
|
||||
assert.equal(path.normalize('a//b//./c'), 'a\\b\\c');
|
||||
assert.equal(path.normalize('a//b//.'), 'a\\b');
|
||||
assert.equal(path.normalize('//server/share/dir/file.ext'),
|
||||
'\\\\server\\share\\dir\\file.ext');
|
||||
} else {
|
||||
assert.equal(path.normalize('./fixtures///b/../b/c.js'),
|
||||
'fixtures/b/c.js');
|
||||
assert.equal(path.normalize('/foo/../../../bar'), '/bar');
|
||||
assert.equal(path.normalize('a//b//../b'), 'a/b');
|
||||
assert.equal(path.normalize('a//b//./c'), 'a/b/c');
|
||||
assert.equal(path.normalize('a//b//.'), 'a/b');
|
||||
}
|
||||
|
||||
// path.resolve tests
|
||||
if (isWindows) {
|
||||
// windows
|
||||
var resolveTests =
|
||||
// arguments result
|
||||
[[['c:/blah\\blah', 'd:/games', 'c:../a'], 'c:\\blah\\a'],
|
||||
[['c:/ignore', 'd:\\a/b\\c/d', '\\e.exe'], 'd:\\e.exe'],
|
||||
[['c:/ignore', 'c:/some/file'], 'c:\\some\\file'],
|
||||
[['d:/ignore', 'd:some/dir//'], 'd:\\ignore\\some\\dir'],
|
||||
[['.'], process.cwd()],
|
||||
[['//server/share', '..', 'relative\\'], '\\\\server\\share\\relative'],
|
||||
[['c:/', '//'], 'c:\\'],
|
||||
[['c:/', '//dir'], 'c:\\dir'],
|
||||
[['c:/', '//server/share'], '\\\\server\\share\\'],
|
||||
[['c:/', '//server//share'], '\\\\server\\share\\'],
|
||||
[['c:/', '///some//dir'], 'c:\\some\\dir']
|
||||
];
|
||||
} else {
|
||||
// Posix
|
||||
var resolveTests =
|
||||
// arguments result
|
||||
[[['/var/lib', '../', 'file/'], '/var/file'],
|
||||
[['/var/lib', '/../', 'file/'], '/file'],
|
||||
[['a/b/c/', '../../..'], process.cwd()],
|
||||
[['.'], process.cwd()],
|
||||
[['/some/dir', '.', '/absolute/'], '/absolute']];
|
||||
}
|
||||
var failures = [];
|
||||
resolveTests.forEach(function(test) {
|
||||
var actual = path.resolve.apply(path, test[0]);
|
||||
var expected = test[1];
|
||||
var message = 'path.resolve(' + test[0].map(JSON.stringify).join(',') + ')' +
|
||||
'\n expect=' + JSON.stringify(expected) +
|
||||
'\n actual=' + JSON.stringify(actual);
|
||||
if (actual !== expected) failures.push('\n' + message);
|
||||
// assert.equal(actual, expected, message);
|
||||
});
|
||||
assert.equal(failures.length, 0, failures.join(''));
|
||||
|
||||
// path.isAbsolute tests
|
||||
if (isWindows) {
|
||||
assert.equal(path.isAbsolute('//server/file'), true);
|
||||
assert.equal(path.isAbsolute('\\\\server\\file'), true);
|
||||
assert.equal(path.isAbsolute('C:/Users/'), true);
|
||||
assert.equal(path.isAbsolute('C:\\Users\\'), true);
|
||||
assert.equal(path.isAbsolute('C:cwd/another'), false);
|
||||
assert.equal(path.isAbsolute('C:cwd\\another'), false);
|
||||
assert.equal(path.isAbsolute('directory/directory'), false);
|
||||
assert.equal(path.isAbsolute('directory\\directory'), false);
|
||||
} else {
|
||||
assert.equal(path.isAbsolute('/home/foo'), true);
|
||||
assert.equal(path.isAbsolute('/home/foo/..'), true);
|
||||
assert.equal(path.isAbsolute('bar/'), false);
|
||||
assert.equal(path.isAbsolute('./baz'), false);
|
||||
}
|
||||
|
||||
// path.relative tests
|
||||
if (isWindows) {
|
||||
// windows
|
||||
var relativeTests =
|
||||
// arguments result
|
||||
[['c:/blah\\blah', 'd:/games', 'd:\\games'],
|
||||
['c:/aaaa/bbbb', 'c:/aaaa', '..'],
|
||||
['c:/aaaa/bbbb', 'c:/cccc', '..\\..\\cccc'],
|
||||
['c:/aaaa/bbbb', 'c:/aaaa/bbbb', ''],
|
||||
['c:/aaaa/bbbb', 'c:/aaaa/cccc', '..\\cccc'],
|
||||
['c:/aaaa/', 'c:/aaaa/cccc', 'cccc'],
|
||||
['c:/', 'c:\\aaaa\\bbbb', 'aaaa\\bbbb'],
|
||||
['c:/aaaa/bbbb', 'd:\\', 'd:\\']];
|
||||
} else {
|
||||
// posix
|
||||
var relativeTests =
|
||||
// arguments result
|
||||
[['/var/lib', '/var', '..'],
|
||||
['/var/lib', '/bin', '../../bin'],
|
||||
['/var/lib', '/var/lib', ''],
|
||||
['/var/lib', '/var/apache', '../apache'],
|
||||
['/var/', '/var/lib', 'lib'],
|
||||
['/', '/var/lib', 'var/lib']];
|
||||
}
|
||||
var failures = [];
|
||||
relativeTests.forEach(function(test) {
|
||||
var actual = path.relative(test[0], test[1]);
|
||||
var expected = test[2];
|
||||
var message = 'path.relative(' +
|
||||
test.slice(0, 2).map(JSON.stringify).join(',') +
|
||||
')' +
|
||||
'\n expect=' + JSON.stringify(expected) +
|
||||
'\n actual=' + JSON.stringify(actual);
|
||||
if (actual !== expected) failures.push('\n' + message);
|
||||
});
|
||||
assert.equal(failures.length, 0, failures.join(''));
|
||||
|
||||
// path.sep tests
|
||||
if (isWindows) {
|
||||
// windows
|
||||
assert.equal(path.sep, '\\');
|
||||
} else {
|
||||
// posix
|
||||
assert.equal(path.sep, '/');
|
||||
}
|
||||
|
||||
// path.delimiter tests
|
||||
if (isWindows) {
|
||||
// windows
|
||||
assert.equal(path.delimiter, ';');
|
||||
} else {
|
||||
// posix
|
||||
assert.equal(path.delimiter, ':');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
require('test').run(exports);
|
970
addon-sdk/source/test/test-places-bookmarks.js
Normal file
970
addon-sdk/source/test/test-places-bookmarks.js
Normal file
@ -0,0 +1,970 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.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 { request } = require('sdk/addon/host');
|
||||
const { filter } = require('sdk/event/utils');
|
||||
const { on, off } = require('sdk/event/core');
|
||||
const { setTimeout } = require('sdk/timers');
|
||||
const { newURI } = require('sdk/url/utils');
|
||||
const { defer, all } = require('sdk/core/promise');
|
||||
const { defer: async } = require('sdk/lang/functional');
|
||||
const { before, after } = require('sdk/test/utils');
|
||||
|
||||
// Test for unsupported platforms
|
||||
try {
|
||||
const {
|
||||
Bookmark, Group, Separator,
|
||||
save, search, remove,
|
||||
MENU, TOOLBAR, UNSORTED
|
||||
} = require('sdk/places/bookmarks');
|
||||
const {
|
||||
invalidResolve, invalidReject, clearBookmarks, createTree,
|
||||
compareWithHost, clearAllBookmarks, createBookmark, createBookmarkItem,
|
||||
createBookmarkTree, addVisits
|
||||
} = require('./places-helper');
|
||||
const { promisedEmitter } = require('sdk/places/utils');
|
||||
const bmsrv = Cc['@mozilla.org/browser/nav-bookmarks-service;1'].
|
||||
getService(Ci.nsINavBookmarksService);
|
||||
const tagsrv = Cc['@mozilla.org/browser/tagging-service;1'].
|
||||
getService(Ci.nsITaggingService);
|
||||
} catch (e) { unsupported(e); }
|
||||
|
||||
exports.testDefaultFolders = function (assert) {
|
||||
var ids = [
|
||||
bmsrv.bookmarksMenuFolder,
|
||||
bmsrv.toolbarFolder,
|
||||
bmsrv.unfiledBookmarksFolder
|
||||
];
|
||||
[MENU, TOOLBAR, UNSORTED].forEach(function (g, i) {
|
||||
assert.ok(g.id === ids[i], ' default group matches id');
|
||||
});
|
||||
};
|
||||
|
||||
exports.testValidation = function (assert) {
|
||||
assert.throws(() => {
|
||||
Bookmark({ title: 'a title' });
|
||||
}, /The `url` property must be a valid URL/, 'throws empty URL error');
|
||||
|
||||
assert.throws(() => {
|
||||
Bookmark({ title: 'a title', url: 'not.a.url' });
|
||||
}, /The `url` property must be a valid URL/, 'throws invalid URL error');
|
||||
|
||||
assert.throws(() => {
|
||||
Bookmark({ url: 'http://foo.com' });
|
||||
}, /The `title` property must be defined/, 'throws title error');
|
||||
|
||||
assert.throws(() => {
|
||||
Bookmark();
|
||||
}, /./, 'throws any error');
|
||||
|
||||
assert.throws(() => {
|
||||
Group();
|
||||
}, /The `title` property must be defined/, 'throws title error for group');
|
||||
|
||||
assert.throws(() => {
|
||||
Bookmark({ url: 'http://foo.com', title: 'my title', tags: 'a tag' });
|
||||
}, /The `tags` property must be a Set, or an array/, 'throws error for non set/array tag');
|
||||
};
|
||||
|
||||
exports.testCreateBookmarks = function (assert, done) {
|
||||
var bm = Bookmark({
|
||||
title: 'moz',
|
||||
url: 'http://mozilla.org',
|
||||
tags: ['moz1', 'moz2', 'moz3']
|
||||
});
|
||||
|
||||
save(bm).on('data', (bookmark, input) => {
|
||||
assert.equal(input, bm, 'input is original input item');
|
||||
assert.ok(bookmark.id, 'Bookmark has ID');
|
||||
assert.equal(bookmark.title, 'moz');
|
||||
assert.equal(bookmark.url, 'http://mozilla.org');
|
||||
assert.equal(bookmark.group, UNSORTED, 'Unsorted folder is default parent');
|
||||
assert.ok(bookmark !== bm, 'bookmark should be a new instance');
|
||||
compareWithHost(assert, bookmark);
|
||||
}).on('end', bookmarks => {
|
||||
assert.equal(bookmarks.length, 1, 'returned bookmarks in end');
|
||||
assert.equal(bookmarks[0].url, 'http://mozilla.org');
|
||||
assert.equal(bookmarks[0].tags.has('moz1'), true, 'has first tag');
|
||||
assert.equal(bookmarks[0].tags.has('moz2'), true, 'has second tag');
|
||||
assert.equal(bookmarks[0].tags.has('moz3'), true, 'has third tag');
|
||||
assert.pass('end event is called');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testCreateGroup = function (assert, done) {
|
||||
save(Group({ title: 'mygroup', group: MENU })).on('data', g => {
|
||||
assert.ok(g.id, 'Bookmark has ID');
|
||||
assert.equal(g.title, 'mygroup', 'matches title');
|
||||
assert.equal(g.group, MENU, 'Menu folder matches');
|
||||
compareWithHost(assert, g);
|
||||
}).on('end', results => {
|
||||
assert.equal(results.length, 1);
|
||||
assert.pass('end event is called');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testCreateSeparator = function (assert, done) {
|
||||
save(Separator({ group: MENU })).on('data', function (s) {
|
||||
assert.ok(s.id, 'Separator has id');
|
||||
assert.equal(s.group, MENU, 'Parent group matches');
|
||||
compareWithHost(assert, s);
|
||||
}).on('end', function (results) {
|
||||
assert.equal(results.length, 1);
|
||||
assert.pass('end event is called');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testCreateError = function (assert, done) {
|
||||
let bookmarks = [
|
||||
{ title: 'moz1', url: 'http://moz1.com', type: 'bookmark'},
|
||||
{ title: 'moz2', url: 'invalidurl', type: 'bookmark'},
|
||||
{ title: 'moz3', url: 'http://moz3.com', type: 'bookmark'}
|
||||
];
|
||||
|
||||
let dataCount = 0, errorCount = 0;
|
||||
save(bookmarks).on('data', bookmark => {
|
||||
assert.ok(/moz[1|3]/.test(bookmark.title), 'valid bookmarks complete');
|
||||
dataCount++;
|
||||
}).on('error', (reason, item) => {
|
||||
assert.ok(
|
||||
/The `url` property must be a valid URL/.test(reason),
|
||||
'Error event called with correct reason');
|
||||
assert.equal(item, bookmarks[1], 'returns input that failed in event');
|
||||
errorCount++;
|
||||
}).on('end', items => {
|
||||
assert.equal(dataCount, 2, 'data event called twice');
|
||||
assert.equal(errorCount, 1, 'error event called once');
|
||||
assert.equal(items.length, bookmarks.length, 'all items should be in result');
|
||||
assert.equal(items[0].toString(), '[object Bookmark]',
|
||||
'should be a saved instance');
|
||||
assert.equal(items[2].toString(), '[object Bookmark]',
|
||||
'should be a saved instance');
|
||||
assert.equal(items[1], bookmarks[1], 'should be original, unsaved object');
|
||||
|
||||
search({ query: 'moz' }).on('end', items => {
|
||||
assert.equal(items.length, 2, 'only two items were successfully saved');
|
||||
bookmarks[1].url = 'http://moz2.com/';
|
||||
dataCount = errorCount = 0;
|
||||
save(bookmarks).on('data', bookmark => {
|
||||
dataCount++;
|
||||
}).on('error', reason => errorCount++)
|
||||
.on('end', items => {
|
||||
assert.equal(items.length, 3, 'all 3 items saved');
|
||||
assert.equal(dataCount, 3, '3 data events called');
|
||||
assert.equal(errorCount, 0, 'no error events called');
|
||||
search({ query: 'moz' }).on('end', items => {
|
||||
assert.equal(items.length, 3, 'only 3 items saved');
|
||||
items.map(item =>
|
||||
assert.ok(/moz\d\.com/.test(item.url), 'correct item'))
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.testSaveDucktypes = function (assert, done) {
|
||||
save({
|
||||
title: 'moz',
|
||||
url: 'http://mozilla.org',
|
||||
type: 'bookmark'
|
||||
}).on('data', (bookmark) => {
|
||||
compareWithHost(assert, bookmark);
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testSaveDucktypesParent = function (assert, done) {
|
||||
let folder = { title: 'myfolder', type: 'group' };
|
||||
let bookmark = { title: 'mozzie', url: 'http://moz.com', group: folder, type: 'bookmark' };
|
||||
let sep = { type: 'separator', group: folder };
|
||||
save([sep, bookmark]).on('end', (res) => {
|
||||
compareWithHost(assert, res[0]);
|
||||
compareWithHost(assert, res[1]);
|
||||
assert.equal(res[0].group.title, 'myfolder', 'parent is ducktyped group');
|
||||
assert.equal(res[1].group.title, 'myfolder', 'parent is ducktyped group');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Tests the scenario where the original bookmark item is resaved
|
||||
* and does not have an ID or an updated date, but should still be
|
||||
* mapped to the item it created previously
|
||||
*/
|
||||
exports.testResaveOriginalItemMapping = function (assert, done) {
|
||||
let bookmark = Bookmark({ title: 'moz', url: 'http://moz.org' });
|
||||
save(bookmark).on('data', newBookmark => {
|
||||
bookmark.title = 'new moz';
|
||||
save(bookmark).on('data', newNewBookmark => {
|
||||
assert.equal(newBookmark.id, newNewBookmark.id, 'should be the same bookmark item');
|
||||
assert.equal(bmsrv.getItemTitle(newBookmark.id), 'new moz', 'should have updated title');
|
||||
done();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.testCreateMultipleBookmarks = function (assert, done) {
|
||||
let data = [
|
||||
Bookmark({title: 'bm1', url: 'http://bm1.com'}),
|
||||
Bookmark({title: 'bm2', url: 'http://bm2.com'}),
|
||||
Bookmark({title: 'bm3', url: 'http://bm3.com'}),
|
||||
];
|
||||
save(data).on('data', function (bookmark, input) {
|
||||
let stored = data.filter(({title}) => title === bookmark.title)[0];
|
||||
assert.equal(input, stored, 'input is original input item');
|
||||
assert.equal(bookmark.title, stored.title, 'titles match');
|
||||
assert.equal(bookmark.url, stored.url, 'urls match');
|
||||
compareWithHost(assert, bookmark);
|
||||
}).on('end', function (bookmarks) {
|
||||
assert.equal(bookmarks.length, 3, 'all bookmarks returned');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testCreateImplicitParent = function (assert, done) {
|
||||
let folder = Group({ title: 'my parent' });
|
||||
let bookmarks = [
|
||||
Bookmark({ title: 'moz1', url: 'http://moz1.com', group: folder }),
|
||||
Bookmark({ title: 'moz2', url: 'http://moz2.com', group: folder }),
|
||||
Bookmark({ title: 'moz3', url: 'http://moz3.com', group: folder })
|
||||
];
|
||||
save(bookmarks).on('data', function (bookmark) {
|
||||
if (bookmark.type === 'bookmark') {
|
||||
assert.equal(bookmark.group.title, folder.title, 'parent is linked');
|
||||
compareWithHost(assert, bookmark);
|
||||
} else if (bookmark.type === 'group') {
|
||||
assert.equal(bookmark.group.id, UNSORTED.id, 'parent ID of group is correct');
|
||||
compareWithHost(assert, bookmark);
|
||||
}
|
||||
}).on('end', function (results) {
|
||||
assert.equal(results.length, 3, 'results should only hold explicit saves');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testCreateExplicitParent = function (assert, done) {
|
||||
let folder = Group({ title: 'my parent' });
|
||||
let bookmarks = [
|
||||
Bookmark({ title: 'moz1', url: 'http://moz1.com', group: folder }),
|
||||
Bookmark({ title: 'moz2', url: 'http://moz2.com', group: folder }),
|
||||
Bookmark({ title: 'moz3', url: 'http://moz3.com', group: folder })
|
||||
];
|
||||
save(bookmarks.concat(folder)).on('data', function (bookmark) {
|
||||
if (bookmark.type === 'bookmark') {
|
||||
assert.equal(bookmark.group.title, folder.title, 'parent is linked');
|
||||
compareWithHost(assert, bookmark);
|
||||
} else if (bookmark.type === 'group') {
|
||||
assert.equal(bookmark.group.id, UNSORTED.id, 'parent ID of group is correct');
|
||||
compareWithHost(assert, bookmark);
|
||||
}
|
||||
}).on('end', function () {
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testCreateNested = function (assert, done) {
|
||||
let topFolder = Group({ title: 'top', group: MENU });
|
||||
let midFolder = Group({ title: 'middle', group: topFolder });
|
||||
let bookmarks = [
|
||||
Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder }),
|
||||
Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder }),
|
||||
Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder })
|
||||
];
|
||||
let dataEventCount = 0;
|
||||
save(bookmarks).on('data', function (bookmark) {
|
||||
if (bookmark.type === 'bookmark') {
|
||||
assert.equal(bookmark.group.title, midFolder.title, 'parent is linked');
|
||||
} else if (bookmark.title === 'top') {
|
||||
assert.equal(bookmark.group.id, MENU.id, 'parent ID of top group is correct');
|
||||
} else {
|
||||
assert.equal(bookmark.group.title, topFolder.title, 'parent title of middle group is correct');
|
||||
}
|
||||
dataEventCount++;
|
||||
compareWithHost(assert, bookmark);
|
||||
}).on('end', () => {
|
||||
assert.equal(dataEventCount, 5, 'data events for all saves have occurred');
|
||||
assert.ok('end event called');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Was a scenario when implicitly saving a bookmark that was already created,
|
||||
* it was not being properly fetched and attempted to recreate
|
||||
*/
|
||||
exports.testAddingToExistingParent = function (assert, done) {
|
||||
let group = { type: 'group', title: 'mozgroup' };
|
||||
let bookmarks = [
|
||||
{ title: 'moz1', url: 'http://moz1.com', type: 'bookmark', group: group },
|
||||
{ title: 'moz2', url: 'http://moz2.com', type: 'bookmark', group: group },
|
||||
{ title: 'moz3', url: 'http://moz3.com', type: 'bookmark', group: group }
|
||||
],
|
||||
firstBatch, secondBatch;
|
||||
|
||||
saveP(bookmarks).then(data => {
|
||||
firstBatch = data;
|
||||
return saveP([
|
||||
{ title: 'moz4', url: 'http://moz4.com', type: 'bookmark', group: group },
|
||||
{ title: 'moz5', url: 'http://moz5.com', type: 'bookmark', group: group }
|
||||
]);
|
||||
}, console.error).then(data => {
|
||||
secondBatch = data;
|
||||
assert.equal(firstBatch[0].group.id, secondBatch[0].group.id,
|
||||
'successfully saved to the same parent');
|
||||
done();
|
||||
}, console.error);
|
||||
};
|
||||
|
||||
exports.testUpdateParent = function (assert, done) {
|
||||
let group = { type: 'group', title: 'mozgroup' };
|
||||
saveP(group).then(item => {
|
||||
item[0].title = 'mozgroup-resave';
|
||||
return saveP(item[0]);
|
||||
}).then(item => {
|
||||
assert.equal(item[0].title, 'mozgroup-resave', 'group saved successfully');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testUpdateSeparator = function (assert, done) {
|
||||
let sep = [Separator(), Separator(), Separator()];
|
||||
saveP(sep).then(item => {
|
||||
item[0].index = 2;
|
||||
return saveP(item[0]);
|
||||
}).then(item => {
|
||||
assert.equal(item[0].index, 2, 'updated index of separator');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testPromisedSave = function (assert, done) {
|
||||
let topFolder = Group({ title: 'top', group: MENU });
|
||||
let midFolder = Group({ title: 'middle', group: topFolder });
|
||||
let bookmarks = [
|
||||
Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}),
|
||||
Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}),
|
||||
Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder})
|
||||
];
|
||||
let first, second, third;
|
||||
saveP(bookmarks).then(bms => {
|
||||
first = bms.filter(b => b.title === 'moz1')[0];
|
||||
second = bms.filter(b => b.title === 'moz2')[0];
|
||||
third = bms.filter(b => b.title === 'moz3')[0];
|
||||
assert.equal(first.index, 0);
|
||||
assert.equal(second.index, 1);
|
||||
assert.equal(third.index, 2);
|
||||
first.index = 3;
|
||||
return saveP(first);
|
||||
}).then(() => {
|
||||
assert.equal(bmsrv.getItemIndex(first.id), 2, 'properly moved bookmark');
|
||||
assert.equal(bmsrv.getItemIndex(second.id), 0, 'other bookmarks adjusted');
|
||||
assert.equal(bmsrv.getItemIndex(third.id), 1, 'other bookmarks adjusted');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testPromisedErrorSave = function (assert, done) {
|
||||
let bookmarks = [
|
||||
{ title: 'moz1', url: 'http://moz1.com', type: 'bookmark'},
|
||||
{ title: 'moz2', url: 'invalidurl', type: 'bookmark'},
|
||||
{ title: 'moz3', url: 'http://moz3.com', type: 'bookmark'}
|
||||
];
|
||||
saveP(bookmarks).then(invalidResolve, reason => {
|
||||
assert.ok(
|
||||
/The `url` property must be a valid URL/.test(reason),
|
||||
'Error event called with correct reason');
|
||||
|
||||
bookmarks[1].url = 'http://moz2.com';
|
||||
return saveP(bookmarks);
|
||||
}).then(res => {
|
||||
return searchP({ query: 'moz' });
|
||||
}).then(res => {
|
||||
assert.equal(res.length, 3, 'all 3 should be saved upon retry');
|
||||
res.map(item => assert.ok(/moz\d\.com/.test(item.url), 'correct item'));
|
||||
done();
|
||||
}, invalidReject);
|
||||
};
|
||||
|
||||
exports.testMovingChildren = function (assert, done) {
|
||||
let topFolder = Group({ title: 'top', group: MENU });
|
||||
let midFolder = Group({ title: 'middle', group: topFolder });
|
||||
let bookmarks = [
|
||||
Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}),
|
||||
Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}),
|
||||
Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder})
|
||||
];
|
||||
save(bookmarks).on('end', bms => {
|
||||
let first = bms.filter(b => b.title === 'moz1')[0];
|
||||
let second = bms.filter(b => b.title === 'moz2')[0];
|
||||
let third = bms.filter(b => b.title === 'moz3')[0];
|
||||
assert.equal(first.index, 0);
|
||||
assert.equal(second.index, 1);
|
||||
assert.equal(third.index, 2);
|
||||
/* When moving down in the same container we take
|
||||
* into account the removal of the original item. If you want
|
||||
* to move from index X to index Y > X you must use
|
||||
* moveItem(id, folder, Y + 1)
|
||||
*/
|
||||
first.index = 3;
|
||||
save(first).on('end', () => {
|
||||
assert.equal(bmsrv.getItemIndex(first.id), 2, 'properly moved bookmark');
|
||||
assert.equal(bmsrv.getItemIndex(second.id), 0, 'other bookmarks adjusted');
|
||||
assert.equal(bmsrv.getItemIndex(third.id), 1, 'other bookmarks adjusted');
|
||||
done();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.testMovingChildrenNewFolder = function (assert, done) {
|
||||
let topFolder = Group({ title: 'top', group: MENU });
|
||||
let midFolder = Group({ title: 'middle', group: topFolder });
|
||||
let newFolder = Group({ title: 'new', group: MENU });
|
||||
let bookmarks = [
|
||||
Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}),
|
||||
Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}),
|
||||
Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder})
|
||||
];
|
||||
save(bookmarks).on('end', bms => {
|
||||
let first = bms.filter(b => b.title === 'moz1')[0];
|
||||
let second = bms.filter(b => b.title === 'moz2')[0];
|
||||
let third = bms.filter(b => b.title === 'moz3')[0];
|
||||
let definedMidFolder = first.group;
|
||||
let definedNewFolder;
|
||||
first.group = newFolder;
|
||||
assert.equal(first.index, 0);
|
||||
assert.equal(second.index, 1);
|
||||
assert.equal(third.index, 2);
|
||||
save(first).on('data', (data) => {
|
||||
if (data.type === 'group') definedNewFolder = data;
|
||||
}).on('end', (moved) => {
|
||||
assert.equal(bmsrv.getItemIndex(second.id), 0, 'other bookmarks adjusted');
|
||||
assert.equal(bmsrv.getItemIndex(third.id), 1, 'other bookmarks adjusted');
|
||||
assert.equal(bmsrv.getItemIndex(first.id), 0, 'properly moved bookmark');
|
||||
assert.equal(bmsrv.getFolderIdForItem(first.id), definedNewFolder.id,
|
||||
'bookmark has new parent');
|
||||
assert.equal(bmsrv.getFolderIdForItem(second.id), definedMidFolder.id,
|
||||
'sibling bookmarks did not move');
|
||||
assert.equal(bmsrv.getFolderIdForItem(third.id), definedMidFolder.id,
|
||||
'sibling bookmarks did not move');
|
||||
done();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.testRemoveFunction = function (assert) {
|
||||
let topFolder = Group({ title: 'new', group: MENU });
|
||||
let midFolder = Group({ title: 'middle', group: topFolder });
|
||||
let bookmarks = [
|
||||
Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}),
|
||||
Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}),
|
||||
Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder})
|
||||
];
|
||||
remove([midFolder, topFolder].concat(bookmarks)).map(item => {
|
||||
assert.equal(item.remove, true, 'remove toggled `remove` property to true');
|
||||
});
|
||||
};
|
||||
|
||||
exports.testRemove = function (assert, done) {
|
||||
let id;
|
||||
createBookmarkItem().then(data => {
|
||||
id = data.id;
|
||||
compareWithHost(assert, data); // ensure bookmark exists
|
||||
save(remove(data)).on('data', (res) => {
|
||||
assert.pass('data event should be called');
|
||||
assert.ok(!res, 'response should be empty');
|
||||
}).on('end', () => {
|
||||
assert.throws(function () {
|
||||
bmsrv.getItemTitle(id);
|
||||
}, 'item should no longer exist');
|
||||
done();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Tests recursively removing children when removing a group
|
||||
*/
|
||||
exports.testRemoveAllChildren = function (assert, done) {
|
||||
let topFolder = Group({ title: 'new', group: MENU });
|
||||
let midFolder = Group({ title: 'middle', group: topFolder });
|
||||
let bookmarks = [
|
||||
Bookmark({ title: 'moz1', url: 'http://moz1.com', group: midFolder}),
|
||||
Bookmark({ title: 'moz2', url: 'http://moz2.com', group: midFolder}),
|
||||
Bookmark({ title: 'moz3', url: 'http://moz3.com', group: midFolder})
|
||||
];
|
||||
|
||||
let saved = [];
|
||||
save(bookmarks).on('data', (data) => saved.push(data)).on('end', () => {
|
||||
save(remove(topFolder)).on('end', () => {
|
||||
assert.equal(saved.length, 5, 'all items should have been saved');
|
||||
saved.map((item) => {
|
||||
assert.throws(function () {
|
||||
bmsrv.getItemTitle(item.id);
|
||||
}, 'item should no longer exist');
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.testResolution = function (assert, done) {
|
||||
let firstSave, secondSave;
|
||||
createBookmarkItem().then((item) => {
|
||||
firstSave = item;
|
||||
assert.ok(item.updated, 'bookmark has updated time');
|
||||
item.title = 'my title';
|
||||
save(item).on('data', (item) => {
|
||||
secondSave = item;
|
||||
assert.ok(firstSave.updated < secondSave.updated, 'snapshots have different update times');
|
||||
firstSave.title = 'updated title';
|
||||
save(firstSave, { resolve: (mine, theirs) => {
|
||||
assert.equal(mine.title, 'updated title', 'correct data for my object');
|
||||
assert.equal(theirs.title, 'my title', 'correct data for their object');
|
||||
assert.equal(mine.url, theirs.url, 'other data is equal');
|
||||
assert.equal(mine.group, theirs.group, 'other data is equal');
|
||||
assert.ok(mine !== firstSave, 'instance is not passed in');
|
||||
assert.ok(theirs !== secondSave, 'instance is not passed in');
|
||||
assert.equal(mine.toString(), '[object Object]', 'serialized objects');
|
||||
assert.equal(theirs.toString(), '[object Object]', 'serialized objects');
|
||||
mine.title = 'a new title';
|
||||
return mine;
|
||||
}}).on('end', (results) => {
|
||||
let result = results[0];
|
||||
assert.equal(result.title, 'a new title', 'resolve handles results');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Same as the resolution test, but with the 'unsaved' snapshot
|
||||
*/
|
||||
exports.testResolutionMapping = function (assert, done) {
|
||||
let bookmark = Bookmark({ title: 'moz', url: 'http://bookmarks4life.com/' });
|
||||
save(bookmark).on('end', (saved) => {
|
||||
saved = saved[0];
|
||||
saved.title = 'updated title';
|
||||
save(saved).on('end', () => {
|
||||
bookmark.title = 'conflicting title';
|
||||
save(bookmark, { resolve: (mine, theirs) => {
|
||||
assert.equal(mine.title, 'conflicting title', 'correct data for my object');
|
||||
assert.equal(theirs.title, 'updated title', 'correct data for their object');
|
||||
assert.equal(mine.url, theirs.url, 'other data is equal');
|
||||
assert.equal(mine.group, theirs.group, 'other data is equal');
|
||||
assert.ok(mine !== bookmark, 'instance is not passed in');
|
||||
assert.ok(theirs !== saved, 'instance is not passed in');
|
||||
assert.equal(mine.toString(), '[object Object]', 'serialized objects');
|
||||
assert.equal(theirs.toString(), '[object Object]', 'serialized objects');
|
||||
mine.title = 'a new title';
|
||||
return mine;
|
||||
}}).on('end', (results) => {
|
||||
let result = results[0];
|
||||
assert.equal(result.title, 'a new title', 'resolve handles results');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.testUpdateTags = function (assert, done) {
|
||||
createBookmarkItem({ tags: ['spidermonkey'] }).then(bookmark => {
|
||||
bookmark.tags.add('jagermonkey');
|
||||
bookmark.tags.add('ionmonkey');
|
||||
bookmark.tags.delete('spidermonkey');
|
||||
save(bookmark).on('data', saved => {
|
||||
assert.equal(saved.tags.size, 2, 'should have 2 tags');
|
||||
assert.ok(saved.tags.has('jagermonkey'), 'should have added tag');
|
||||
assert.ok(saved.tags.has('ionmonkey'), 'should have added tag');
|
||||
assert.ok(!saved.tags.has('spidermonkey'), 'should not have removed tag');
|
||||
done();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* View `createBookmarkTree` in `./places-helper.js` to see
|
||||
* expected tree construction
|
||||
*/
|
||||
|
||||
exports.testSearchByGroupSimple = function (assert, done) {
|
||||
createBookmarkTree().then(() => {
|
||||
// In initial release of Places API, groups can only be queried
|
||||
// via a 'simple query', which is one folder set, and no other
|
||||
// parameters
|
||||
return searchP({ group: UNSORTED });
|
||||
}).then(results => {
|
||||
let groups = results.filter(({type}) => type === 'group');
|
||||
assert.equal(groups.length, 2, 'returns folders');
|
||||
assert.equal(results.length, 7,
|
||||
'should return all bookmarks and folders under UNSORTED');
|
||||
assert.equal(groups[0].toString(), '[object Group]', 'returns instance');
|
||||
return searchP({
|
||||
group: groups.filter(({title}) => title === 'mozgroup')[0]
|
||||
});
|
||||
}).then(results => {
|
||||
let groups = results.filter(({type}) => type === 'group');
|
||||
assert.equal(groups.length, 1, 'returns one subfolder');
|
||||
assert.equal(results.length, 6,
|
||||
'returns all children bookmarks/folders');
|
||||
assert.ok(results.filter(({url}) => url === 'http://w3schools.com/'),
|
||||
'returns nested children');
|
||||
done();
|
||||
}).then(null, console.error);
|
||||
};
|
||||
|
||||
exports.testSearchByGroupComplex = function (assert, done) {
|
||||
let mozgroup;
|
||||
createBookmarkTree().then(results => {
|
||||
mozgroup = results.filter(({title}) => title === 'mozgroup')[0];
|
||||
return searchP({ group: mozgroup, query: 'javascript' });
|
||||
}).then(results => {
|
||||
assert.equal(results.length, 1, 'only one javascript result under mozgroup');
|
||||
assert.equal(results[0].url, 'http://w3schools.com/', 'correct result');
|
||||
return searchP({ group: mozgroup, url: '*.mozilla.org' });
|
||||
}).then(results => {
|
||||
assert.equal(results.length, 2, 'expected results');
|
||||
assert.ok(
|
||||
!results.filter(({url}) => /developer.mozilla/.test(url)).length,
|
||||
'does not find results from other folders');
|
||||
done();
|
||||
}, console.error);
|
||||
};
|
||||
|
||||
exports.testSearchEmitters = function (assert, done) {
|
||||
createBookmarkTree().then(() => {
|
||||
let count = 0;
|
||||
search({ tags: ['mozilla', 'firefox'] }).on('data', data => {
|
||||
assert.ok(/mozilla|firefox/.test(data.title), 'one of the correct items');
|
||||
assert.ok(data.tags.has('firefox'), 'has firefox tag');
|
||||
assert.ok(data.tags.has('mozilla'), 'has mozilla tag');
|
||||
assert.equal(data + '', '[object Bookmark]', 'returns bookmark');
|
||||
count++;
|
||||
}).on('end', data => {
|
||||
assert.equal(count, 3, 'data event was called for each item');
|
||||
assert.equal(data.length, 3,
|
||||
'should return two bookmarks that have both mozilla AND firefox');
|
||||
assert.equal(data[0].title, 'mozilla.com', 'returns correct bookmark');
|
||||
assert.equal(data[1].title, 'mozilla.org', 'returns correct bookmark');
|
||||
assert.equal(data[2].title, 'firefox', 'returns correct bookmark');
|
||||
assert.equal(data[0] + '', '[object Bookmark]', 'returns bookmarks');
|
||||
done();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.testSearchTags = function (assert, done) {
|
||||
createBookmarkTree().then(() => {
|
||||
// AND tags
|
||||
return searchP({ tags: ['mozilla', 'firefox'] });
|
||||
}).then(data => {
|
||||
assert.equal(data.length, 3,
|
||||
'should return two bookmarks that have both mozilla AND firefox');
|
||||
assert.equal(data[0].title, 'mozilla.com', 'returns correct bookmark');
|
||||
assert.equal(data[1].title, 'mozilla.org', 'returns correct bookmark');
|
||||
assert.equal(data[2].title, 'firefox', 'returns correct bookmark');
|
||||
assert.equal(data[0] + '', '[object Bookmark]', 'returns bookmarks');
|
||||
return searchP([{tags: ['firefox']}, {tags: ['javascript']}]);
|
||||
}).then(data => {
|
||||
// OR tags
|
||||
assert.equal(data.length, 6,
|
||||
'should return all bookmarks with firefox OR javascript tag');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Tests 4 scenarios
|
||||
* '*.mozilla.com'
|
||||
* 'mozilla.com'
|
||||
* 'http://mozilla.com/'
|
||||
* 'http://mozilla.com/*'
|
||||
*/
|
||||
exports.testSearchURL = function (assert, done) {
|
||||
createBookmarkTree().then(() => {
|
||||
return searchP({ url: 'mozilla.org' });
|
||||
}).then(data => {
|
||||
assert.equal(data.length, 2, 'only URLs with host domain');
|
||||
assert.equal(data[0].url, 'http://mozilla.org/');
|
||||
assert.equal(data[1].url, 'http://mozilla.org/thunderbird/');
|
||||
return searchP({ url: '*.mozilla.org' });
|
||||
}).then(data => {
|
||||
assert.equal(data.length, 3, 'returns domain and when host is other than domain');
|
||||
assert.equal(data[0].url, 'http://mozilla.org/');
|
||||
assert.equal(data[1].url, 'http://mozilla.org/thunderbird/');
|
||||
assert.equal(data[2].url, 'http://developer.mozilla.org/en-US/');
|
||||
return searchP({ url: 'http://mozilla.org' });
|
||||
}).then(data => {
|
||||
assert.equal(data.length, 1, 'only exact URL match');
|
||||
assert.equal(data[0].url, 'http://mozilla.org/');
|
||||
return searchP({ url: 'http://mozilla.org/*' });
|
||||
}).then(data => {
|
||||
assert.equal(data.length, 2, 'only URLs that begin with query');
|
||||
assert.equal(data[0].url, 'http://mozilla.org/');
|
||||
assert.equal(data[1].url, 'http://mozilla.org/thunderbird/');
|
||||
return searchP([{ url: 'mozilla.org' }, { url: 'component.fm' }]);
|
||||
}).then(data => {
|
||||
assert.equal(data.length, 3, 'returns URLs that match EITHER query');
|
||||
assert.equal(data[0].url, 'http://mozilla.org/');
|
||||
assert.equal(data[1].url, 'http://mozilla.org/thunderbird/');
|
||||
assert.equal(data[2].url, 'http://component.fm/');
|
||||
}).then(() => {
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Searches url, title, tags
|
||||
*/
|
||||
exports.testSearchQuery = function (assert, done) {
|
||||
createBookmarkTree().then(() => {
|
||||
return searchP({ query: 'thunder' });
|
||||
}).then(data => {
|
||||
assert.equal(data.length, 3);
|
||||
assert.equal(data[0].title, 'mozilla.com', 'query matches tag, url, or title');
|
||||
assert.equal(data[1].title, 'mozilla.org', 'query matches tag, url, or title');
|
||||
assert.equal(data[2].title, 'thunderbird', 'query matches tag, url, or title');
|
||||
return searchP([{ query: 'rust' }, { query: 'component' }]);
|
||||
}).then(data => {
|
||||
// rust OR component
|
||||
assert.equal(data.length, 3);
|
||||
assert.equal(data[0].title, 'mozilla.com', 'query matches tag, url, or title');
|
||||
assert.equal(data[1].title, 'mozilla.org', 'query matches tag, url, or title');
|
||||
assert.equal(data[2].title, 'web audio components', 'query matches tag, url, or title');
|
||||
return searchP([{ query: 'moz', tags: ['javascript']}]);
|
||||
}).then(data => {
|
||||
assert.equal(data.length, 1);
|
||||
assert.equal(data[0].title, 'mdn',
|
||||
'only one item matches moz query AND has a javascript tag');
|
||||
}).then(() => {
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Test caching on bulk calls.
|
||||
* Each construction of a bookmark item snapshot results in
|
||||
* the recursive lookup of parent groups up to the root groups --
|
||||
* ensure that the appropriate instances equal each other, and no duplicate
|
||||
* fetches are called
|
||||
*
|
||||
* Implementation-dependent, this checks the host event `sdk-places-bookmarks-get`,
|
||||
* and if implementation changes, this could increase or decrease
|
||||
*/
|
||||
|
||||
exports.testCaching = function (assert, done) {
|
||||
let count = 0;
|
||||
let stream = filter(request, ({event}) =>
|
||||
/sdk-places-bookmarks-get/.test(event));
|
||||
on(stream, 'data', handle);
|
||||
|
||||
let group = { type: 'group', title: 'mozgroup' };
|
||||
let bookmarks = [
|
||||
{ title: 'moz1', url: 'http://moz1.com', type: 'bookmark', group: group },
|
||||
{ title: 'moz2', url: 'http://moz2.com', type: 'bookmark', group: group },
|
||||
{ title: 'moz3', url: 'http://moz3.com', type: 'bookmark', group: group }
|
||||
];
|
||||
|
||||
/*
|
||||
* Use timeout in tests since the platform calls are synchronous
|
||||
* and the counting event shim may not have occurred yet
|
||||
*/
|
||||
|
||||
saveP(bookmarks).then(() => {
|
||||
assert.equal(count, 0, 'all new items and root group, no fetches should occur');
|
||||
count = 0;
|
||||
return saveP([
|
||||
{ title: 'moz4', url: 'http://moz4.com', type: 'bookmark', group: group },
|
||||
{ title: 'moz5', url: 'http://moz5.com', type: 'bookmark', group: group }
|
||||
]);
|
||||
// Test `save` look-up
|
||||
}).then(() => {
|
||||
assert.equal(count, 1, 'should only look up parent once');
|
||||
count = 0;
|
||||
return searchP({ query: 'moz' });
|
||||
}).then(results => {
|
||||
// Should query for each bookmark (5) from the query (id -> data),
|
||||
// their parent during `construct` (1) and the root shouldn't
|
||||
// require a lookup
|
||||
assert.equal(count, 6, 'lookup occurs once for each item and parent');
|
||||
off(stream, 'data', handle);
|
||||
done();
|
||||
});
|
||||
|
||||
function handle ({data}) count++
|
||||
};
|
||||
|
||||
/*
|
||||
* Search Query Options
|
||||
*/
|
||||
|
||||
exports.testSearchCount = function (assert, done) {
|
||||
let max = 8;
|
||||
createBookmarkTree()
|
||||
.then(testCount(1))
|
||||
.then(testCount(2))
|
||||
.then(testCount(3))
|
||||
.then(testCount(5))
|
||||
.then(testCount(10))
|
||||
.then(() => {
|
||||
done();
|
||||
});
|
||||
|
||||
function testCount (n) {
|
||||
return function () {
|
||||
return searchP({}, { count: n }).then(results => {
|
||||
if (n > max) n = max;
|
||||
assert.equal(results.length, n,
|
||||
'count ' + n + ' returns ' + n + ' results');
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
exports.testSearchSort = function (assert, done) {
|
||||
let urls = [
|
||||
'http://mozilla.com/', 'http://webaud.io/', 'http://mozilla.com/webfwd/',
|
||||
'http://developer.mozilla.com/', 'http://bandcamp.com/'
|
||||
];
|
||||
|
||||
saveP(
|
||||
urls.map(url =>
|
||||
Bookmark({ url: url, title: url.replace(/http:\/\/|\//g,'')}))
|
||||
).then(() => {
|
||||
return searchP({}, { sort: 'title' });
|
||||
}).then(results => {
|
||||
checkOrder(results, [4,3,0,2,1]);
|
||||
return searchP({}, { sort: 'title', descending: true });
|
||||
}).then(results => {
|
||||
checkOrder(results, [1,2,0,3,4]);
|
||||
return searchP({}, { sort: 'url' });
|
||||
}).then(results => {
|
||||
checkOrder(results, [4,3,0,2,1]);
|
||||
return searchP({}, { sort: 'url', descending: true });
|
||||
}).then(results => {
|
||||
checkOrder(results, [1,2,0,3,4]);
|
||||
return addVisits(['http://mozilla.com/', 'http://mozilla.com']);
|
||||
}).then(() =>
|
||||
saveP(Bookmark({ url: 'http://github.com', title: 'github.com' }))
|
||||
).then(() => addVisits('http://bandcamp.com/'))
|
||||
.then(() => searchP({ query: 'webfwd' }))
|
||||
.then(results => {
|
||||
results[0].title = 'new title for webfwd';
|
||||
return saveP(results[0]);
|
||||
})
|
||||
.then(() =>
|
||||
searchP({}, { sort: 'visitCount' })
|
||||
).then(results => {
|
||||
assert.equal(results[5].url, 'http://mozilla.com/',
|
||||
'last entry is the highest visit count');
|
||||
return searchP({}, { sort: 'visitCount', descending: true });
|
||||
}).then(results => {
|
||||
assert.equal(results[0].url, 'http://mozilla.com/',
|
||||
'first entry is the highest visit count');
|
||||
return searchP({}, { sort: 'date' });
|
||||
}).then(results => {
|
||||
assert.equal(results[5].url, 'http://bandcamp.com/',
|
||||
'latest visited should be first');
|
||||
return searchP({}, { sort: 'date', descending: true });
|
||||
}).then(results => {
|
||||
assert.equal(results[0].url, 'http://bandcamp.com/',
|
||||
'latest visited should be at the end');
|
||||
return searchP({}, { sort: 'dateAdded' });
|
||||
}).then(results => {
|
||||
assert.equal(results[5].url, 'http://github.com/',
|
||||
'last added should be at the end');
|
||||
return searchP({}, { sort: 'dateAdded', descending: true });
|
||||
}).then(results => {
|
||||
assert.equal(results[0].url, 'http://github.com/',
|
||||
'last added should be first');
|
||||
return searchP({}, { sort: 'lastModified' });
|
||||
}).then(results => {
|
||||
assert.equal(results[5].url, 'http://mozilla.com/webfwd/',
|
||||
'last modified should be last');
|
||||
return searchP({}, { sort: 'lastModified', descending: true });
|
||||
}).then(results => {
|
||||
assert.equal(results[0].url, 'http://mozilla.com/webfwd/',
|
||||
'last modified should be first');
|
||||
}).then(() => {
|
||||
done();
|
||||
});
|
||||
|
||||
function checkOrder (results, nums) {
|
||||
assert.equal(results.length, nums.length, 'expected return count');
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
assert.equal(results[i].url, urls[nums[i]], 'successful order');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.testSearchComplexQueryWithOptions = function (assert, done) {
|
||||
createBookmarkTree().then(() => {
|
||||
return searchP([
|
||||
{ tags: ['rust'], url: '*.mozilla.org' },
|
||||
{ tags: ['javascript'], query: 'mozilla' }
|
||||
], { sort: 'title' });
|
||||
}).then(results => {
|
||||
let expected = [
|
||||
'http://developer.mozilla.org/en-US/',
|
||||
'http://mozilla.org/'
|
||||
];
|
||||
for (let i = 0; i < expected.length; i++)
|
||||
assert.equal(results[i].url, expected[i], 'correct ordering and item');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testCheckSaveOrder = function (assert, done) {
|
||||
let group = Group({ title: 'mygroup' });
|
||||
let bookmarks = [
|
||||
Bookmark({ url: 'http://url1.com', title: 'url1', group: group }),
|
||||
Bookmark({ url: 'http://url2.com', title: 'url2', group: group }),
|
||||
Bookmark({ url: 'http://url3.com', title: 'url3', group: group }),
|
||||
Bookmark({ url: 'http://url4.com', title: 'url4', group: group }),
|
||||
Bookmark({ url: 'http://url5.com', title: 'url5', group: group })
|
||||
];
|
||||
saveP(bookmarks).then(results => {
|
||||
for (let i = 0; i < bookmarks.length; i++)
|
||||
assert.equal(results[i].url, bookmarks[i].url,
|
||||
'correct ordering of bookmark results');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
before(exports, name => {
|
||||
clearAllBookmarks();
|
||||
});
|
||||
|
||||
after(exports, name => {
|
||||
clearAllBookmarks();
|
||||
});
|
||||
|
||||
// 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.
|
||||
function unsupported (err) {
|
||||
if (!/^Unsupported Application/.test(err.message))
|
||||
throw err;
|
||||
|
||||
module.exports = {
|
||||
"test Unsupported Application": function Unsupported (assert) {
|
||||
assert.pass(err.message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function saveP () {
|
||||
return promisedEmitter(save.apply(null, Array.slice(arguments)));
|
||||
}
|
||||
|
||||
function searchP () {
|
||||
return promisedEmitter(search.apply(null, Array.slice(arguments)));
|
||||
}
|
||||
require('test').run(exports);
|
@ -11,6 +11,7 @@ const host = 'http://localhost:' + port;
|
||||
const { onFaviconChange, serve, binFavicon } = require('./favicon-helpers');
|
||||
const { once } = require('sdk/system/events');
|
||||
const { defer } = require('sdk/core/promise');
|
||||
const { clearHistory } = require('./places-helper');
|
||||
const faviconService = Cc["@mozilla.org/browser/favicon-service;1"].
|
||||
getService(Ci.nsIFaviconService);
|
||||
|
||||
@ -172,8 +173,10 @@ function waitAndExpire (url) {
|
||||
|
||||
function complete(tab, srv, done) {
|
||||
tab.close(function () {
|
||||
srv.stop(done);
|
||||
})
|
||||
clearHistory(() => {
|
||||
srv.stop(done);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
require("test").run(exports);
|
||||
|
264
addon-sdk/source/test/test-places-history.js
Normal file
264
addon-sdk/source/test/test-places-history.js
Normal file
@ -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 { Cc, Ci } = require('chrome');
|
||||
const { defer, all } = require('sdk/core/promise');
|
||||
const { has } = require('sdk/util/array');
|
||||
const { setTimeout } = require('sdk/timers');
|
||||
const { before, after } = require('sdk/test/utils');
|
||||
try {
|
||||
const {
|
||||
search
|
||||
} = require('sdk/places/history');
|
||||
const {
|
||||
invalidResolve, invalidReject, clearBookmarks, createTree,
|
||||
compareWithHost, clearAllBookmarks, addVisits, clearHistory
|
||||
} = require('./places-helper');
|
||||
const { promisedEmitter } = require('sdk/places/utils');
|
||||
const hsrv = Cc['@mozilla.org/browser/nav-history-service;1'].
|
||||
getService(Ci.nsINavHistoryService);
|
||||
} catch(e) { unsupported(e); }
|
||||
|
||||
exports.testEmptyQuery = function (assert, done) {
|
||||
let within = toBeWithin();
|
||||
addVisits([
|
||||
'http://simplequery-1.com', 'http://simplequery-2.com'
|
||||
]).then(searchP).then(results => {
|
||||
assert.equal(results.length, 2, 'Correct number of entries returned');
|
||||
assert.equal(results[0].url, 'http://simplequery-1.com/',
|
||||
'matches url');
|
||||
assert.equal(results[1].url, 'http://simplequery-2.com/',
|
||||
'matches url');
|
||||
assert.equal(results[0].title, 'Test visit for ' + results[0].url,
|
||||
'title matches');
|
||||
assert.equal(results[1].title, 'Test visit for ' + results[1].url,
|
||||
'title matches');
|
||||
assert.equal(results[0].visitCount, 1, 'matches access');
|
||||
assert.equal(results[1].visitCount, 1, 'matches access');
|
||||
assert.ok(within(results[0].time), 'accurate access time');
|
||||
assert.ok(within(results[1].time), 'accurate access time');
|
||||
assert.equal(Object.keys(results[0]).length, 4,
|
||||
'no addition exposed properties on history result');
|
||||
done();
|
||||
}, invalidReject);
|
||||
};
|
||||
|
||||
exports.testVisitCount = function (assert, done) {
|
||||
addVisits([
|
||||
'http://simplequery-1.com', 'http://simplequery-1.com',
|
||||
'http://simplequery-1.com', 'http://simplequery-1.com'
|
||||
]).then(searchP).then(results => {
|
||||
assert.equal(results.length, 1, 'Correct number of entries returned');
|
||||
assert.equal(results[0].url, 'http://simplequery-1.com/', 'correct url');
|
||||
assert.equal(results[0].visitCount, 4, 'matches access count');
|
||||
done();
|
||||
}, invalidReject);
|
||||
};
|
||||
|
||||
/*
|
||||
* Tests 4 scenarios
|
||||
* '*.mozilla.org'
|
||||
* 'mozilla.org'
|
||||
* 'http://mozilla.org/'
|
||||
* 'http://mozilla.org/*'
|
||||
*/
|
||||
exports.testSearchURL = function (assert, done) {
|
||||
addVisits([
|
||||
'http://developer.mozilla.org', 'http://mozilla.org',
|
||||
'http://mozilla.org/index', 'https://mozilla.org'
|
||||
]).then(() => searchP({ url: '*.mozilla.org' }))
|
||||
.then(results => {
|
||||
assert.equal(results.length, 4, 'returns all entries');
|
||||
return searchP({ url: 'mozilla.org' });
|
||||
}).then(results => {
|
||||
assert.equal(results.length, 3, 'returns entries where mozilla.org is host');
|
||||
return searchP({ url: 'http://mozilla.org/' });
|
||||
}).then(results => {
|
||||
assert.equal(results.length, 1, 'should just be an exact match');
|
||||
return searchP({ url: 'http://mozilla.org/*' });
|
||||
}).then(results => {
|
||||
assert.equal(results.length, 2, 'should match anything starting with substring');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testSearchTimeRange = function (assert, done) {
|
||||
let firstTime, secondTime;
|
||||
addVisits([
|
||||
'http://earlyvisit.org', 'http://earlyvisit.org/earlytown.html'
|
||||
]).then(searchP).then(results => {
|
||||
firstTime = results[0].time;
|
||||
var deferred = defer();
|
||||
setTimeout(function () deferred.resolve(), 1000);
|
||||
return deferred.promise;
|
||||
}).then(() => {
|
||||
return addVisits(['http://newvisit.org', 'http://newvisit.org/whoawhoa.html']);
|
||||
}).then(searchP).then(results => {
|
||||
results.filter(({url, time}) => {
|
||||
if (/newvisit/.test(url)) secondTime = time;
|
||||
});
|
||||
return searchP({ from: firstTime - 1000 });
|
||||
}).then(results => {
|
||||
assert.equal(results.length, 4, 'should return all entries');
|
||||
return searchP({ to: firstTime + 500 });
|
||||
}).then(results => {
|
||||
assert.equal(results.length, 2, 'should return only first entries');
|
||||
results.map(item => {
|
||||
assert.ok(/earlyvisit/.test(item.url), 'correct entry');
|
||||
});
|
||||
return searchP({ from: firstTime + 500 });
|
||||
}).then(results => {
|
||||
assert.equal(results.length, 2, 'should return only last entries');
|
||||
results.map(item => {
|
||||
assert.ok(/newvisit/.test(item.url), 'correct entry');
|
||||
});
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testSearchQuery = function (assert, done) {
|
||||
addVisits([
|
||||
'http://mozilla.com', 'http://webaud.io', 'http://mozilla.com/webfwd'
|
||||
]).then(() => {
|
||||
return searchP({ query: 'moz' });
|
||||
}).then(results => {
|
||||
assert.equal(results.length, 2, 'should return urls that match substring');
|
||||
results.map(({url}) => {
|
||||
assert.ok(/moz/.test(url), 'correct item');
|
||||
});
|
||||
return searchP([{ query: 'webfwd' }, { query: 'aud.io' }]);
|
||||
}).then(results => {
|
||||
assert.equal(results.length, 2, 'should OR separate queries');
|
||||
results.map(({url}) => {
|
||||
assert.ok(/webfwd|aud\.io/.test(url), 'correct item');
|
||||
});
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Query Options
|
||||
*/
|
||||
|
||||
exports.testSearchCount = function (assert, done) {
|
||||
addVisits([
|
||||
'http://mozilla.com', 'http://webaud.io', 'http://mozilla.com/webfwd',
|
||||
'http://developer.mozilla.com', 'http://bandcamp.com'
|
||||
]).then(testCount(1))
|
||||
.then(testCount(2))
|
||||
.then(testCount(3))
|
||||
.then(testCount(5))
|
||||
.then(done);
|
||||
|
||||
function testCount (n) {
|
||||
return function () {
|
||||
return searchP({}, { count: n }).then(results => {
|
||||
assert.equal(results.length, n,
|
||||
'count ' + n + ' returns ' + n + ' results');
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
exports.testSearchSort = function (assert, done) {
|
||||
let places = [
|
||||
'http://mozilla.com/', 'http://webaud.io/', 'http://mozilla.com/webfwd/',
|
||||
'http://developer.mozilla.com/', 'http://bandcamp.com/'
|
||||
];
|
||||
addVisits(places).then(() => {
|
||||
return searchP({}, { sort: 'title' });
|
||||
}).then(results => {
|
||||
checkOrder(results, [4,3,0,2,1]);
|
||||
return searchP({}, { sort: 'title', descending: true });
|
||||
}).then(results => {
|
||||
checkOrder(results, [1,2,0,3,4]);
|
||||
return searchP({}, { sort: 'url' });
|
||||
}).then(results => {
|
||||
checkOrder(results, [4,3,0,2,1]);
|
||||
return searchP({}, { sort: 'url', descending: true });
|
||||
}).then(results => {
|
||||
checkOrder(results, [1,2,0,3,4]);
|
||||
return addVisits('http://mozilla.com') // for visit conut
|
||||
.then(() => addVisits('http://github.com')); // for checking date
|
||||
}).then(() => {
|
||||
return searchP({}, { sort: 'visitCount' });
|
||||
}).then(results => {
|
||||
assert.equal(results[5].url, 'http://mozilla.com/',
|
||||
'last entry is the highest visit count');
|
||||
return searchP({}, { sort: 'visitCount', descending: true });
|
||||
}).then(results => {
|
||||
assert.equal(results[0].url, 'http://mozilla.com/',
|
||||
'first entry is the highest visit count');
|
||||
return searchP({}, { sort: 'date' });
|
||||
}).then(results => {
|
||||
assert.equal(results[5].url, 'http://github.com/',
|
||||
'latest visited should be first');
|
||||
return searchP({}, { sort: 'date', descending: true });
|
||||
}).then(results => {
|
||||
assert.equal(results[0].url, 'http://github.com/',
|
||||
'latest visited should be at the end');
|
||||
}).then(done);
|
||||
|
||||
function checkOrder (results, nums) {
|
||||
assert.equal(results.length, nums.length, 'expected return count');
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
assert.equal(results[i].url, places[nums[i]], 'successful order');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.testEmitters = function (assert, done) {
|
||||
let urls = [
|
||||
'http://mozilla.com/', 'http://webaud.io/', 'http://mozilla.com/webfwd/',
|
||||
'http://developer.mozilla.com/', 'http://bandcamp.com/'
|
||||
];
|
||||
addVisits(urls).then(() => {
|
||||
let count = 0;
|
||||
search().on('data', item => {
|
||||
assert.ok(~urls.indexOf(item.url), 'data value found in url list');
|
||||
count++;
|
||||
}).on('end', results => {
|
||||
assert.equal(results.length, 5, 'correct count of items');
|
||||
assert.equal(count, 5, 'data event called 5 times');
|
||||
done();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function toBeWithin (range) {
|
||||
range = range || 2000;
|
||||
var current = new Date() * 1000; // convert to microseconds
|
||||
return compared => {
|
||||
return compared - current < range;
|
||||
};
|
||||
}
|
||||
|
||||
function clear (done) {
|
||||
clearAllBookmarks();
|
||||
clearHistory(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.
|
||||
function unsupported (err) {
|
||||
if (!/^Unsupported Application/.test(err.message))
|
||||
throw err;
|
||||
|
||||
module.exports = {
|
||||
"test Unsupported Application": function Unsupported (assert) {
|
||||
assert.pass(err.message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function searchP () {
|
||||
return promisedEmitter(search.apply(null, Array.slice(arguments)));
|
||||
}
|
||||
|
||||
before(exports, (name, done) => clear(done));
|
||||
after(exports, (name, done) => clear(done));
|
||||
|
||||
require('test').run(exports);
|
292
addon-sdk/source/test/test-places-host.js
Normal file
292
addon-sdk/source/test/test-places-host.js
Normal file
@ -0,0 +1,292 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.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 { defer, all } = require('sdk/core/promise');
|
||||
const { setTimeout } = require('sdk/timers');
|
||||
const { newURI } = require('sdk/url/utils');
|
||||
const { send } = require('sdk/addon/events');
|
||||
try {
|
||||
require('sdk/places/host/host-bookmarks');
|
||||
require('sdk/places/host/host-tags');
|
||||
require('sdk/places/host/host-query');
|
||||
const {
|
||||
invalidResolve, invalidReject, clearBookmarks, createTree,
|
||||
compareWithHost, clearAllBookmarks, createBookmark, createBookmarkTree
|
||||
} = require('./places-helper');
|
||||
|
||||
const bmsrv = Cc['@mozilla.org/browser/nav-bookmarks-service;1'].
|
||||
getService(Ci.nsINavBookmarksService);
|
||||
const hsrv = Cc['@mozilla.org/browser/nav-history-service;1'].
|
||||
getService(Ci.nsINavHistoryService);
|
||||
const tagsrv = Cc['@mozilla.org/browser/tagging-service;1'].
|
||||
getService(Ci.nsITaggingService);
|
||||
clearAllBookmarks();
|
||||
} catch(e) { unsupported(e); }
|
||||
|
||||
exports.testBookmarksCreate = function (assert, done) {
|
||||
let items = [{
|
||||
title: 'my title',
|
||||
url: 'http://moz.com',
|
||||
tags: ['some', 'tags', 'yeah'],
|
||||
type: 'bookmark'
|
||||
}, {
|
||||
title: 'my folder',
|
||||
type: 'group',
|
||||
group: bmsrv.bookmarksMenuFolder
|
||||
}, {
|
||||
type: 'separator',
|
||||
group: bmsrv.unfiledBookmarksFolder
|
||||
}];
|
||||
|
||||
all(items.map(function (item) {
|
||||
return send('sdk-places-bookmarks-create', item).then(function (data) {
|
||||
compareWithHost(assert, data);
|
||||
}, invalidReject(assert));
|
||||
})).then(function () {
|
||||
clearAllBookmarks();
|
||||
done();
|
||||
}, invalidReject(assert));
|
||||
};
|
||||
|
||||
exports.testBookmarksCreateFail = function (assert, done) {
|
||||
let items = [{
|
||||
title: 'my title',
|
||||
url: 'not-a-url',
|
||||
type: 'bookmark'
|
||||
}, {
|
||||
type: 'group',
|
||||
group: bmsrv.bookmarksMenuFolder
|
||||
}, {
|
||||
group: bmsrv.unfiledBookmarksFolder
|
||||
}];
|
||||
all(items.map(function (item) {
|
||||
return send('sdk-places-bookmarks-create', item).then(null, function (reason) {
|
||||
assert.ok(reason, 'bookmark create should fail');
|
||||
});
|
||||
})).then(function () {
|
||||
clearAllBookmarks();
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testBookmarkLastUpdated = function (assert, done) {
|
||||
let timestamp;
|
||||
let item;
|
||||
createBookmark().then(function (data) {
|
||||
item = data;
|
||||
timestamp = item.updated;
|
||||
return send('sdk-places-bookmarks-last-updated', { id: item.id });
|
||||
}).then(function (updated) {
|
||||
assert.equal(timestamp, updated, 'should return last updated time');
|
||||
item.title = 'updated mozilla';
|
||||
return send('sdk-places-bookmarks-save', item).then(function (data) {
|
||||
let deferred = defer();
|
||||
setTimeout(function () deferred.resolve(data), 100);
|
||||
return deferred.promise;
|
||||
});
|
||||
}).then(function (data) {
|
||||
assert.ok(data.updated > timestamp, 'time has elapsed and updated the updated property');
|
||||
clearAllBookmarks();
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testBookmarkRemove = function (assert, done) {
|
||||
let id;
|
||||
createBookmark().then(function (data) {
|
||||
id = data.id;
|
||||
compareWithHost(assert, data); // ensure bookmark exists
|
||||
bmsrv.getItemTitle(id); // does not throw an error
|
||||
return send('sdk-places-bookmarks-remove', data);
|
||||
}).then(function () {
|
||||
assert.throws(function () {
|
||||
bmsrv.getItemTitle(id);
|
||||
}, 'item should no longer exist');
|
||||
clearAllBookmarks();
|
||||
done();
|
||||
}, console.error);
|
||||
};
|
||||
|
||||
exports.testBookmarkGet = function (assert, done) {
|
||||
let bookmark;
|
||||
createBookmark().then(function (data) {
|
||||
bookmark = data;
|
||||
return send('sdk-places-bookmarks-get', { id: data.id });
|
||||
}).then(function (data) {
|
||||
'title url index group updated type tags'.split(' ').map(function (prop) {
|
||||
if (prop === 'tags') {
|
||||
for (let tag of bookmark.tags) {
|
||||
assert.ok(~data.tags.indexOf(tag),
|
||||
'correctly fetched tag ' + tag);
|
||||
}
|
||||
assert.equal(bookmark.tags.length, data.tags.length,
|
||||
'same amount of tags');
|
||||
}
|
||||
else
|
||||
assert.equal(bookmark[prop], data[prop], 'correctly fetched ' + prop);
|
||||
});
|
||||
clearAllBookmarks();
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testTagsTag = function (assert, done) {
|
||||
let url;
|
||||
createBookmark().then(function (data) {
|
||||
url = data.url;
|
||||
return send('sdk-places-tags-tag', {
|
||||
url: data.url, tags: ['mozzerella', 'foxfire']
|
||||
});
|
||||
}).then(function () {
|
||||
let tags = tagsrv.getTagsForURI(newURI(url));
|
||||
assert.ok(~tags.indexOf('mozzerella'), 'first tag found');
|
||||
assert.ok(~tags.indexOf('foxfire'), 'second tag found');
|
||||
assert.ok(~tags.indexOf('firefox'), 'default tag found');
|
||||
assert.equal(tags.length, 3, 'no extra tags');
|
||||
clearAllBookmarks();
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testTagsUntag = function (assert, done) {
|
||||
let item;
|
||||
createBookmark({tags: ['tag1', 'tag2', 'tag3']}).then(function (data) {
|
||||
item = data;
|
||||
return send('sdk-places-tags-untag', {
|
||||
url: item.url,
|
||||
tags: ['tag2', 'firefox']
|
||||
});
|
||||
}).then(function () {
|
||||
let tags = tagsrv.getTagsForURI(newURI(item.url));
|
||||
assert.ok(~tags.indexOf('tag1'), 'first tag persisted');
|
||||
assert.ok(~tags.indexOf('tag3'), 'second tag persisted');
|
||||
assert.ok(!~tags.indexOf('firefox'), 'first tag removed');
|
||||
assert.ok(!~tags.indexOf('tag2'), 'second tag removed');
|
||||
assert.equal(tags.length, 2, 'no extra tags');
|
||||
clearAllBookmarks();
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testTagsGetURLsByTag = function (assert, done) {
|
||||
let item;
|
||||
createBookmark().then(function (data) {
|
||||
item = data;
|
||||
return send('sdk-places-tags-get-urls-by-tag', {
|
||||
tag: 'firefox'
|
||||
});
|
||||
}).then(function(urls) {
|
||||
assert.equal(item.url, urls[0], 'returned correct url');
|
||||
assert.equal(urls.length, 1, 'returned only one url');
|
||||
clearAllBookmarks();
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testTagsGetTagsByURL = function (assert, done) {
|
||||
let item;
|
||||
createBookmark({ tags: ['firefox', 'mozilla', 'metal']}).then(function (data) {
|
||||
item = data;
|
||||
return send('sdk-places-tags-get-tags-by-url', {
|
||||
url: data.url,
|
||||
});
|
||||
}).then(function(tags) {
|
||||
assert.ok(~tags.indexOf('firefox'), 'returned first tag');
|
||||
assert.ok(~tags.indexOf('mozilla'), 'returned second tag');
|
||||
assert.ok(~tags.indexOf('metal'), 'returned third tag');
|
||||
assert.equal(tags.length, 3, 'returned all tags');
|
||||
clearAllBookmarks();
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testHostQuery = function (assert, done) {
|
||||
all([
|
||||
createBookmark({ url: 'http://firefox.com', tags: ['firefox', 'mozilla'] }),
|
||||
createBookmark({ url: 'http://mozilla.com', tags: ['mozilla'] }),
|
||||
createBookmark({ url: 'http://thunderbird.com' })
|
||||
]).then(data => {
|
||||
return send('sdk-places-query', {
|
||||
queries: { tags: ['mozilla'] },
|
||||
options: { sortingMode: 6, queryType: 1 } // sort by URI ascending, bookmarks only
|
||||
});
|
||||
}).then(results => {
|
||||
assert.equal(results.length, 2, 'should only return two');
|
||||
assert.equal(results[0].url, 'http://mozilla.com/', 'is sorted by URI asc');
|
||||
return send('sdk-places-query', {
|
||||
queries: { tags: ['mozilla'] },
|
||||
options: { sortingMode: 5, queryType: 1 } // sort by URI descending, bookmarks only
|
||||
});
|
||||
}).then(results => {
|
||||
assert.equal(results.length, 2, 'should only return two');
|
||||
assert.equal(results[0].url, 'http://firefox.com/', 'is sorted by URI desc');
|
||||
clearAllBookmarks();
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testHostMultiQuery = function (assert, done) {
|
||||
all([
|
||||
createBookmark({ url: 'http://firefox.com', tags: ['firefox', 'mozilla'] }),
|
||||
createBookmark({ url: 'http://mozilla.com', tags: ['mozilla'] }),
|
||||
createBookmark({ url: 'http://thunderbird.com' })
|
||||
]).then(data => {
|
||||
return send('sdk-places-query', {
|
||||
queries: [{ tags: ['firefox'] }, { uri: 'http://thunderbird.com/' }],
|
||||
options: { sortingMode: 5, queryType: 1 } // sort by URI descending, bookmarks only
|
||||
});
|
||||
}).then(results => {
|
||||
assert.equal(results.length, 2, 'should return 2 results ORing queries');
|
||||
assert.equal(results[0].url, 'http://firefox.com/', 'should match URL or tag');
|
||||
assert.equal(results[1].url, 'http://thunderbird.com/', 'should match URL or tag');
|
||||
return send('sdk-places-query', {
|
||||
queries: [{ tags: ['firefox'], url: 'http://mozilla.com/' }],
|
||||
options: { sortingMode: 5, queryType: 1 } // sort by URI descending, bookmarks only
|
||||
});
|
||||
}).then(results => {
|
||||
assert.equal(results.length, 0, 'query props should be AND\'d');
|
||||
clearAllBookmarks();
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
exports.testGetAllBookmarks = function (assert, done) {
|
||||
createBookmarkTree().then(() => {
|
||||
return send('sdk-places-bookmarks-get-all', {});
|
||||
}).then(res => {
|
||||
assert.equal(res.length, 8, 'all bookmarks returned');
|
||||
clearAllBookmarks();
|
||||
done();
|
||||
}, console.error);
|
||||
};
|
||||
|
||||
exports.testGetAllChildren = function (assert, done) {
|
||||
createBookmarkTree().then(results => {
|
||||
return send('sdk-places-bookmarks-get-children', {
|
||||
id: results.filter(({title}) => title === 'mozgroup')[0].id
|
||||
});
|
||||
}).then(results => {
|
||||
assert.equal(results.length, 5,
|
||||
'should return all children and folders at a single depth');
|
||||
clearAllBookmarks();
|
||||
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.
|
||||
function unsupported (err) {
|
||||
if (!/^Unsupported Application/.test(err.message))
|
||||
throw err;
|
||||
|
||||
module.exports = {
|
||||
"test Unsupported Application": function Unsupported (assert) {
|
||||
assert.pass(err.message);
|
||||
}
|
||||
};
|
||||
}
|
||||
require('test').run(exports);
|
91
addon-sdk/source/test/test-places-utils.js
Normal file
91
addon-sdk/source/test/test-places-utils.js
Normal file
@ -0,0 +1,91 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.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 { defer, all } = require('sdk/core/promise');
|
||||
const { setTimeout } = require('sdk/timers');
|
||||
try {
|
||||
const { TreeNode } = require('sdk/places/utils');
|
||||
} catch(e) { unsupported(e); }
|
||||
|
||||
exports['test construct tree'] = function (assert) {
|
||||
let tree = TreeNode(1);
|
||||
tree.add([2, 3, 4]);
|
||||
tree.get(2).add([2.1, 2.2, 2.3]);
|
||||
let newTreeNode = TreeNode(4.3);
|
||||
newTreeNode.add([4.31, 4.32]);
|
||||
tree.get(4).add([4.1, 4.2, newTreeNode]);
|
||||
|
||||
assert.equal(tree.get(2).value, 2, 'get returns node with correct value');
|
||||
assert.equal(tree.get(2.3).value, 2.3, 'get returns node with correct value');
|
||||
assert.equal(tree.get(4.32).value, 4.32, 'get returns node even if created from nested node');
|
||||
assert.equal(tree.get(4).children.length, 3, 'nodes have correct children length');
|
||||
assert.equal(tree.get(3).children.length, 0, 'nodes have correct children length');
|
||||
|
||||
assert.equal(tree.get(4).get(4.32).value, 4.32, 'node.get descends from itself');
|
||||
assert.equal(tree.get(4).get(2), null, 'node.get descends from itself fails if not descendant');
|
||||
};
|
||||
|
||||
exports['test walk'] = function (assert) {
|
||||
let resultsAll = [];
|
||||
let tree = TreeNode(1);
|
||||
tree.add([2, 3, 4]);
|
||||
tree.get(2).add([2.1, 2.2]);
|
||||
|
||||
tree.walk(function (node) {
|
||||
resultsAll.push(node.value);
|
||||
});
|
||||
|
||||
[1, 2, 2.1, 2.2, 3, 4].forEach(function (num) {
|
||||
assert.ok(~resultsAll.indexOf(num), 'function applied to each node from root');
|
||||
});
|
||||
|
||||
let resultsNode = [];
|
||||
tree.get(2).walk(function (node) resultsNode.push(node.value));
|
||||
|
||||
[2, 2.1, 2.2].forEach(function (num) {
|
||||
assert.ok(~resultsNode.indexOf(num), 'function applied to each node from node');
|
||||
});
|
||||
};
|
||||
|
||||
exports['test async walk'] = function (assert, done) {
|
||||
let resultsAll = [];
|
||||
let tree = TreeNode(1);
|
||||
tree.add([2, 3, 4]);
|
||||
tree.get(2).add([2.1, 2.2]);
|
||||
|
||||
tree.walk(function (node) {
|
||||
let deferred = defer();
|
||||
setTimeout(function () {
|
||||
resultsAll.push(node.value);
|
||||
deferred.resolve(node.value);
|
||||
}, node.value === 2 ? 50 : 5);
|
||||
return deferred.promise;
|
||||
}).then(function () {
|
||||
[1, 2, 2.1, 2.2, 3, 4].forEach(function (num) {
|
||||
assert.ok(~resultsAll.indexOf(num), 'function applied to each node from root');
|
||||
});
|
||||
assert.ok(resultsAll.indexOf(2) < resultsAll.indexOf(2.1),
|
||||
'child should wait for parent to complete');
|
||||
assert.ok(resultsAll.indexOf(2) < resultsAll.indexOf(2.2),
|
||||
'child should wait for parent to complete');
|
||||
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.
|
||||
function unsupported (err) {
|
||||
if (!/^Unsupported Application/.test(err.message))
|
||||
throw err;
|
||||
|
||||
module.exports = {
|
||||
"test Unsupported Application": function Unsupported (assert) {
|
||||
assert.pass(err.message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
require('test').run(exports);
|
@ -17,6 +17,9 @@ const { LoaderWithHookedConsole } = require("sdk/test/loader");
|
||||
const { getMode, isGlobalPBSupported,
|
||||
isWindowPBSupported, isTabPBSupported } = require('sdk/private-browsing/utils');
|
||||
const { pb } = require('./private-browsing/helper');
|
||||
const prefs = require('sdk/preferences/service');
|
||||
|
||||
const kAutoStartPref = "browser.privatebrowsing.autostart";
|
||||
|
||||
// is global pb is enabled?
|
||||
if (isGlobalPBSupported) {
|
||||
@ -105,3 +108,12 @@ exports.testGetOwnerWindow = function(test) {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
exports.testNewGlobalPBService = function(test) {
|
||||
test.assertEqual(isPrivate(), false, 'isPrivate() is false by default');
|
||||
prefs.set(kAutoStartPref, true);
|
||||
test.assertEqual(prefs.get(kAutoStartPref, false), true, kAutoStartPref + ' is true now');
|
||||
test.assertEqual(isPrivate(), true, 'isPrivate() is true now');
|
||||
prefs.set(kAutoStartPref, false);
|
||||
test.assertEqual(isPrivate(), false, 'isPrivate() is false again');
|
||||
}
|
||||
|
79
addon-sdk/source/test/test-rules.js
Normal file
79
addon-sdk/source/test/test-rules.js
Normal file
@ -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/. */
|
||||
'use strict';
|
||||
|
||||
const { Rules } = require('sdk/util/rules');
|
||||
const { on, off, emit } = require('sdk/event/core');
|
||||
|
||||
exports.testAdd = function (test, done) {
|
||||
let rules = Rules();
|
||||
let urls = [
|
||||
'http://www.firefox.com',
|
||||
'*.mozilla.org',
|
||||
'*.html5audio.org'
|
||||
];
|
||||
let count = 0;
|
||||
on(rules, 'add', function (rule) {
|
||||
if (count < urls.length) {
|
||||
test.ok(rules.get(rule), 'rule added to internal registry');
|
||||
test.equal(rule, urls[count], 'add event fired with proper params');
|
||||
if (++count < urls.length) rules.add(urls[count]);
|
||||
else done();
|
||||
}
|
||||
});
|
||||
rules.add(urls[0]);
|
||||
};
|
||||
|
||||
exports.testRemove = function (test, done) {
|
||||
let rules = Rules();
|
||||
let urls = [
|
||||
'http://www.firefox.com',
|
||||
'*.mozilla.org',
|
||||
'*.html5audio.org'
|
||||
];
|
||||
let count = 0;
|
||||
on(rules, 'remove', function (rule) {
|
||||
if (count < urls.length) {
|
||||
test.ok(!rules.get(rule), 'rule removed to internal registry');
|
||||
test.equal(rule, urls[count], 'remove event fired with proper params');
|
||||
if (++count < urls.length) rules.remove(urls[count]);
|
||||
else done();
|
||||
}
|
||||
});
|
||||
urls.forEach(function (url) rules.add(url));
|
||||
rules.remove(urls[0]);
|
||||
};
|
||||
|
||||
exports.testMatchesAny = function(test) {
|
||||
let rules = Rules();
|
||||
rules.add('*.mozilla.org');
|
||||
rules.add('data:*');
|
||||
matchTest('http://mozilla.org', true);
|
||||
matchTest('http://www.mozilla.org', true);
|
||||
matchTest('http://www.google.com', false);
|
||||
matchTest('data:text/html;charset=utf-8,', true);
|
||||
|
||||
function matchTest(string, expected) {
|
||||
test.equal(rules.matchesAny(string), expected,
|
||||
'Expected to find ' + string + ' in rules');
|
||||
}
|
||||
};
|
||||
|
||||
exports.testIterable = function(test) {
|
||||
let rules = Rules();
|
||||
rules.add('*.mozilla.org');
|
||||
rules.add('data:*');
|
||||
rules.add('http://google.com');
|
||||
rules.add('http://addons.mozilla.org');
|
||||
rules.remove('http://google.com');
|
||||
|
||||
test.equal(rules.length, 3, 'has correct length of keys');
|
||||
Array.forEach(rules, function (rule, i) {
|
||||
test.equal(rule, ['*.mozilla.org', 'data:*', 'http://addons.mozilla.org'][i]);
|
||||
});
|
||||
for (let i in rules)
|
||||
test.equal(rules[i], ['*.mozilla.org', 'data:*', 'http://addons.mozilla.org'][i]);
|
||||
};
|
||||
|
||||
require('test').run(exports);
|
@ -42,11 +42,47 @@ exports.testTabCounts = function(test) {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// TEST: tabs.activeTab getter
|
||||
exports.testActiveTab_getter = function(test) {
|
||||
test.waitUntilDone();
|
||||
let evtCount = 0;
|
||||
let activeTab = null;
|
||||
|
||||
function endTest(type, tab) {
|
||||
if (type == 'activate') {
|
||||
test.assertStrictEqual(tabs.activeTab, tab, 'the active tab is the opened tab');
|
||||
activeTab = tabs.activeTab;
|
||||
}
|
||||
else {
|
||||
test.assertEqual(tab.url, url, 'the opened tab has the correct url');
|
||||
}
|
||||
|
||||
if (++evtCount != 2)
|
||||
return;
|
||||
|
||||
test.assertStrictEqual(activeTab, tab, 'the active tab is the ready tab');
|
||||
test.assertStrictEqual(tabs.activeTab, tab, 'the active tab is the ready tab');
|
||||
|
||||
tab.close(function() {
|
||||
// end test
|
||||
test.done();
|
||||
});
|
||||
}
|
||||
|
||||
let url = URL.replace("#title#", "testActiveTab_getter");
|
||||
tabs.open({
|
||||
url: url,
|
||||
onReady: endTest.bind(null, 'ready'),
|
||||
onActivate: endTest.bind(null, 'activate')
|
||||
});
|
||||
};
|
||||
|
||||
// TEST: tab.activate()
|
||||
exports.testActiveTab_setter_alt = function(test) {
|
||||
exports.testActiveTab_setter = function(test) {
|
||||
test.waitUntilDone();
|
||||
|
||||
let url = URL.replace("#title#", "testActiveTab_setter_alt");
|
||||
let url = URL.replace("#title#", "testActiveTab_setter");
|
||||
let tab1URL = URL.replace("#title#", "tab1");
|
||||
|
||||
tabs.open({
|
||||
|
76
addon-sdk/source/test/test-test-utils-async.js
Normal file
76
addon-sdk/source/test/test-test-utils-async.js
Normal file
@ -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 { defer: async } = require('sdk/lang/functional');
|
||||
const { before, after } = require('sdk/test/utils');
|
||||
|
||||
let AFTER_RUN = 0;
|
||||
let BEFORE_RUN = 0;
|
||||
|
||||
/*
|
||||
* Tests are dependent on ordering, as the before and after functions
|
||||
* are called outside of each test, and sometimes checked in the next test
|
||||
* (like in the `after` tests)
|
||||
*/
|
||||
exports.testABeforeAsync = function (assert, done) {
|
||||
assert.equal(BEFORE_RUN, 1, 'before function was called');
|
||||
BEFORE_RUN = 0;
|
||||
AFTER_RUN = 0;
|
||||
async(done)();
|
||||
};
|
||||
|
||||
exports.testABeforeNameAsync = function (assert, done) {
|
||||
assert.equal(BEFORE_RUN, 2, 'before function was called with name');
|
||||
BEFORE_RUN = 0;
|
||||
AFTER_RUN = 0;
|
||||
async(done)();
|
||||
};
|
||||
|
||||
exports.testAfterAsync = function (assert, done) {
|
||||
assert.equal(AFTER_RUN, 1, 'after function was called previously');
|
||||
BEFORE_RUN = 0;
|
||||
AFTER_RUN = 0;
|
||||
async(done)();
|
||||
};
|
||||
|
||||
exports.testAfterNameAsync = function (assert, done) {
|
||||
assert.equal(AFTER_RUN, 2, 'after function was called with name');
|
||||
BEFORE_RUN = 0;
|
||||
AFTER_RUN = 0;
|
||||
async(done)();
|
||||
};
|
||||
|
||||
exports.testSyncABefore = function (assert) {
|
||||
assert.equal(BEFORE_RUN, 1, 'before function was called for sync test');
|
||||
BEFORE_RUN = 0;
|
||||
AFTER_RUN = 0;
|
||||
};
|
||||
|
||||
exports.testSyncAfter = function (assert) {
|
||||
assert.equal(AFTER_RUN, 1, 'after function was called for sync test');
|
||||
BEFORE_RUN = 0;
|
||||
AFTER_RUN = 0;
|
||||
};
|
||||
|
||||
before(exports, (name, done) => {
|
||||
if (name === 'testABeforeNameAsync')
|
||||
BEFORE_RUN = 2;
|
||||
else
|
||||
BEFORE_RUN = 1;
|
||||
async(done)();
|
||||
});
|
||||
|
||||
after(exports, (name, done) => {
|
||||
// testAfterName runs after testAfter, which is where this
|
||||
// check occurs in the assertation
|
||||
if (name === 'testAfterAsync')
|
||||
AFTER_RUN = 2;
|
||||
else
|
||||
AFTER_RUN = 1;
|
||||
async(done)();
|
||||
});
|
||||
|
||||
require('sdk/test').run(exports);
|
74
addon-sdk/source/test/test-test-utils-sync.js
Normal file
74
addon-sdk/source/test/test-test-utils-sync.js
Normal file
@ -0,0 +1,74 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.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 { defer: async } = require('sdk/lang/functional');
|
||||
const { before, after } = require('sdk/test/utils');
|
||||
|
||||
let AFTER_RUN = 0;
|
||||
let BEFORE_RUN = 0;
|
||||
|
||||
/*
|
||||
* Tests are dependent on ordering, as the before and after functions
|
||||
* are called outside of each test, and sometimes checked in the next test
|
||||
* (like in the `after` tests)
|
||||
*/
|
||||
exports.testABeforeAsync = function (assert, done) {
|
||||
assert.equal(BEFORE_RUN, 1, 'before function was called');
|
||||
BEFORE_RUN = 0;
|
||||
AFTER_RUN = 0;
|
||||
async(done)();
|
||||
};
|
||||
|
||||
exports.testABeforeNameAsync = function (assert, done) {
|
||||
assert.equal(BEFORE_RUN, 2, 'before function was called with name');
|
||||
BEFORE_RUN = 0;
|
||||
AFTER_RUN = 0;
|
||||
async(done)();
|
||||
};
|
||||
|
||||
exports.testAfterAsync = function (assert, done) {
|
||||
assert.equal(AFTER_RUN, 1, 'after function was called previously');
|
||||
BEFORE_RUN = 0;
|
||||
AFTER_RUN = 0;
|
||||
async(done)();
|
||||
};
|
||||
|
||||
exports.testAfterNameAsync = function (assert, done) {
|
||||
assert.equal(AFTER_RUN, 2, 'after function was called with name');
|
||||
BEFORE_RUN = 0;
|
||||
AFTER_RUN = 0;
|
||||
async(done)();
|
||||
};
|
||||
|
||||
exports.testSyncABefore = function (assert) {
|
||||
assert.equal(BEFORE_RUN, 1, 'before function was called for sync test');
|
||||
BEFORE_RUN = 0;
|
||||
AFTER_RUN = 0;
|
||||
};
|
||||
|
||||
exports.testSyncAfter = function (assert) {
|
||||
assert.equal(AFTER_RUN, 1, 'after function was called for sync test');
|
||||
BEFORE_RUN = 0;
|
||||
AFTER_RUN = 0;
|
||||
};
|
||||
|
||||
before(exports, (name) => {
|
||||
if (name === 'testABeforeNameAsync')
|
||||
BEFORE_RUN = 2;
|
||||
else
|
||||
BEFORE_RUN = 1;
|
||||
});
|
||||
|
||||
after(exports, (name) => {
|
||||
// testAfterName runs after testAfter, which is where this
|
||||
// check occurs in the assertation
|
||||
if (name === 'testAfterAsync')
|
||||
AFTER_RUN = 2;
|
||||
else
|
||||
AFTER_RUN = 1;
|
||||
});
|
||||
|
||||
require('sdk/test').run(exports);
|
24
addon-sdk/source/test/test-unsupported-skip.js
Normal file
24
addon-sdk/source/test/test-unsupported-skip.js
Normal file
@ -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 app = require('sdk/system/xul-app');
|
||||
|
||||
/*
|
||||
* Include a module that is unsupported for the current system.
|
||||
* The `Unsupported Application` error should be caught by the test loader
|
||||
* and no errors should occur
|
||||
*/
|
||||
if (!app.is('Firefox')) {
|
||||
require('./fixtures/loader/unsupported/firefox');
|
||||
} else {
|
||||
require('./fixtures/loader/unsupported/fennec');
|
||||
}
|
||||
|
||||
exports.testRunning = function (assert) {
|
||||
assert.fail('Tests should not run in unsupported applications');
|
||||
};
|
||||
|
||||
require('test').run(exports);
|
@ -1,313 +1,303 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.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 url = require("sdk/url");
|
||||
var { Loader } = require("sdk/test/loader");
|
||||
var { pathFor } = require("sdk/system");
|
||||
var file = require("sdk/io/file");
|
||||
var loader = Loader(module);
|
||||
var httpd = loader.require("sdk/test/httpd");
|
||||
var port = 8099;
|
||||
var tabs = require("sdk/tabs");
|
||||
const { URL, toFilename, fromFilename, isValidURI, getTLD, DataURL } = require('sdk/url');
|
||||
const { pathFor } = require('sdk/system');
|
||||
const file = require('sdk/io/file');
|
||||
const tabs = require('sdk/tabs');
|
||||
const { decode } = require('sdk/base64');
|
||||
|
||||
exports.testResolve = function(test) {
|
||||
test.assertEqual(url.URL("bar", "http://www.foo.com/").toString(),
|
||||
"http://www.foo.com/bar");
|
||||
const httpd = require('sdk/test/httpd');
|
||||
const port = 8099;
|
||||
|
||||
test.assertEqual(url.URL("bar", "http://www.foo.com"),
|
||||
"http://www.foo.com/bar");
|
||||
const defaultLocation = '{\'scheme\':\'about\',\'userPass\':null,\'host\':null,\'hostname\':null,\'port\':null,\'path\':\'addons\',\'pathname\':\'addons\',\'hash\':\'\',\'href\':\'about:addons\',\'origin\':\'about:\',\'protocol\':\'about:\',\'search\':\'\'}'.replace(/'/g, '"');
|
||||
|
||||
test.assertEqual(url.URL("http://bar.com/", "http://foo.com/"),
|
||||
"http://bar.com/",
|
||||
"relative should override base");
|
||||
exports.testResolve = function(assert) {
|
||||
assert.equal(URL('bar', 'http://www.foo.com/').toString(),
|
||||
'http://www.foo.com/bar');
|
||||
|
||||
test.assertRaises(function() { url.URL("blah"); },
|
||||
"malformed URI: blah",
|
||||
"url.resolve() should throw malformed URI on base");
|
||||
assert.equal(URL('bar', 'http://www.foo.com'),
|
||||
'http://www.foo.com/bar');
|
||||
|
||||
test.assertRaises(function() { url.URL("chrome://global"); },
|
||||
"invalid URI: chrome://global",
|
||||
"url.resolve() should throw invalid URI on base");
|
||||
assert.equal(URL('http://bar.com/', 'http://foo.com/'),
|
||||
'http://bar.com/',
|
||||
'relative should override base');
|
||||
|
||||
test.assertRaises(function() { url.URL("chrome://foo/bar"); },
|
||||
"invalid URI: chrome://foo/bar",
|
||||
"url.resolve() should throw on bad chrome URI");
|
||||
assert.throws(function() { URL('blah'); },
|
||||
/malformed URI: blah/i,
|
||||
'url.resolve() should throw malformed URI on base');
|
||||
|
||||
test.assertEqual(url.URL("", "http://www.foo.com"),
|
||||
"http://www.foo.com/",
|
||||
"url.resolve() should add slash to end of domain");
|
||||
assert.throws(function() { URL('chrome://global'); },
|
||||
/invalid URI: chrome:\/\/global/i,
|
||||
'url.resolve() should throw invalid URI on base');
|
||||
|
||||
assert.throws(function() { URL('chrome://foo/bar'); },
|
||||
/invalid URI: chrome:\/\/foo\/bar/i,
|
||||
'url.resolve() should throw on bad chrome URI');
|
||||
|
||||
assert.equal(URL('', 'http://www.foo.com'),
|
||||
'http://www.foo.com/',
|
||||
'url.resolve() should add slash to end of domain');
|
||||
};
|
||||
|
||||
exports.testParseHttp = function(test) {
|
||||
var aUrl = "http://sub.foo.com/bar?locale=en-US&otherArg=%20x%20#myhash";
|
||||
var info = url.URL(aUrl);
|
||||
test.assertEqual(info.scheme, "http");
|
||||
test.assertEqual(info.protocol, "http:");
|
||||
test.assertEqual(info.host, "sub.foo.com");
|
||||
test.assertEqual(info.hostname, "sub.foo.com");
|
||||
test.assertEqual(info.port, null);
|
||||
test.assertEqual(info.userPass, null);
|
||||
test.assertEqual(info.path, "/bar?locale=en-US&otherArg=%20x%20#myhash");
|
||||
test.assertEqual(info.pathname, "/bar");
|
||||
test.assertEqual(info.href, aUrl);
|
||||
test.assertEqual(info.hash, "#myhash");
|
||||
test.assertEqual(info.search, "?locale=en-US&otherArg=%20x%20");
|
||||
exports.testParseHttp = function(assert) {
|
||||
var aUrl = 'http://sub.foo.com/bar?locale=en-US&otherArg=%20x%20#myhash';
|
||||
var info = URL(aUrl);
|
||||
|
||||
assert.equal(info.scheme, 'http');
|
||||
assert.equal(info.protocol, 'http:');
|
||||
assert.equal(info.host, 'sub.foo.com');
|
||||
assert.equal(info.hostname, 'sub.foo.com');
|
||||
assert.equal(info.port, null);
|
||||
assert.equal(info.userPass, null);
|
||||
assert.equal(info.path, '/bar?locale=en-US&otherArg=%20x%20#myhash');
|
||||
assert.equal(info.pathname, '/bar');
|
||||
assert.equal(info.href, aUrl);
|
||||
assert.equal(info.hash, '#myhash');
|
||||
assert.equal(info.search, '?locale=en-US&otherArg=%20x%20');
|
||||
};
|
||||
|
||||
exports.testParseHttpSearchAndHash = function (test) {
|
||||
var info = url.URL("https://www.moz.com/some/page.html");
|
||||
test.assertEqual(info.hash, "");
|
||||
test.assertEqual(info.search, "");
|
||||
exports.testParseHttpSearchAndHash = function (assert) {
|
||||
var info = URL('https://www.moz.com/some/page.html');
|
||||
assert.equal(info.hash, '');
|
||||
assert.equal(info.search, '');
|
||||
|
||||
var hashOnly = url.URL("https://www.sub.moz.com/page.html#justhash");
|
||||
test.assertEqual(hashOnly.search, "");
|
||||
test.assertEqual(hashOnly.hash, "#justhash");
|
||||
var hashOnly = URL('https://www.sub.moz.com/page.html#justhash');
|
||||
assert.equal(hashOnly.search, '');
|
||||
assert.equal(hashOnly.hash, '#justhash');
|
||||
|
||||
var queryOnly = url.URL("https://www.sub.moz.com/page.html?my=query");
|
||||
test.assertEqual(queryOnly.search, "?my=query");
|
||||
test.assertEqual(queryOnly.hash, "");
|
||||
var queryOnly = URL('https://www.sub.moz.com/page.html?my=query');
|
||||
assert.equal(queryOnly.search, '?my=query');
|
||||
assert.equal(queryOnly.hash, '');
|
||||
|
||||
var qMark = url.URL("http://www.moz.org?");
|
||||
test.assertEqual(qMark.search, "");
|
||||
test.assertEqual(qMark.hash, "");
|
||||
var qMark = URL('http://www.moz.org?');
|
||||
assert.equal(qMark.search, '');
|
||||
assert.equal(qMark.hash, '');
|
||||
|
||||
var hash = url.URL("http://www.moz.org#");
|
||||
test.assertEqual(hash.search, "");
|
||||
test.assertEqual(hash.hash, "");
|
||||
var hash = URL('http://www.moz.org#');
|
||||
assert.equal(hash.search, '');
|
||||
assert.equal(hash.hash, '');
|
||||
|
||||
var empty = url.URL("http://www.moz.org?#");
|
||||
test.assertEqual(hash.search, "");
|
||||
test.assertEqual(hash.hash, "");
|
||||
var empty = URL('http://www.moz.org?#');
|
||||
assert.equal(hash.search, '');
|
||||
assert.equal(hash.hash, '');
|
||||
|
||||
var strange = url.URL("http://moz.org?test1#test2?test3");
|
||||
test.assertEqual(strange.search, "?test1");
|
||||
test.assertEqual(strange.hash, "#test2?test3");
|
||||
var strange = URL('http://moz.org?test1#test2?test3');
|
||||
assert.equal(strange.search, '?test1');
|
||||
assert.equal(strange.hash, '#test2?test3');
|
||||
};
|
||||
|
||||
exports.testParseHttpWithPort = function(test) {
|
||||
var info = url.URL("http://foo.com:5/bar");
|
||||
test.assertEqual(info.port, 5);
|
||||
exports.testParseHttpWithPort = function(assert) {
|
||||
var info = URL('http://foo.com:5/bar');
|
||||
assert.equal(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.testParseChrome = function(assert) {
|
||||
var info = URL('chrome://global/content/blah');
|
||||
assert.equal(info.scheme, 'chrome');
|
||||
assert.equal(info.host, 'global');
|
||||
assert.equal(info.port, null);
|
||||
assert.equal(info.userPass, null);
|
||||
assert.equal(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.testParseAbout = function(assert) {
|
||||
var info = URL('about:boop');
|
||||
assert.equal(info.scheme, 'about');
|
||||
assert.equal(info.host, null);
|
||||
assert.equal(info.port, null);
|
||||
assert.equal(info.userPass, null);
|
||||
assert.equal(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.testParseFTP = function(assert) {
|
||||
var info = URL('ftp://1.2.3.4/foo');
|
||||
assert.equal(info.scheme, 'ftp');
|
||||
assert.equal(info.host, '1.2.3.4');
|
||||
assert.equal(info.port, null);
|
||||
assert.equal(info.userPass, null);
|
||||
assert.equal(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");
|
||||
exports.testParseFTPWithUserPass = function(assert) {
|
||||
var info = URL('ftp://user:pass@1.2.3.4/foo');
|
||||
assert.equal(info.userPass, 'user:pass');
|
||||
};
|
||||
|
||||
exports.testToFilename = function(test) {
|
||||
test.assertRaises(
|
||||
function() { url.toFilename("resource://nonexistent"); },
|
||||
"resource does not exist: resource://nonexistent/",
|
||||
"url.toFilename() on nonexistent resources should throw"
|
||||
exports.testToFilename = function(assert) {
|
||||
assert.throws(
|
||||
function() { toFilename('resource://nonexistent'); },
|
||||
/resource does not exist: resource:\/\/nonexistent\//i,
|
||||
'toFilename() on nonexistent resources should throw'
|
||||
);
|
||||
|
||||
test.assertRaises(
|
||||
function() { url.toFilename("http://foo.com/"); },
|
||||
"cannot map to filename: http://foo.com/",
|
||||
"url.toFilename() on http: URIs should raise error"
|
||||
assert.throws(
|
||||
function() { toFilename('http://foo.com/'); },
|
||||
/cannot map to filename: http:\/\/foo.com\//i,
|
||||
'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"
|
||||
assert.ok(
|
||||
/.*console\.xul$/.test(toFilename('chrome://global/content/console.xul')),
|
||||
'toFilename() w/ console.xul works when it maps to filesystem'
|
||||
);
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
if (/chrome url isn\'t on filesystem/.test(e.message))
|
||||
test.pass("accessing console.xul in jar raises exception");
|
||||
assert.pass('accessing console.xul in jar raises exception');
|
||||
else
|
||||
test.fail("accessing console.xul raises " + e);
|
||||
assert.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$/);
|
||||
// assert.ok(/.*main\.js$/.test(toFilename('chrome://myapp/content/main.js')));
|
||||
};
|
||||
|
||||
exports.testFromFilename = function(test) {
|
||||
var profileDirName = require("sdk/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.testFromFilename = function(assert) {
|
||||
var profileDirName = require('sdk/system').pathFor('ProfD');
|
||||
var fileUrl = fromFilename(profileDirName);
|
||||
assert.equal(URL(fileUrl).scheme, 'file',
|
||||
'toFilename() should return a file: url');
|
||||
assert.equal(fromFilename(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");
|
||||
exports.testURL = function(assert) {
|
||||
assert.ok(URL('h:foo') instanceof URL, 'instance is of correct type');
|
||||
assert.throws(function() URL(),
|
||||
/malformed URI: undefined/i,
|
||||
'url.URL should throw on undefined');
|
||||
assert.throws(function() URL(''),
|
||||
/malformed URI: /i,
|
||||
'url.URL should throw on empty string');
|
||||
assert.throws(function() URL('foo'),
|
||||
/malformed URI: foo/i,
|
||||
'url.URL should throw on invalid URI');
|
||||
assert.ok(URL('h:foo').scheme, 'has scheme');
|
||||
assert.equal(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");
|
||||
assert.equal(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");
|
||||
assert.throws(function() URL('path').toString(),
|
||||
/malformed URI: path/i,
|
||||
'no base for relative URI should throw');
|
||||
|
||||
let a = URL("h:foo");
|
||||
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");
|
||||
assert.equal(b.toString(),
|
||||
'h:foo',
|
||||
'a URL can be initialized from another URL');
|
||||
assert.notStrictEqual(a, b,
|
||||
'a URL initialized from another URL is not the same object');
|
||||
assert.ok(a == 'h:foo',
|
||||
'toString is implicit when a URL is compared to a string via ==');
|
||||
assert.strictEqual(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";
|
||||
exports.testStringInterface = function(assert) {
|
||||
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,hostname,port,path,pathname,hash,href,origin,protocol,search",
|
||||
"enumerable key list check for URL.");
|
||||
test.assertEqual(
|
||||
assert.equal(Object.keys(a),
|
||||
'scheme,userPass,host,hostname,port,path,pathname,hash,href,origin,protocol,search',
|
||||
'enumerable key list check for URL.');
|
||||
assert.equal(
|
||||
JSON.stringify(a),
|
||||
"{\"scheme\":\"about\",\"userPass\":null,\"host\":null,\"hostname\":null,\"port\":null,\"path\":\"addons\",\"pathname\":\"addons\",\"hash\":\"\",\"href\":\"about:addons\",\"origin\":\"about:\",\"protocol\":\"about:\",\"search\":\"\"}",
|
||||
"JSON.stringify should return a object with correct props and vals.");
|
||||
defaultLocation,
|
||||
'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.");
|
||||
assert.equal(a.indexOf(':'), EM.indexOf(':'), 'indexOf on URL works');
|
||||
assert.equal(a.valueOf(), EM.valueOf(), 'valueOf on URL works.');
|
||||
assert.equal(a.toSource(), EM.toSource(), 'toSource on URL works.');
|
||||
assert.equal(a.lastIndexOf('a'), EM.lastIndexOf('a'), 'lastIndexOf on URL works.');
|
||||
assert.equal(a.match('t:').toString(), EM.match('t:').toString(), 'match on URL works.');
|
||||
assert.equal(a.toUpperCase(), EM.toUpperCase(), 'toUpperCase on URL works.');
|
||||
assert.equal(a.toLowerCase(), EM.toLowerCase(), 'toLowerCase on URL works.');
|
||||
assert.equal(a.split(':').toString(), EM.split(':').toString(), 'split on URL works.');
|
||||
assert.equal(a.charAt(2), EM.charAt(2), 'charAt on URL works.');
|
||||
assert.equal(a.charCodeAt(2), EM.charCodeAt(2), 'charCodeAt on URL works.');
|
||||
assert.equal(a.concat(EM), EM.concat(a), 'concat on URL works.');
|
||||
assert.equal(a.substr(2,3), EM.substr(2,3), 'substr on URL works.');
|
||||
assert.equal(a.substring(2,3), EM.substring(2,3), 'substring on URL works.');
|
||||
assert.equal(a.trim(), EM.trim(), 'trim on URL works.');
|
||||
assert.equal(a.trimRight(), EM.trimRight(), 'trimRight on URL works.');
|
||||
assert.equal(a.trimLeft(), EM.trimLeft(), 'trimLeft on URL works.');
|
||||
}
|
||||
|
||||
exports.testDataURLwithouthURI = function (test) {
|
||||
const { DataURL } = url;
|
||||
|
||||
exports.testDataURLwithouthURI = function (assert) {
|
||||
let dataURL = new DataURL();
|
||||
|
||||
test.assertEqual(dataURL.base64, false, "base64 is false for empty uri")
|
||||
test.assertEqual(dataURL.data, "", "data is an empty string for empty uri")
|
||||
test.assertEqual(dataURL.mimeType, "", "mimeType is an empty string for empty uri")
|
||||
test.assertEqual(Object.keys(dataURL.parameters).length, 0, "parameters is an empty object for empty uri");
|
||||
assert.equal(dataURL.base64, false, 'base64 is false for empty uri')
|
||||
assert.equal(dataURL.data, '', 'data is an empty string for empty uri')
|
||||
assert.equal(dataURL.mimeType, '', 'mimeType is an empty string for empty uri')
|
||||
assert.equal(Object.keys(dataURL.parameters).length, 0, 'parameters is an empty object for empty uri');
|
||||
|
||||
test.assertEqual(dataURL.toString(), "data:,");
|
||||
assert.equal(dataURL.toString(), 'data:,');
|
||||
}
|
||||
|
||||
exports.testDataURLwithMalformedURI = function (test) {
|
||||
const { DataURL } = url;
|
||||
|
||||
test.assertRaises(function() {
|
||||
let dataURL = new DataURL("http://www.mozilla.com/");
|
||||
exports.testDataURLwithMalformedURI = function (assert) {
|
||||
assert.throws(function() {
|
||||
let dataURL = new DataURL('http://www.mozilla.com/');
|
||||
},
|
||||
"Malformed Data URL: http://www.mozilla.com/",
|
||||
"DataURL raises an exception for malformed data uri"
|
||||
/Malformed Data URL: http:\/\/www.mozilla.com\//i,
|
||||
'DataURL raises an exception for malformed data uri'
|
||||
);
|
||||
}
|
||||
|
||||
exports.testDataURLparse = function (test) {
|
||||
const { DataURL } = url;
|
||||
exports.testDataURLparse = function (assert) {
|
||||
let dataURL = new DataURL('data:text/html;charset=US-ASCII,%3Ch1%3EHello!%3C%2Fh1%3E');
|
||||
|
||||
let dataURL = new DataURL("data:text/html;charset=US-ASCII,%3Ch1%3EHello!%3C%2Fh1%3E");
|
||||
assert.equal(dataURL.base64, false, 'base64 is false for non base64 data uri')
|
||||
assert.equal(dataURL.data, '<h1>Hello!</h1>', 'data is properly decoded')
|
||||
assert.equal(dataURL.mimeType, 'text/html', 'mimeType is set properly')
|
||||
assert.equal(Object.keys(dataURL.parameters).length, 1, 'one parameters specified');
|
||||
assert.equal(dataURL.parameters['charset'], 'US-ASCII', 'charset parsed');
|
||||
|
||||
test.assertEqual(dataURL.base64, false, "base64 is false for non base64 data uri")
|
||||
test.assertEqual(dataURL.data, "<h1>Hello!</h1>", "data is properly decoded")
|
||||
test.assertEqual(dataURL.mimeType, "text/html", "mimeType is set properly")
|
||||
test.assertEqual(Object.keys(dataURL.parameters).length, 1, "one parameters specified");
|
||||
test.assertEqual(dataURL.parameters["charset"], "US-ASCII", "charset parsed");
|
||||
|
||||
test.assertEqual(dataURL.toString(), "data:text/html;charset=US-ASCII,%3Ch1%3EHello!%3C%2Fh1%3E");
|
||||
assert.equal(dataURL.toString(), 'data:text/html;charset=US-ASCII,%3Ch1%3EHello!%3C%2Fh1%3E');
|
||||
}
|
||||
|
||||
exports.testDataURLparseBase64 = function (test) {
|
||||
const { DataURL } = url;
|
||||
const { decode } = require("sdk/base64");
|
||||
exports.testDataURLparseBase64 = function (assert) {
|
||||
let text = 'Awesome!';
|
||||
let b64text = 'QXdlc29tZSE=';
|
||||
let dataURL = new DataURL('data:text/plain;base64,' + b64text);
|
||||
|
||||
let text = "Awesome!";
|
||||
let b64text = "QXdlc29tZSE=";
|
||||
let dataURL = new DataURL("data:text/plain;base64," + b64text);
|
||||
|
||||
test.assertEqual(dataURL.base64, true, "base64 is true for base64 encoded data uri")
|
||||
test.assertEqual(dataURL.data, text, "data is properly decoded")
|
||||
test.assertEqual(dataURL.mimeType, "text/plain", "mimeType is set properly")
|
||||
test.assertEqual(Object.keys(dataURL.parameters).length, 1, "one parameters specified");
|
||||
test.assertEqual(dataURL.parameters["base64"], "", "parameter set without value");
|
||||
|
||||
test.assertEqual(dataURL.toString(), "data:text/plain;base64," + encodeURIComponent(b64text));
|
||||
assert.equal(dataURL.base64, true, 'base64 is true for base64 encoded data uri')
|
||||
assert.equal(dataURL.data, text, 'data is properly decoded')
|
||||
assert.equal(dataURL.mimeType, 'text/plain', 'mimeType is set properly')
|
||||
assert.equal(Object.keys(dataURL.parameters).length, 1, 'one parameters specified');
|
||||
assert.equal(dataURL.parameters['base64'], '', 'parameter set without value');
|
||||
assert.equal(dataURL.toString(), 'data:text/plain;base64,' + encodeURIComponent(b64text));
|
||||
}
|
||||
|
||||
exports.testIsValidURI = function (test) {
|
||||
exports.testIsValidURI = function (assert) {
|
||||
validURIs().forEach(function (aUri) {
|
||||
test.assertEqual(url.isValidURI(aUri), true, aUri + ' is a valid URL');
|
||||
assert.equal(isValidURI(aUri), true, aUri + ' is a valid URL');
|
||||
});
|
||||
};
|
||||
|
||||
exports.testIsInvalidURI = function (test) {
|
||||
exports.testIsInvalidURI = function (assert) {
|
||||
invalidURIs().forEach(function (aUri) {
|
||||
test.assertEqual(url.isValidURI(aUri), false, aUri + ' is an invalid URL');
|
||||
assert.equal(isValidURI(aUri), false, aUri + ' is an invalid URL');
|
||||
});
|
||||
};
|
||||
|
||||
exports.testURLFromURL = function(test) {
|
||||
let aURL = url.URL('http://mozilla.org');
|
||||
let bURL = url.URL(aURL);
|
||||
test.assertEqual(aURL.toString(), bURL.toString(), 'Making a URL from a URL works');
|
||||
exports.testURLFromURL = function(assert) {
|
||||
let aURL = URL('http://mozilla.org');
|
||||
let bURL = URL(aURL);
|
||||
assert.equal(aURL.toString(), bURL.toString(), 'Making a URL from a URL works');
|
||||
};
|
||||
|
||||
exports.testTLD = function(test) {
|
||||
exports.testTLD = function(assert) {
|
||||
let urls = [
|
||||
{ url: 'http://my.sub.domains.mozilla.co.uk', tld: 'co.uk' },
|
||||
{ url: 'http://my.mozilla.com', tld: 'com' },
|
||||
@ -318,16 +308,19 @@ exports.testTLD = function(test) {
|
||||
];
|
||||
|
||||
urls.forEach(function (uri) {
|
||||
test.assertEqual(url.getTLD(uri.url), uri.tld);
|
||||
test.assertEqual(url.getTLD(url.URL(uri.url)), uri.tld);
|
||||
assert.equal(getTLD(uri.url), uri.tld);
|
||||
assert.equal(getTLD(URL(uri.url)), uri.tld);
|
||||
});
|
||||
}
|
||||
|
||||
exports.testWindowLocationMatch = function (test) {
|
||||
let srv = serve();
|
||||
exports.testWindowLocationMatch = function (assert, done) {
|
||||
let server = httpd.startServerAsync(port);
|
||||
server.registerPathHandler('/index.html', function (request, response) {
|
||||
response.write('<html><head></head><body><h1>url tests</h1></body></html>');
|
||||
});
|
||||
|
||||
let aUrl = 'http://localhost:' + port + '/index.html?q=aQuery#somehash';
|
||||
let urlObject = url.URL(aUrl);
|
||||
test.waitUntilDone();
|
||||
let urlObject = URL(aUrl);
|
||||
|
||||
tabs.open({
|
||||
url: aUrl,
|
||||
@ -335,10 +328,10 @@ exports.testWindowLocationMatch = function (test) {
|
||||
tab.attach({
|
||||
onMessage: function (loc) {
|
||||
for (let prop in loc) {
|
||||
test.assertEqual(urlObject[prop], loc[prop], prop + ' matches');
|
||||
assert.equal(urlObject[prop], loc[prop], prop + ' matches');
|
||||
}
|
||||
tab.close();
|
||||
srv.stop(test.done.bind(test));
|
||||
|
||||
tab.close(function() server.stop(done));
|
||||
},
|
||||
contentScript: '(' + function () {
|
||||
let res = {};
|
||||
@ -446,15 +439,4 @@ function invalidURIs () {
|
||||
];
|
||||
}
|
||||
|
||||
function serve () {
|
||||
let basePath = pathFor("ProfD");
|
||||
let filePath = file.join(basePath, 'index.html');
|
||||
let content = "<html><head></head><body><h1>url tests</h1></body></html>";
|
||||
let fileStream = file.open(filePath, 'w');
|
||||
fileStream.write(content);
|
||||
fileStream.close();
|
||||
|
||||
let srv = httpd.startServerAsync(port, basePath);
|
||||
return srv;
|
||||
}
|
||||
|
||||
require('sdk/test').run(exports);
|
||||
|
208
addon-sdk/source/test/test-weak-set.js
Normal file
208
addon-sdk/source/test/test-weak-set.js
Normal file
@ -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 { Cu } = require('chrome');
|
||||
const { Loader } = require('sdk/test/loader');
|
||||
const { defer } = require('sdk/core/promise');
|
||||
|
||||
function gc() {
|
||||
let { promise, resolve } = defer();
|
||||
|
||||
Cu.schedulePreciseGC(resolve);
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
exports['test adding item'] = function(assert, done) {
|
||||
let loader = Loader(module);
|
||||
let { add, remove, has, clear, iterator } = loader.require('sdk/lang/weak-set');
|
||||
|
||||
let items = {};
|
||||
let item = {};
|
||||
|
||||
add(items, item);
|
||||
|
||||
gc().
|
||||
then(() => {
|
||||
assert.ok(has(items, item),
|
||||
'the item is in the weak set');
|
||||
}).
|
||||
then(loader.unload).
|
||||
then(done, assert.fail);
|
||||
};
|
||||
|
||||
exports['test remove item'] = function(assert, done) {
|
||||
let loader = Loader(module);
|
||||
let { add, remove, has, clear, iterator } = loader.require('sdk/lang/weak-set');
|
||||
|
||||
let items = {};
|
||||
let item = {};
|
||||
|
||||
add(items, item);
|
||||
|
||||
remove(items, item);
|
||||
|
||||
gc().
|
||||
then(() => {
|
||||
assert.ok(!has(items, item),
|
||||
'the item is not in weak set');
|
||||
}).
|
||||
then(loader.unload).
|
||||
then(done, assert.fail);
|
||||
};
|
||||
|
||||
exports['test iterate'] = function(assert, done) {
|
||||
let loader = Loader(module);
|
||||
let { add, remove, has, clear, iterator } = loader.require('sdk/lang/weak-set');
|
||||
|
||||
let items = {};
|
||||
let addedItems = [{}, {}];
|
||||
|
||||
add(items, addedItems[0]);
|
||||
add(items, addedItems[1]);
|
||||
add(items, addedItems[0]); // weak set shouldn't add this twice
|
||||
|
||||
gc().
|
||||
then(() => {
|
||||
let count = 0;
|
||||
|
||||
for (let item of iterator(items)) {
|
||||
assert.equal(item, addedItems[count],
|
||||
'item in the expected order');
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
assert.equal(count, 2,
|
||||
'items in the expected number');
|
||||
}).
|
||||
then(loader.unload).
|
||||
then(done, assert.fail);
|
||||
};
|
||||
|
||||
exports['test clear'] = function(assert, done) {
|
||||
let loader = Loader(module);
|
||||
let { add, remove, has, clear, iterator } = loader.require('sdk/lang/weak-set');
|
||||
|
||||
let items = {};
|
||||
let addedItems = [{}, {}];
|
||||
|
||||
add(items, addedItems[0]);
|
||||
add(items, addedItems[1]);
|
||||
|
||||
clear(items)
|
||||
|
||||
gc().
|
||||
then(() => {
|
||||
let count = 0;
|
||||
|
||||
for (let item of iterator(items))
|
||||
assert.fail('the loop should not be executed');
|
||||
|
||||
assert.equal(count, 0,
|
||||
'no items in the weak set');
|
||||
}).
|
||||
then(loader.unload).
|
||||
then(done, assert.fail);
|
||||
};
|
||||
|
||||
exports['test adding item without reference'] = function(assert, done) {
|
||||
let loader = Loader(module);
|
||||
let { add, remove, has, clear, iterator } = loader.require('sdk/lang/weak-set');
|
||||
|
||||
let items = {};
|
||||
|
||||
add(items, {});
|
||||
|
||||
gc().
|
||||
then(() => {
|
||||
let count = 0;
|
||||
|
||||
for (let item of iterator(items))
|
||||
assert.fail('the loop should not be executed');
|
||||
|
||||
assert.equal(count, 0,
|
||||
'no items in the weak set');
|
||||
}).
|
||||
then(loader.unload).
|
||||
then(done, assert.fail);
|
||||
};
|
||||
|
||||
exports['test adding non object or null item'] = function(assert) {
|
||||
let loader = Loader(module);
|
||||
let { add, remove, has, clear, iterator } = loader.require('sdk/lang/weak-set');
|
||||
|
||||
let items = {};
|
||||
|
||||
assert.throws(() => {
|
||||
add(items, 'foo');
|
||||
},
|
||||
/^value is not a non-null object/,
|
||||
'only non-null object are allowed');
|
||||
|
||||
assert.throws(() => {
|
||||
add(items, 0);
|
||||
},
|
||||
/^value is not a non-null object/,
|
||||
'only non-null object are allowed');
|
||||
|
||||
assert.throws(() => {
|
||||
add(items, undefined);
|
||||
},
|
||||
/^value is not a non-null object/,
|
||||
'only non-null object are allowed');
|
||||
|
||||
assert.throws(() => {
|
||||
add(items, null);
|
||||
},
|
||||
/^value is not a non-null object/,
|
||||
'only non-null object are allowed');
|
||||
|
||||
assert.throws(() => {
|
||||
add(items, true);
|
||||
},
|
||||
/^value is not a non-null object/,
|
||||
'only non-null object are allowed');
|
||||
};
|
||||
|
||||
exports['test adding to non object or null item'] = function(assert) {
|
||||
let loader = Loader(module);
|
||||
let { add, remove, has, clear, iterator } = loader.require('sdk/lang/weak-set');
|
||||
|
||||
let item = {};
|
||||
|
||||
assert.throws(() => {
|
||||
add('foo', item);
|
||||
},
|
||||
/^value is not a non-null object/,
|
||||
'only non-null object are allowed');
|
||||
|
||||
assert.throws(() => {
|
||||
add(0, item);
|
||||
},
|
||||
/^value is not a non-null object/,
|
||||
'only non-null object are allowed');
|
||||
|
||||
assert.throws(() => {
|
||||
add(undefined, item);
|
||||
},
|
||||
/^value is not a non-null object/,
|
||||
'only non-null object are allowed');
|
||||
|
||||
assert.throws(() => {
|
||||
add(null, item);
|
||||
},
|
||||
/^value is not a non-null object/,
|
||||
'only non-null object are allowed');
|
||||
|
||||
assert.throws(() => {
|
||||
add(true, item);
|
||||
},
|
||||
/^value is not a non-null object/,
|
||||
'only non-null object are allowed');
|
||||
};
|
||||
|
||||
require('test').run(exports);
|
@ -1,7 +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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { Cc, Ci } = require("chrome");
|
||||
@ -10,18 +9,37 @@ const url = require("sdk/url");
|
||||
const timer = require("sdk/timers");
|
||||
const self = require("sdk/self");
|
||||
const windowUtils = require("sdk/deprecated/window-utils");
|
||||
const { getMostRecentBrowserWindow } = require('sdk/window/utils');
|
||||
|
||||
let jetpackID = "testID";
|
||||
try {
|
||||
jetpackID = require("sdk/self").id;
|
||||
} catch(e) {}
|
||||
|
||||
const australis = !!require("sdk/window/utils").getMostRecentBrowserWindow().CustomizableUI;
|
||||
|
||||
exports.testConstructor = function(test) {
|
||||
test.waitUntilDone();
|
||||
|
||||
let browserWindow = windowUtils.activeBrowserWindow;
|
||||
let doc = browserWindow.document;
|
||||
let AddonsMgrListener = browserWindow.AddonsMgrListener;
|
||||
let AddonsMgrListener;
|
||||
if (australis) {
|
||||
AddonsMgrListener = {
|
||||
onInstalling: () => {},
|
||||
onInstalled: () => {},
|
||||
onUninstalling: () => {},
|
||||
onUninstalled: () => {}
|
||||
};
|
||||
} else {
|
||||
AddonsMgrListener = browserWindow.AddonsMgrListener;
|
||||
}
|
||||
|
||||
function container() doc.getElementById("addon-bar");
|
||||
function widgetCount() container() ? container().getElementsByTagName("toolbaritem").length : 0;
|
||||
function container() australis ? doc.getElementById("nav-bar") : doc.getElementById("addon-bar");
|
||||
function getWidgets() container() ? container().querySelectorAll('[id^="widget\:"]') : [];
|
||||
function widgetCount() getWidgets().length;
|
||||
let widgetStartCount = widgetCount();
|
||||
function widgetNode(index) container() ? container().getElementsByTagName("toolbaritem")[index] : null;
|
||||
function widgetNode(index) getWidgets()[index];
|
||||
|
||||
// Test basic construct/destroy
|
||||
AddonsMgrListener.onInstalling();
|
||||
@ -129,29 +147,30 @@ exports.testConstructor = function(test) {
|
||||
AddonsMgrListener.onUninstalled();
|
||||
|
||||
// Test concurrent widget module instances on addon-bar hiding
|
||||
let loader = Loader(module);
|
||||
let anotherWidgetsInstance = loader.require("sdk/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");
|
||||
|
||||
if (!australis) {
|
||||
let loader = Loader(module);
|
||||
let anotherWidgetsInstance = loader.require("sdk/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) {
|
||||
@ -490,8 +509,8 @@ exports.testConstructor = function(test) {
|
||||
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;
|
||||
function container() australis ? doc.getElementById("nav-bar") : doc.getElementById("addon-bar");
|
||||
function widgetCount2() container() ? container().querySelectorAll('[id^="widget\:"]').length : 0;
|
||||
let widgetStartCount2 = widgetCount2();
|
||||
|
||||
let w1Opts = {id:"first", label: "first widget", content: "first content"};
|
||||
@ -570,45 +589,47 @@ exports.testConstructor = function(test) {
|
||||
});
|
||||
});
|
||||
|
||||
tests.push(function testAddonBarHide() {
|
||||
const tabBrowser = require("sdk/deprecated/tab-browser");
|
||||
if (!australis) {
|
||||
tests.push(function testAddonBarHide() {
|
||||
const tabBrowser = require("sdk/deprecated/tab-browser");
|
||||
|
||||
// Hide the addon-bar
|
||||
browserWindow.setToolbarVisibility(container(), false);
|
||||
test.assert(container().collapsed,
|
||||
"1st window starts with an hidden addon-bar");
|
||||
// Hide the addon-bar
|
||||
browserWindow.setToolbarVisibility(container(), false);
|
||||
test.assert(container().collapsed,
|
||||
"1st window starts with an hidden addon-bar");
|
||||
|
||||
// Then open a browser window and verify that the addon-bar remains hidden
|
||||
tabBrowser.addTab("about:blank", { inNewWindow: true, onLoad: function(e) {
|
||||
let browserWindow2 = e.target.defaultView;
|
||||
let doc2 = browserWindow2.document;
|
||||
function container2() doc2.getElementById("addon-bar");
|
||||
function widgetCount2() container2() ? container2().childNodes.length : 0;
|
||||
let widgetStartCount2 = widgetCount2();
|
||||
test.assert(container2().collapsed,
|
||||
"2nd window starts with an hidden addon-bar");
|
||||
// Then open a browser window and verify that the addon-bar remains hidden
|
||||
tabBrowser.addTab("about:blank", { inNewWindow: true, onLoad: function(e) {
|
||||
let browserWindow2 = e.target.defaultView;
|
||||
let doc2 = browserWindow2.document;
|
||||
function container2() doc2.getElementById("addon-bar");
|
||||
function widgetCount2() container2() ? container2().childNodes.length : 0;
|
||||
let widgetStartCount2 = widgetCount2();
|
||||
test.assert(container2().collapsed,
|
||||
"2nd window starts with an hidden addon-bar");
|
||||
|
||||
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");
|
||||
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");
|
||||
test.assert(container().collapsed, "1st window has an hidden addon-bar");
|
||||
test.assert(container2().collapsed, "2nd window has an hidden addon-bar");
|
||||
|
||||
// Reset addon-bar visibility before exiting this test
|
||||
browserWindow.setToolbarVisibility(container(), true);
|
||||
// Reset addon-bar visibility before exiting this test
|
||||
browserWindow.setToolbarVisibility(container(), true);
|
||||
|
||||
closeBrowserWindow(browserWindow2, function() {
|
||||
doneTest();
|
||||
});
|
||||
}});
|
||||
});
|
||||
closeBrowserWindow(browserWindow2, function() {
|
||||
doneTest();
|
||||
});
|
||||
}});
|
||||
});
|
||||
}
|
||||
|
||||
// test widget.width
|
||||
tests.push(function testWidgetWidth() testSingleWidget({
|
||||
@ -664,7 +685,7 @@ exports.testConstructor = function(test) {
|
||||
doneTest();
|
||||
};
|
||||
|
||||
exports.testPanelWidget1 = function testPanelWidget1(test) {
|
||||
exports.testWidgetWithValidPanel = function(test) {
|
||||
const widgets = require("sdk/widget");
|
||||
|
||||
let widget1 = widgets.Widget({
|
||||
@ -677,6 +698,13 @@ exports.testPanelWidget1 = function testPanelWidget1(test) {
|
||||
panel: require("sdk/panel").Panel({
|
||||
contentURL: "data:text/html;charset=utf-8,<body>Look ma, a panel!</body>",
|
||||
onShow: function() {
|
||||
let { document } = getMostRecentBrowserWindow();
|
||||
let widgetEle = document.getElementById("widget:" + jetpackID + "-" + widget1.id);
|
||||
let panelEle = document.getElementById('mainPopupSet').lastChild;
|
||||
// See bug https://bugzilla.mozilla.org/show_bug.cgi?id=859592
|
||||
test.assertEqual(panelEle.getAttribute("type"), "arrow", 'the panel is a arrow type');
|
||||
test.assertStrictEqual(panelEle.anchorNode, widgetEle, 'the panel is properly anchored to the widget');
|
||||
|
||||
widget1.destroy();
|
||||
test.pass("panel displayed on click");
|
||||
test.done();
|
||||
@ -686,7 +714,7 @@ exports.testPanelWidget1 = function testPanelWidget1(test) {
|
||||
test.waitUntilDone();
|
||||
};
|
||||
|
||||
exports.testPanelWidget2 = function testPanelWidget2(test) {
|
||||
exports.testWidgetWithInvalidPanel = function(test) {
|
||||
const widgets = require("sdk/widget");
|
||||
test.assertRaises(
|
||||
function() {
|
||||
@ -1043,94 +1071,96 @@ exports.testSVGWidget = function(test) {
|
||||
});
|
||||
};
|
||||
|
||||
exports.testNavigationBarWidgets = function testNavigationBarWidgets(test) {
|
||||
test.waitUntilDone();
|
||||
if (!australis) {
|
||||
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"});
|
||||
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"});
|
||||
|
||||
// First wait for all 3 widgets to be added to the current browser window
|
||||
let firstAttachCount = 0;
|
||||
function onAttachFirstWindow(widget) {
|
||||
if (++firstAttachCount<3)
|
||||
return;
|
||||
onWidgetsReady();
|
||||
}
|
||||
w1.once("attach", onAttachFirstWindow);
|
||||
w2.once("attach", onAttachFirstWindow);
|
||||
w3.once("attach", onAttachFirstWindow);
|
||||
|
||||
function getWidgetNode(toolbar, position) {
|
||||
return toolbar.getElementsByTagName("toolbaritem")[position];
|
||||
}
|
||||
function openBrowserWindow() {
|
||||
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 = "about:blank";
|
||||
return ww.openWindow(null, "chrome://browser/content/browser.xul",
|
||||
"_blank", "chrome,all,dialog=no", urlString);
|
||||
}
|
||||
|
||||
// Then move them before openeing a new browser window
|
||||
function onWidgetsReady() {
|
||||
// Hack to move 2nd and 3rd widgets manually to the navigation bar right after
|
||||
// the search box.
|
||||
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");
|
||||
let searchBox = doc.getElementById("search-container");
|
||||
// Insert 3rd at the right of search box by adding it before its right sibling
|
||||
navBar.insertItem(w3ToolbarItem.id, searchBox.nextSibling, null, false);
|
||||
// Then insert 2nd before 3rd
|
||||
navBar.insertItem(w2ToolbarItem.id, w3ToolbarItem, 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");
|
||||
|
||||
// Wait for all widget to be attached to this new window before checking
|
||||
// their position
|
||||
let attachCount = 0;
|
||||
let browserWindow2;
|
||||
function onAttach(widget) {
|
||||
if (++attachCount < 3)
|
||||
// First wait for all 3 widgets to be added to the current browser window
|
||||
let firstAttachCount = 0;
|
||||
function onAttachFirstWindow(widget) {
|
||||
if (++firstAttachCount<3)
|
||||
return;
|
||||
let doc = browserWindow2.document;
|
||||
let addonBar = doc.getElementById("addon-bar");
|
||||
let searchBox = doc.getElementById("search-container");
|
||||
|
||||
// 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,
|
||||
// i.e. right after search box
|
||||
test.assertEqual(searchBox.nextSibling.getAttribute("label"), w2.label);
|
||||
test.assertEqual(searchBox.nextSibling.nextSibling.getAttribute("label"), w3.label);
|
||||
|
||||
w1.destroy();
|
||||
w2.destroy();
|
||||
w3.destroy();
|
||||
|
||||
closeBrowserWindow(browserWindow2, function() {
|
||||
test.done();
|
||||
});
|
||||
onWidgetsReady();
|
||||
}
|
||||
w1.on("attach", onAttach);
|
||||
w2.on("attach", onAttach);
|
||||
w3.on("attach", onAttach);
|
||||
w1.once("attach", onAttachFirstWindow);
|
||||
w2.once("attach", onAttachFirstWindow);
|
||||
w3.once("attach", onAttachFirstWindow);
|
||||
|
||||
browserWindow2 = openBrowserWindow(browserWindow);
|
||||
}
|
||||
};
|
||||
function getWidgetNode(toolbar, position) {
|
||||
return toolbar.getElementsByTagName("toolbaritem")[position];
|
||||
}
|
||||
function openBrowserWindow() {
|
||||
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 = "about:blank";
|
||||
return ww.openWindow(null, "chrome://browser/content/browser.xul",
|
||||
"_blank", "chrome,all,dialog=no", urlString);
|
||||
}
|
||||
|
||||
// Then move them before openeing a new browser window
|
||||
function onWidgetsReady() {
|
||||
// Hack to move 2nd and 3rd widgets manually to the navigation bar right after
|
||||
// the search box.
|
||||
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");
|
||||
let searchBox = doc.getElementById("search-container");
|
||||
// Insert 3rd at the right of search box by adding it before its right sibling
|
||||
navBar.insertItem(w3ToolbarItem.id, searchBox.nextSibling, null, false);
|
||||
// Then insert 2nd before 3rd
|
||||
navBar.insertItem(w2ToolbarItem.id, w3ToolbarItem, 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");
|
||||
|
||||
// Wait for all widget to be attached to this new window before checking
|
||||
// their position
|
||||
let attachCount = 0;
|
||||
let browserWindow2;
|
||||
function onAttach(widget) {
|
||||
if (++attachCount < 3)
|
||||
return;
|
||||
let doc = browserWindow2.document;
|
||||
let addonBar = doc.getElementById("addon-bar");
|
||||
let searchBox = doc.getElementById("search-container");
|
||||
|
||||
// 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,
|
||||
// i.e. right after search box
|
||||
test.assertEqual(searchBox.nextSibling.getAttribute("label"), w2.label);
|
||||
test.assertEqual(searchBox.nextSibling.nextSibling.getAttribute("label"), w3.label);
|
||||
|
||||
w1.destroy();
|
||||
w2.destroy();
|
||||
w3.destroy();
|
||||
|
||||
closeBrowserWindow(browserWindow2, function() {
|
||||
test.done();
|
||||
});
|
||||
}
|
||||
w1.on("attach", onAttach);
|
||||
w2.on("attach", onAttach);
|
||||
w3.on("attach", onAttach);
|
||||
|
||||
browserWindow2 = openBrowserWindow(browserWindow);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/******************* helpers *********************/
|
||||
|
||||
|
@ -7,7 +7,7 @@ const { Loader } = require('sdk/test/loader');
|
||||
const { browserWindows } = require('sdk/windows');
|
||||
|
||||
// TEST: browserWindows Iterator
|
||||
exports.testBrowserWindowsIterator = function(test) {
|
||||
exports.testBrowserWindowsIterator = function(assert) {
|
||||
let activeWindowCount = 0;
|
||||
let windows = [];
|
||||
let i = 0;
|
||||
@ -15,45 +15,44 @@ exports.testBrowserWindowsIterator = function(test) {
|
||||
if (window === browserWindows.activeWindow)
|
||||
activeWindowCount++;
|
||||
|
||||
test.assertEqual(windows.indexOf(window), -1, 'window not already in iterator');
|
||||
test.assertEqual(browserWindows[i++], window, 'browserWindows[x] works');
|
||||
assert.equal(windows.indexOf(window), -1, 'window not already in iterator');
|
||||
assert.equal(browserWindows[i++], window, 'browserWindows[x] works');
|
||||
windows.push(window);
|
||||
}
|
||||
test.assertEqual(activeWindowCount, 1, 'activeWindow was found in the iterator');
|
||||
assert.equal(activeWindowCount, 1, 'activeWindow was found in the iterator');
|
||||
|
||||
i = 0;
|
||||
for (let j in browserWindows) {
|
||||
test.assertEqual(j, i++, 'for (x in browserWindows) works');
|
||||
assert.equal(j, i++, 'for (x in browserWindows) works');
|
||||
}
|
||||
};
|
||||
|
||||
exports.testWindowTabsObject_alt = function(test) {
|
||||
test.waitUntilDone();
|
||||
|
||||
exports.testWindowTabsObject_alt = function(assert, done) {
|
||||
let window = browserWindows.activeWindow;
|
||||
window.tabs.open({
|
||||
url: "data:text/html;charset=utf-8,<title>tab 2</title>",
|
||||
url: 'data:text/html;charset=utf-8,<title>tab 2</title>',
|
||||
inBackground: true,
|
||||
onReady: function onReady(tab) {
|
||||
test.assertEqual(tab.title, "tab 2", "Correct new tab title");
|
||||
test.assertNotEqual(window.tabs.activeTab, tab, "Correct active tab");
|
||||
assert.equal(tab.title, 'tab 2', 'Correct new tab title');
|
||||
assert.notEqual(window.tabs.activeTab, tab, 'Correct active tab');
|
||||
|
||||
// end test
|
||||
tab.close(test.done.bind(test));
|
||||
tab.close(done);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// TEST: browserWindows.activeWindow
|
||||
exports.testWindowActivateMethod_simple = function(test) {
|
||||
exports.testWindowActivateMethod_simple = function(assert) {
|
||||
let window = browserWindows.activeWindow;
|
||||
let tab = window.tabs.activeTab;
|
||||
|
||||
window.activate();
|
||||
|
||||
test.assertEqual(browserWindows.activeWindow, window,
|
||||
"Active window is active after window.activate() call");
|
||||
test.assertEqual(window.tabs.activeTab, tab,
|
||||
"Active tab is active after window.activate() call");
|
||||
|
||||
assert.equal(browserWindows.activeWindow, window,
|
||||
'Active window is active after window.activate() call');
|
||||
assert.equal(window.tabs.activeTab, tab,
|
||||
'Active tab is active after window.activate() call');
|
||||
};
|
||||
|
||||
require('sdk/test').run(exports);
|
||||
|
@ -184,12 +184,6 @@ exports.testOnOpenOnCloseListeners = function(test) {
|
||||
/*
|
||||
Disabled due to all of the Win8 PGO bustage in bug 873007.
|
||||
exports.testActiveWindow = function(test) {
|
||||
const xulApp = require("sdk/system/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
|
||||
|
Loading…
x
Reference in New Issue
Block a user