зеркало из https://github.com/mozilla/gecko-dev.git
Bug 901239 - Uplift Add-on SDK to Firefox r=me
This commit is contained in:
Родитель
3c077e5bb9
Коммит
9b6727d39c
|
@ -140,6 +140,7 @@ We'd like to thank our many Jetpack project contributors! They include:
|
|||
* Tim Taubert
|
||||
* Shane Tomlinson
|
||||
* Dave Townsend
|
||||
* [Fraser Tweedale](https://github.com/frasertweedale)
|
||||
* [Matthias Tylkowski](https://github.com/tylkomat)
|
||||
|
||||
### V ###
|
||||
|
|
|
@ -0,0 +1,272 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#Classes and Inheritance
|
||||
A class is a blueprint from which individual objects are created. These
|
||||
individual objects are the instances of the class. Each class defines one or
|
||||
more members, which are initialized to a given value when the class is
|
||||
instantiated. Data members are properties that allow each instance to have
|
||||
their own state, whereas member functions are properties that allow instances to
|
||||
have behavior. Inheritance allows classes to inherit state and behavior from an
|
||||
existing classes, known as the base class. Unlike languages like C++ and Java,
|
||||
JavaScript does not have native support for classical inheritance. Instead, it
|
||||
uses something called prototypal inheritance. As it turns out, it is possible to
|
||||
emulate classical inheritance using prototypal inheritance, but not without
|
||||
writing a significant amount of boilerplate code.
|
||||
|
||||
Classes in JavaScript are defined using constructor functions. Each constructor
|
||||
function has an associated object, known as its prototype, which is shared
|
||||
between all instances of that class. We will show how to define classes using
|
||||
constructors, and how to use prototypes to efficiently define member functions
|
||||
on each instance. Classical inheritance can be implemented in JavaScript using
|
||||
constructors and prototypes. We will show how to make inheritance work correctly
|
||||
with respect to constructors, prototypes, and the instanceof operator, and how
|
||||
to override methods in subclasses. The SDK uses a special constructor internally,
|
||||
known as `Class`, to create constructors that behave properly with respect to
|
||||
inheritance. The last section shows how to work with the `Class` constructor. It
|
||||
is possible to read this section on its own. However, to fully appreciate how
|
||||
`Class` works, and the problem it is supposed to solve, it is recommended that
|
||||
you read the entire article.
|
||||
|
||||
##Constructors
|
||||
In JavaScript, a class is defined by defining a constructor function for that
|
||||
class. To illustrate this, let's define a simple constructor for a class
|
||||
`Shape`:
|
||||
|
||||
function Shape(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
We can now use this constructor to create instances of `Shape`:
|
||||
|
||||
let shape = new Shape(2, 3);
|
||||
shape instanceof Shape; // => true
|
||||
shape.x; // => 2
|
||||
shape.y; // => 3
|
||||
|
||||
The keyword new tells JavaScript that we are performing a constructor call.
|
||||
Constructor calls differ from ordinary function calls in that JavaScript
|
||||
automatically creates a new object and binds it to the keyword this for the
|
||||
duration of the call. Moreover, if the constructor does not return a value, the
|
||||
result of the call defaults to the value of this. Constructors are just ordinary
|
||||
functions, however, so it is perfectly legal to perform ordinary function calls
|
||||
on them. In fact, some people (including the Add-on SDK team) prefer to use
|
||||
constructors this way. However, since the value of this is undefined for
|
||||
ordinary function calls, we need to add some boilerplate code to convert them to
|
||||
constructor calls:
|
||||
|
||||
function Shape(x, y) {
|
||||
if (!this)
|
||||
return new Shape(x, y);
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
##Prototypes
|
||||
Every object has an implicit property, known as its prototype. When JavaScript
|
||||
looks for a property, it first looks for it in the object itself. If it cannot
|
||||
find the property there, it looks for it in the object's prototype. If the
|
||||
property is found on the prototype, the lookup succeeds, and JavaScript pretends
|
||||
that it found the property on the original object. Every function has an
|
||||
explicit property, known as `prototype`. When a function is used in a
|
||||
constructor call, JavaScript makes the value of this property the prototype of
|
||||
the newly created object:
|
||||
|
||||
let shape = Shape(2, 3);
|
||||
Object.getPrototypeOf(shape) == Shape.prototype; // => true
|
||||
|
||||
All instances of a class have the same prototype. This makes the prototype the
|
||||
perfect place to define properties that are shared between instances of the
|
||||
class. To illustrate this, let's add a member function to the class `Shape`:
|
||||
|
||||
Shape.prototype.draw = function () {
|
||||
throw Error("not yet implemented");
|
||||
}
|
||||
let shape = Shape(2, 3);
|
||||
Shape.draw(); // => Error: not yet implemented
|
||||
|
||||
##Inheritance and Constructors
|
||||
Suppose we want to create a new class, `Circle`, and inherit it from `Shape`.
|
||||
Since every `Circle` is also a `Shape`, the constructor for `Circle` must be
|
||||
called every time we call the constructor for `Shape`. Since JavaScript does
|
||||
not have native support for inheritance, it doesn't do this automatically.
|
||||
Instead, we need to call the constructor for `Shape` explicitly. The resulting
|
||||
constructor looks as follows:
|
||||
|
||||
function Circle(x, y, radius) {
|
||||
if (!this)
|
||||
return new Circle(x, y, radius);
|
||||
Shape.call(this, x, y);
|
||||
this.radius = radius;
|
||||
}
|
||||
|
||||
Note that the constructor for `Shape` is called as an ordinary function, and
|
||||
reuses the object created for the constructor call to `Circle`. Had we used a
|
||||
constructor call instead, the constructor for `Shape` would have been applied to
|
||||
a different object than the constructor for `Circle`. We can now use the above
|
||||
constructor to create instances of the class `Circle`:
|
||||
|
||||
let circle = Circle(2, 3, 5);
|
||||
circle instanceof Circle; // => true
|
||||
circle.x; // => 2
|
||||
circle.y; // => 3
|
||||
circle.radius; // => 5
|
||||
|
||||
##Inheritance and Prototypes
|
||||
There is a problem with the definition of `Circle` in the previous section that
|
||||
we have glossed over thus far. Consider the following:
|
||||
|
||||
let circle = Circle(2, 3, 5);
|
||||
circle.draw(); // => TypeError: circle.draw is not a function
|
||||
|
||||
This is not quite right. The method `draw` is defined on instances of `Shape`,
|
||||
so we definitely want it to be defined on instances of `Circle`. The problem is
|
||||
that `draw` is defined on the prototype of `Shape`, but not on the prototype of
|
||||
`Circle`. We could of course copy every property from the prototype of `Shape`
|
||||
over to the prototype of `Circle`, but this is needlessly inefficient. Instead,
|
||||
we use a clever trick, based on the observation that prototypes are ordinary
|
||||
objects. Since prototypes are objects, they have a prototype as well. We can
|
||||
thus override the prototype of `Circle` with an object which prototype is the
|
||||
prototype of `Shape`.
|
||||
|
||||
Circle.prototype = Object.create(Shape.prototype);
|
||||
|
||||
Now when JavaScript looks for the method draw on an instance of Circle, it first
|
||||
looks for it on the object itself. When it cannot find the property there, it
|
||||
looks for it on the prototype of `Circle`. When it cannot find the property
|
||||
there either, it looks for it on `Shape`, at which point the lookup succeeds.
|
||||
The resulting behavior is what we were aiming for.
|
||||
|
||||
##Inheritance and Instanceof
|
||||
The single line of code we added in the previous section solved the problem with
|
||||
prototypes, but introduced a new problem with the **instanceof** operator.
|
||||
Consider the following:
|
||||
|
||||
let circle = Circle(2, 3, 5);
|
||||
circle instanceof Shape; // => false
|
||||
|
||||
Since instances of `Circle` inherit from `Shape`, we definitely want the result
|
||||
of this expression to be true. To understand why it is not, we need to
|
||||
understand how **instanceof** works. Every prototype has a `constructor`
|
||||
property, which is a reference to the constructor for objects with this
|
||||
prototype. In other words:
|
||||
|
||||
Circle.prototype.constructor == Circle // => true
|
||||
|
||||
The **instanceof** operator compares the `constructor` property of the prototype
|
||||
of the left hand side with that of the right hand side, and returns true if they
|
||||
are equal. Otherwise, it repeats the comparison for the prototype of the right
|
||||
hand side, and so on, until either it returns **true**, or the prototype becomes
|
||||
**null**, in which case it returns **false**. The problem is that when we
|
||||
overrode the prototype of `Circle` with an object whose prototype is the
|
||||
prototype of `Shape`, we didn't correctly set its `constructor` property. This
|
||||
property is set automatically for the `prototype` property of a constructor, but
|
||||
not for objects created with `Object.create`. The `constructor` property is
|
||||
supposed to be non-configurable, non-enumberable, and non-writable, so the
|
||||
correct way to define it is as follows:
|
||||
|
||||
Circle.prototype = Object.create(Shape.prototype, {
|
||||
constructor: {
|
||||
value: Circle
|
||||
}
|
||||
});
|
||||
|
||||
##Overriding Methods
|
||||
As a final example, we show how to override the stub implementation of the
|
||||
method `draw` in `Shape` with a more specialized one in `Circle`. Recall that
|
||||
JavaScript returns the first property it finds when walking the prototype chain
|
||||
of an object from the bottom up. Consequently, overriding a method is as simple
|
||||
as providing a new definition on the prototype of the subclass:
|
||||
|
||||
Circle.prototype.draw = function (ctx) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius,
|
||||
0, 2 * Math.PI, false);
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
With this definition in place, we get:
|
||||
|
||||
let shape = Shape(2, 3);
|
||||
shape.draw(); // Error: not yet implemented
|
||||
let circle = Circle(2, 3, 5);
|
||||
circle.draw(); // TypeError: ctx is not defined
|
||||
|
||||
which is the behavior we were aiming for.
|
||||
|
||||
##Classes in the Add-on SDK
|
||||
We have shown how to emulate classical inheritance in JavaScript using
|
||||
constructors and prototypes. However, as we have seen, this takes a significant
|
||||
amount of boilerplate code. The Add-on SDK team consists of highly trained
|
||||
professionals, but they are also lazy: that is why the SDK contains a helper
|
||||
function that handles this boilerplate code for us. It is defined in the module
|
||||
“core/heritage”:
|
||||
|
||||
const { Class } = require('sdk/core/heritage');
|
||||
|
||||
The function `Class` is a meta-constructor: it creates constructors that behave
|
||||
properly with respect to inheritance. It takes a single argument, which is an
|
||||
object which properties will be defined on the prototype of the resulting
|
||||
constructor. The semantics of `Class` are based on what we've learned earlier.
|
||||
For instance, to define a constructor for a class `Shape` in terms of `Class`,
|
||||
we can write:
|
||||
|
||||
let Shape = Class({
|
||||
initialize: function (x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
},
|
||||
draw: function () {
|
||||
throw new Error("not yet implemented");
|
||||
}
|
||||
});
|
||||
|
||||
The property `initialize` is special. When it is present, the call to the
|
||||
constructor is forwarded to it, as are any arguments passed to it (including the
|
||||
this object). In effect, initialize specifies the body of the constructor. Note
|
||||
that the constructors created with `Class` automatically check whether they are
|
||||
called as constructors, so an explicit check is no longer necessary.
|
||||
|
||||
Another special property is `extends`. It specifies the base class from which
|
||||
this class inherits, if any. `Class` uses this information to automatically set
|
||||
up the prototype chain of the constructor. If the extends property is omitted,
|
||||
`Class` itself is used as the base class:
|
||||
|
||||
var shape = new Shape(2, 3);
|
||||
shape instanceof Shape; // => true
|
||||
shape instanceof Class; // => true
|
||||
|
||||
To illustrate the use of the `extends` property, let's redefine the constructor
|
||||
for the class `Circle` in terms of `Class`:
|
||||
|
||||
var Circle = Class({
|
||||
extends: Shape,
|
||||
initialize: function(x, y, radius) {
|
||||
Shape.prototype.initialize.call(this, x, y);
|
||||
this.radius = radius;
|
||||
},
|
||||
draw: function () {
|
||||
context.beginPath();
|
||||
context.arc(this.x, this.y, this.radius,
|
||||
0, 2 * Math.PI, false);
|
||||
context.fill();
|
||||
}
|
||||
});
|
||||
|
||||
Unlike the definition of `Circle` in the previous section, we no longer have to
|
||||
override its prototype, or set its `constructor` property. This is all handled
|
||||
automatically. On the other hand, the call to the constructor for `Shape` still
|
||||
has to be made explicitly. This is done by forwarding to the initialize method
|
||||
of the prototype of the base class. Note that this is always safe, even if there
|
||||
is no `initialize` method defined on the base class: in that case the call is
|
||||
forwarded to a stub implementation defined on `Class` itself.
|
||||
|
||||
The last special property we will look into is `implements`. It specifies a list
|
||||
of objects, which properties are to be copied to the prototype of the
|
||||
constructor. Note that only properties defined on the object itself are copied:
|
||||
properties defined on one of its prototypes are not. This allows objects to
|
||||
inherit from more than one class. It is not true multiple inheritance, however:
|
||||
no constructors are called for objects inherited via `implements`, and
|
||||
**instanceof** only works correctly for classes inherited via `extends`.
|
|
@ -0,0 +1,149 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#Content Processes
|
||||
A content process was supposed to run all the code associated with a single tab.
|
||||
Conversely, an add-on process was supposed to run all the code associated with a
|
||||
single add-on. Neither content or add-on proceses were ever actually
|
||||
implemented, but by the time they were cancelled, the SDK was already designed
|
||||
with them in mind. To understand this article, it's probably best to read it as
|
||||
if content and add-on processes actually exist.
|
||||
|
||||
To communicate between add-on and content processes, the SDK uses something
|
||||
called content scripts. These are explained in the first section. Content
|
||||
scripts communicate with add-on code using something called event emitters.
|
||||
These are explained in the next section. Content workers combine these ideas,
|
||||
allowing you to inject a content script into a content process, and
|
||||
automatically set up a communication channel between them. These are explained
|
||||
in the third section.
|
||||
|
||||
In the next section, we will look at how content scripts interact with the DOM
|
||||
in a content process. There are several caveats here, all of them related to
|
||||
security, that might cause things to not behave in the way you might expect.
|
||||
|
||||
The final section explains why the SDK still uses the notion of content scripts
|
||||
and message passing, even though the multiprocess model for which they were
|
||||
designed never materialized. This, too, is primarily related to security.
|
||||
|
||||
##Content Scripts
|
||||
When the SDK was first designed, Firefox was being refactored towards a
|
||||
multiprocess model. In this model, the UI would be rendered in one process
|
||||
(called the chrome process), whereas each tab and each add-on would run in their
|
||||
own dedicated process (called content and add-on processes, respectively). The
|
||||
project behind this refactor was known as Electrolysis, or E10s. Although E10s
|
||||
has now been suspended, the SDK was designed with this multiprocess model in
|
||||
mind. Afterwards, it was decided to keep the design the way it is: even though
|
||||
its no longer necessary, it turns out that from a security point of view there
|
||||
are several important advantages to thinking about content and add-on code as
|
||||
living in different processes.
|
||||
|
||||
Many add-ons have to interact with content. The problem with the multiprocess
|
||||
model is that add-ons and content are now in different processes, and scripts in
|
||||
one process cannot interact directly with scripts in another. We can, however,
|
||||
pass JSON messages between scripts in different processes. The solution we've
|
||||
come up with is to introduce the notion of content scripts. A content script is
|
||||
a script that is injected into a content process by the main script running in
|
||||
the add-on process. Content scripts differ from scripts that are loaded by the
|
||||
page itself in that they are provided with a messaging API that can be used to
|
||||
send messages back to the add-on script.
|
||||
|
||||
##Event Emitters
|
||||
The messaging API we use to send JSON messages between scripts in different
|
||||
processes is based on the use of event emitters. An event emitter maintains a
|
||||
list of callbacks (or listeners) for one or more named events. Each event
|
||||
emitter has several methods: the method on is used to add a listener for an
|
||||
event. Conversely, the method removeListener is used to remove a listener for an
|
||||
event. The method once is a helper function which adds a listener for an event,
|
||||
and automatically removes it the first time it is called.
|
||||
|
||||
Each event emitter has two associated emit functions. One emit function is
|
||||
associated with the event emitter itself. When this function is called with a
|
||||
given event name, it calls all the listeners currently associated with that
|
||||
event. The other emit function is associated with another event emitter: it was
|
||||
passed as an argument to the constructor of this event emitter, and made into a
|
||||
method. Calling this method causes an event to be emitted on the other event
|
||||
emitter.
|
||||
|
||||
Suppose we have two event emitters in different processes, and we want them to
|
||||
be able to emit events to each other. In this case, we would replace the emit
|
||||
function passed to the constructor of each emitter with a function that sends a
|
||||
message to the other process. We can then hook up a listener to be called when
|
||||
this message arrives at the other process, which in turn calls the emit function
|
||||
on the other event emitter. The combination of this function and the
|
||||
corresponding listener is referred to as a pipe.
|
||||
|
||||
##Content Workers
|
||||
A content worker is an object that is used to inject content scripts into a
|
||||
content process, and to provide a pipe between each content script and the main
|
||||
add-on script. The idea is to use a single content worker for each content
|
||||
process. The constructor for the content worker takes an object containing one
|
||||
or more named options. Among other things, this allows us to specify one or more
|
||||
content scripts to be loaded.
|
||||
|
||||
When a content script is first loaded, the content worker automatically imports
|
||||
a messaging API that allows the it to emit messages over a pipe. On the add-on
|
||||
side, this pipe is exposed via the the port property on the worker. In addition
|
||||
to the port property, workers also support the web worker API, which allows
|
||||
scripts to send messages to each other using the postMessage function. This
|
||||
function uses the same pipe internally, and causes a 'message' event to be
|
||||
emitted on the other side.
|
||||
|
||||
As explained earlier, Firefox doesn't yet use separate processes for tabs or
|
||||
add-ons, so instead, each content script is loaded in a sandbox. Sandboxes were
|
||||
explained [this article]("dev-guide/guides/contributors-guide/modules.html").
|
||||
|
||||
##Accessing the DOM
|
||||
The global for the content sandbox has the window object as its prototype. This
|
||||
allows the content script to access any property on the window object, even
|
||||
though that object lives outside the sandbox. Recall that the window object
|
||||
inside the sandbox is actually a wrapper to the real object. A potential
|
||||
problem with the content script having access to the window object is that a
|
||||
malicious page could override methods on the window object that it knows are
|
||||
being used by the add-on, in order to trick the add-on into doing something it
|
||||
does not expect. Similarly, if the content script defines any values on the
|
||||
window object, a malicious page could potentially steal that information.
|
||||
|
||||
To avoid problems like this, content scripts should always see the built-in
|
||||
properties of the window object, even when they are overridden by another
|
||||
script. Conversely, other scripts should not see any properties added to the
|
||||
window object by the content script. This is where xray wrappers come in. Xray
|
||||
wrappers automatically wrap native objects like the window object, and only
|
||||
exposes their native properties, even if they have been overridden on the
|
||||
wrapped object. Conversely, any properties defined on the wrapper are not
|
||||
visible from the wrapped object. This avoids both problems we mentioned earlier.
|
||||
|
||||
The fact that you can't override the properties of the window object via a
|
||||
content script is sometimes inconvenient, so it is possible to circumvent this:
|
||||
by defining the property on window.wrappedObject, the property is defined on the
|
||||
underlying object, rather than the wrapper itself. This feature should only be
|
||||
used when you really need it, however.
|
||||
|
||||
##A few Notes on Security
|
||||
As we stated earlier, the SDK was designed with multiprocess support in mind,
|
||||
despite the fact that work on implementing this in Firefox has currently been
|
||||
suspended. Since both add-on modules and content scripts are currently loaded in
|
||||
sandboxes rather than separate processes, and sandboxes can communicate with
|
||||
each other directly (using imports/exports), you might be wondering why we have
|
||||
to go through all the trouble of passing messages between add-on and content
|
||||
scripts. The reason for this extra complexity is that the code for add-on
|
||||
modules and content scripts has different privileges. Every add-on module can
|
||||
get chrome privileges simply by asking for them, whereas content scripts have
|
||||
the same privileges as the page it is running on.
|
||||
|
||||
When two sandboxes have the same privileges, a wrapper in one sandbox provides
|
||||
transparent access to an object in the other sandbox. When the two sandboxes
|
||||
have different privileges, things become more complicated, however. Code with
|
||||
content privileges should not be able to acces code with chrome privileges, so
|
||||
we use specialized wrappers, called security wrappers, to limit access to the
|
||||
object in the other sandbox. The xray wrappers we saw earlier are an example of
|
||||
such a security wrapper. Security wrappers are created automatically, by the
|
||||
underlying host application.
|
||||
|
||||
A full discussion of the different kinds of security wrappers and how they work
|
||||
is out of scope for this document, but the main point is this: security wrappers
|
||||
are very complex, and very error-prone. They are subject to change, in order to
|
||||
fix some security leak that recently popped up. As a result, code that worked
|
||||
just fine last week suddenly does not work the way you expect. By only passing
|
||||
messages between add-on modules and content scripts, these problems can be
|
||||
avoided, making your add-on both easier to debug and to maintain.
|
|
@ -0,0 +1,318 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#Getting Started
|
||||
The contribution process consists of a number of steps. First, you need to get
|
||||
a copy of the code. Next, you need to open a bug for the bug or feature you want
|
||||
to work on, and assign it to yourself. Alternatively, you can take an existing
|
||||
bug to work on. Once you've taken a bug, you can start writing a patch. Once
|
||||
your patch is complete, you've made sure it doesn't break any tests, and you've
|
||||
gotten a positive review for it, the last step is to request for your patch to
|
||||
be merged with the main codebase.
|
||||
|
||||
Although these individual steps are all obvious, there are quite some details
|
||||
involved. The rest of this article will cover each individual step of the
|
||||
contribution process in more detail.
|
||||
|
||||
##Getting the Code
|
||||
The Add-on SDK code is hosted on GitHub. GitHub is a web-based hosting service
|
||||
for software projects that is based on Git, a distributed version control
|
||||
system. Both GitHub and Git are an integral part of our workflow. If you haven't
|
||||
familiarized yourself with Git before, I strongly suggest you do so now. You're
|
||||
free to ignore that suggestion if you want, but it's going to hurt you later on
|
||||
(don't come crying to me if you end up accidentally detaching your head, for
|
||||
instance). A full explanation of how to use Git is out of scope for this
|
||||
document, but a very good one
|
||||
[can be found online here](http://git-scm.com/book). Reading at least sections
|
||||
1-3 from that book should be enough to get you started.
|
||||
|
||||
If you're already familiar with Git, or if you decided to ignore my advice and
|
||||
jump right in, the following steps will get you a local copy of the Add-on SDK
|
||||
code on your machine:
|
||||
|
||||
1. Fork the SDK repository to your GitHub account
|
||||
2. Clone the forked repository to your machine
|
||||
|
||||
A fork is similar to a clone in that it creates a complete copy of a repository,
|
||||
including the history of every file. The difference is that a fork copies the
|
||||
repository to your GitHub account, whereas a clone copies it to your machine. To
|
||||
create a fork of the SDK repository, you need a GitHub account. If you don't
|
||||
already have one, you can [create one here](https://github.com/) (don't worry:
|
||||
it's free!). Once you got yourself an account, go to
|
||||
[the Add-on SDK repository](https://github.com/mozilla/addon-sdk), and click the
|
||||
fork button in the upper-right corner. This will start the forking process.
|
||||
This could take anywhere between a couple of seconds and a couple of minutes.
|
||||
|
||||
Once the forking process is complete, the forked repository will be available at
|
||||
https://github.com/\<your-username\>/addon-sdk. To create a clone of the this
|
||||
repository, you need to have Git installed on your machine. If you don’t have it
|
||||
already, you can [download it here](http://git-scm.com/). Once you have Git
|
||||
installed (make sure you also configured your name and e-mail
|
||||
address), open your terminal, and enter the following command from the directory
|
||||
where you want to have the clone stored:
|
||||
|
||||
> `git clone ssh://github.com/<your-username>/addon-sdk`
|
||||
|
||||
This will start the cloning process. Like the forking process, this could take
|
||||
anywhere between a couple of seconds and a couple of minutes, depending on the
|
||||
speed of your connection.
|
||||
|
||||
If you did everything correctly so far, once the cloning process is complete,
|
||||
the cloned repository will have been stored inside the directory from which you
|
||||
ran the clone command, in a new directory called addon-sdk. Now we can start
|
||||
working with it. Yay!
|
||||
|
||||
As a final note: it is possible to skip step 1, and clone the SDK repository
|
||||
directly to your machine. This is useful if you only want to study the SDK code.
|
||||
However, if your goal is to actually contribute to the SDK, skipping step 1 is a
|
||||
bad idea, because you won’t be able to make pull requests in that case.
|
||||
|
||||
##Opening a Bug
|
||||
In any large software project, keeping track of bugs is crucially important.
|
||||
Without it, developers wouldn't be able to answer questions such as: what do I
|
||||
need to work on, who is working on what, etc. Mozilla uses its own web-based,
|
||||
general-purpose bugtracker, called Bugzilla, to keep track of bugs. Like GitHub
|
||||
and Git, Bugzilla is an integral part of our workflow. When you discover a new
|
||||
bug, or want to implement a new feature, you start by creating an entry for it
|
||||
in Bugzilla. By doing so, you give the SDK team a chance to confirm whether your
|
||||
bug isn't actually a feature, or your feature isn't actually a bug
|
||||
(that is, a feature we feel doesn't belong into the SDK).
|
||||
|
||||
Within Bugzilla, the term _bug_ is often used interchangably to refer to both
|
||||
bugs and features. Similarly, the Bugzilla entry for a bug is also named bug,
|
||||
and the process of creating it is known as _opening a bug_. It is important that
|
||||
you understand this terminology, as other people will regularly refer to it.
|
||||
|
||||
I really urge you to open a bug first and wait for it to get confirmed before
|
||||
you start working on something. Nothing sucks more than your patch getting
|
||||
rejected because we felt it shouldn't go into the SDK. Having this discussion
|
||||
first saves you from doing useless work. If you have questions about a bug, but
|
||||
don't know who to ask (or the person you need to ask isn't online), Bugzilla is
|
||||
the communication channel of choice. When you open a bug, the relevant people
|
||||
are automatically put on the cc-list, so they will get an e-mail every time you
|
||||
write a comment in the bug.
|
||||
|
||||
To open a bug, you need a Bugzilla account. If you don't already have one, you
|
||||
can [create it here](https://bugzilla.mozilla.org/). Once you got yourself an
|
||||
account, click the "new" link in the upper-left corner. This will take you to a
|
||||
page where you need to select the product that is affected by your bug. It isn't
|
||||
immediately obvious what you should pick here (and with not immediately obvious
|
||||
I mean completely non-obvious), so I'll just point it out to you: as you might
|
||||
expect, the Add-on SDK is listed under "Other products", at the bottom of the
|
||||
page.
|
||||
|
||||
After selecting the Add-on SDK, you will be taken to another page, where you
|
||||
need to fill out the details for the bug. The important fields are the component
|
||||
affected by this bug, the summary, and a short description of the bug (don't
|
||||
worry about coming up with the perfect description for your bug. If something is
|
||||
not clear, someone from the SDK team will simply write a comment asking for
|
||||
clarification). The other fields are optional, and you can leave them as is, if
|
||||
you so desire.
|
||||
|
||||
Note that when you fill out the summary field, Bugzilla automatically looks for
|
||||
bugs that are possible duplicates of the one you're creating. If you spot such a
|
||||
duplicate, there's no need to create another bug. In fact, doing so is
|
||||
pointless, as duplicate bugs are almost always immediately closed. Don't worry
|
||||
about accidentally opening a duplicate bug though. Doing so is not considered a
|
||||
major offense (unless you do it on purpose, of course).
|
||||
|
||||
After filling out the details for the bug, the final step is to click the
|
||||
"Submit Bug" button at the bottom of the page. Once you click this button, the
|
||||
bug will be stored in Bugzilla’s database, and the creation process is
|
||||
completed. The initial status of your bug will be `UNCONFIRMED`. All you need to
|
||||
do now is wait for someone from the SDK team to change the status to either
|
||||
`NEW` or `WONTFIX`.
|
||||
|
||||
##Taking a Bug
|
||||
Since this is a contributor's guide, I've assumed until now that if you opened a
|
||||
bug, you did so with the intention of fixing it. Simply because you're the one
|
||||
that opened it doesn't mean you have to fix a bug, however. Conversely, simply
|
||||
because you're _not_ the one that opened it doesn't mean you can't fix a bug. In
|
||||
fact, you can work on any bug you like, provided nobody else is already working
|
||||
on it. To check if somebody is already working on a bug, go to the entry for
|
||||
that bug and check the "Assigned To" field. If it says "Nobody; OK to take it
|
||||
and work on it", you're good to go: you can assign the bug to yourself by
|
||||
clicking on "(take)" right next to it.
|
||||
|
||||
Keep in mind that taking a bug to creates the expectation that you will work on
|
||||
it. It's perfectly ok to take your time, but if this is the first bug you're
|
||||
working on, you might want to make sure that this isn't something that has very
|
||||
high priority for the SDK team. You can do so by checking the importance field
|
||||
on the bug page (P1 is the highest priority). If you've assigned a bug to
|
||||
yourself that looked easy at the time, but turns out to be too hard for you to
|
||||
fix, don't feel bad! It happens to all of us. Just remove yourself as the
|
||||
assignee for the bug, and write a comment explaining why you're no longer able
|
||||
to work on it, so somebody else can take a shot at it.
|
||||
|
||||
A word of warning: taking a bug that is already assigned to someone else is
|
||||
considered extremely rude. Just imagine yourself working hard on a series of
|
||||
patches, when suddenly this jerk comes out of nowhere and submits his own
|
||||
patches for the bug. Not only is doing so an inefficient use of time, it also
|
||||
shows a lack of respect for other the hard work of other contributors. The other
|
||||
side of the coin is that contributors do get busy every now and then, so if you
|
||||
stumble upon a bug that is already assigned to someone else but hasn't shown any
|
||||
activity lately, chances are the person to which the bug is assigned will gladly
|
||||
let you take it off his/her hands. The general rule is to always ask the person
|
||||
assigned to the bug if it is ok for you to take it.
|
||||
|
||||
As a final note, if you're not sure what bug to work on, or having a hard time
|
||||
finding a bug you think you can handle, a useful tip is to search for the term
|
||||
"good first bug". Bugs that are particularly easy, or are particularly well
|
||||
suited to familiarize yourself with the SDK, are often given this label by the
|
||||
SDK team when they're opened.
|
||||
|
||||
##Writing a Patch
|
||||
Once you've taken a bug, you're ready to start doing what you really want to do:
|
||||
writing some code. The changes introduced by your code are known as a patch.
|
||||
Your goal, of course, is to get this patch landed in the main SDK repository. In
|
||||
case you aren't familiar with git, the following command will cause it to
|
||||
generate a diff:
|
||||
|
||||
> `git diff`
|
||||
|
||||
A diff describes all the changes introduced by your patch. These changes are not
|
||||
yet final, since they are not yet stored in the repository. Once your patch is
|
||||
complete, you can _commit_ it to the repository by writing:
|
||||
|
||||
> `git commit`
|
||||
|
||||
After pressing enter, you will be prompted for a commit message. What goes in
|
||||
the commit message is more or less up to you, but you should at least include
|
||||
the bug number and a short summary (usually a single line) of what the patch
|
||||
does. This makes it easier to find your commit later on.
|
||||
|
||||
It is considered good manners to write your code in the same style as the rest
|
||||
of a file. It doesn't really matter what coding style you use, as long as it's
|
||||
consistent. The SDK might not always use the exact same coding style for each
|
||||
file, but it strives to be as consistent as possible. Having said that: if
|
||||
you're not completely sure what coding style to use, just pick something and
|
||||
don't worry about it. If the rest of the file doesn't make it clear what you
|
||||
should do, it most likely doesn't matter.
|
||||
|
||||
##Making a Pull Request
|
||||
To submit a patch for review, you need to make a pull request. Basically, a pull
|
||||
request is a way of saying: "Hey, I've created this awesome patch on top of my
|
||||
fork of the SDK repository, could you please merge it with the global
|
||||
repository?". GitHub has built-in support for pull requests. However, you can
|
||||
only make pull requests from repositories on your GitHub account, not from
|
||||
repositories on your local machine. This is why I told you to fork the SDK
|
||||
repository to your GitHub account first (you did listen to me, didn't you?).
|
||||
|
||||
In the previous section, you commited your patch to your local repository, so
|
||||
here, the next step is to synchronize your local repository with the remote one,
|
||||
by writing:
|
||||
|
||||
> `git push`
|
||||
|
||||
This pushes the changes from your local repository into the remote repository.
|
||||
As you might have guessed, a push is the opposite of a pull, where somebody else
|
||||
pulls changes from a remote repository into their own repository (hence the term
|
||||
'pull request'). After pressing enter, GitHub will prompt you for your username
|
||||
and password before actually allowing the push.
|
||||
|
||||
If you did everything correctly up until this point, your patch should now show
|
||||
up in your remote repository (take a look at your repository on GitHub to make
|
||||
sure). We're now ready to make a pull request. To do so, go to your repository
|
||||
on GitHub and click the "Pull Request" button at the top of the page. This will
|
||||
take you to a new page, where you need to fill out the title of your pull
|
||||
request, as well as a short description of what the patch does. As we said
|
||||
before, it is common practice to at least include the bug number and a short
|
||||
summary in the title. After you've filled in both fields, click the "Send Pull
|
||||
Request" button.
|
||||
|
||||
That's it, we're done! Or are we? This is software development after all, so
|
||||
we'd expect there to be at least one redundant step. Luckily, there is such a
|
||||
step, because we also have to submit our patch for review on Bugzilla. I imagine
|
||||
you might be wondering to yourself right now: "WHY???". Let me try to explain.
|
||||
The reason we have this extra step is that most Mozilla projects use Mercurial
|
||||
and Bugzilla as their version control and project management tool, respectively.
|
||||
To stay consistent with the rest of Mozilla, we provide a Mercurial mirror of
|
||||
our Git repository, and submit our patches for review in both GitHub and
|
||||
Bugzilla.
|
||||
|
||||
If that doesn't make any sense to you, that's ok: it doesn't to me, either. The
|
||||
good news, however, is that you don't have to redo all the work you just did.
|
||||
Normally, when you want to submit a patch for review on Bugzilla, you have to
|
||||
create a diff for the patch and add it as an attachment to the bug (if you still
|
||||
haven't opened one, this would be the time to do it). However, these changes are
|
||||
also described by the commit of your patch, so its sufficient to attach a file
|
||||
that links to the pull request. To find the link to your pull request, go to
|
||||
your GitHub account and click the "Pull Requests" button at the top. This will
|
||||
take you to a list of your active pull requests. You can use the template here
|
||||
below as your attachment. Simply copy the link to your pull request, and use it
|
||||
to replace all instances of \<YOUR_LINK_HERE\>:
|
||||
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="refresh" content="<YOUR_LINK_HERE>">
|
||||
<title>Bugzilla Code Review</title>
|
||||
<p>You can review this patch at <a href="<YOUR_LINK_HERE >"><YOUR_LINK_HERE></a>,
|
||||
or wait 5 seconds to be redirected there automatically.</p>
|
||||
|
||||
Finally, to add the attachment to the bug, go to the bug in Bugzilla, and click
|
||||
on "Add an attachment" right above the comments. Make sure you fill out a
|
||||
description for the attachment, and to set the review flag to '?' (you can find
|
||||
a list of reviewers on
|
||||
[this page](https://github.com/mozilla/addon-sdk/wiki/contribute)). The '?' here
|
||||
means that you're making a request. If your patch gets a positive review, the
|
||||
reviewer will set this flag to '+'. Otherwise, he/she will set it to '-', with
|
||||
some feedback on why your patch got rejected. Of course, since we also use
|
||||
GitHub for our review process, you're most likely to get your feedback there,
|
||||
instead of Bugzilla. If your patch didn't get a positive review right away,
|
||||
don't sweat it. If you waited for your bug to get confirmed before submitting
|
||||
your patch, you'll usually only have to change a few small things to get a
|
||||
positive review for your next attempt. Once your patch gets a positive review,
|
||||
you don't need to do anything else. Since you did a pull request, it will
|
||||
automatically be merged into the remote repository, usually by the person that
|
||||
reviewed your patch.
|
||||
|
||||
##Getting Additional Help
|
||||
If something in this article wasn't clear to you, or if you need additional
|
||||
help, the best place to go is irc. Mozilla relies heavily on irc for direct
|
||||
communication between contributors. The SDK team hangs out on the #jetpack
|
||||
channel on the irc.mozilla.org server (Jetpack was the original name of the
|
||||
SDK, in case you're wondering).
|
||||
|
||||
Unless you are know what you are doing, it can be hard to get the information
|
||||
you need from irc, uso here are a few useful tips:
|
||||
|
||||
* Mozilla is a global organization, with contributors all over the world, so the
|
||||
people you are trying to reach are likely not in the same timezone as you.
|
||||
Most contributors to the SDK are currently based in the US, so if you're in
|
||||
Europe, and asking a question on irc in the early afternoon, you're not likely
|
||||
to get many replies.
|
||||
|
||||
* Most members of the SDK team are also Mozilla employees, which means they're
|
||||
often busy doing other stuff. That doesn't mean they don't want to help you.
|
||||
On the contrary: Mozilla encourages employees to help out contributors
|
||||
whenever they can. But it does mean that we're sometimes busy doing other
|
||||
things than checking irc, so your question may go unnoticed. If that happens,
|
||||
the best course of action is often to just ask again.
|
||||
|
||||
* If you direct your question to a specific person, rather than the entire
|
||||
channel, your chances of getting an answer are a lot better. If you prefix
|
||||
your message with that person's irc name, he/she will get a notification in
|
||||
his irc client. Try to make sure that the person you're asking is actually the
|
||||
one you need, though. Don't just ask random questions to random persons in the
|
||||
hopes you'll get more response that way.
|
||||
|
||||
* If you're not familiar with irc, a common idiom is to send someone a message
|
||||
saying "ping" to ask if that person is there. When that person actually shows
|
||||
up and sees the ping, he will send you a message back saying "pong". Cute,
|
||||
isn't it? But hey, it works.
|
||||
|
||||
* Even if someone does end up answering your questions, it can happen that that
|
||||
person gets distracted by some other task and forget he/she was talking to
|
||||
you. Please don't take that as a sign we don't care about your questions. We
|
||||
do, but we too get busy sometimes: we're only human. If you were talking to
|
||||
somebody and haven't gotten any reply to your last message for some time, feel
|
||||
free to just ask again.
|
||||
|
||||
* If you've decided to pick up a good first bug, you can (in theory at least)
|
||||
get someone from the SDK team to mentor you. A mentor is someone who is
|
||||
already familiar with the code who can walk you through it, and who is your go
|
||||
to guy in case you have any questions about it. The idea of mentoring was
|
||||
introduced a while ago to make it easier for new contributors to familiarize
|
||||
themselves with the code. Unfortunately, it hasn't really caught on yet, but
|
||||
we're trying to change that. So by all means: ask!
|
|
@ -0,0 +1,316 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#Modules
|
||||
A module is a self-contained unit of code, which is usually stored in a file,
|
||||
and has a well defined interface. The use of modules greatly improves the
|
||||
maintainability of code, by splitting it up into independent components, and
|
||||
enforcing logical boundaries between them. Unfortunately, JavaScript does not
|
||||
yet have native support for modules: it has to rely on the host application to
|
||||
provide it with functionality such as loading subscripts, and exporting/
|
||||
importing names. We will show how to do each of these things using the built-in
|
||||
Components object provided by Xulrunner application such as Firefox and
|
||||
Thunderbird.
|
||||
|
||||
To improve encapsulation, each module should be defined in the scope of its own
|
||||
global object. This is made possible by the use of sandboxes. Each sandbox lives
|
||||
in its own compartment. A compartment is a separate memory space. Each
|
||||
compartment has a set of privileges that determines what scripts running in that
|
||||
compartment can and cannot do. We will show how sandboxes and compartments can
|
||||
be used to improve security in our module system.
|
||||
|
||||
The module system used by the SDK is based on the CommonJS specification: it is
|
||||
implemented using a loader object, which handles all the bookkeeping related to
|
||||
module loading, such as resolving and caching URLs. We show how to create your
|
||||
own custom loaders, using the `Loader` constructor provided by the SDK. The SDK
|
||||
uses its own internal loader, known as Cuddlefish. All modules within the SDK
|
||||
are loaded using Cuddlefish by default. Like any other custom loader, Cuddlefish
|
||||
is created using the `Loader` constructor. In the final section, we will take a
|
||||
look at some of the options passed by the SDK to the `Loader` constructor to
|
||||
create the Cuddlefish loader.
|
||||
|
||||
##Loading Subscripts
|
||||
When a JavaScript project reaches a certain size, it becomes necessary to split
|
||||
it up into multiple files. Unfortunately, JavaScript does not provide any means
|
||||
to load scripts from other locations: we have to rely on the host application to
|
||||
provide us with this functionality. Applications such as Firefox and Thunderbird
|
||||
are based on Xulrunner. Xulrunner adds a built-in object, known as `Components`,
|
||||
to the global scope. This object forms the central access point for all
|
||||
functionality provided by the host application. A complete explanation of how to
|
||||
use `Components` is out of scope for this document. However, the following
|
||||
example shows how it can be used to load scripts from other locations:
|
||||
|
||||
const {
|
||||
classes: Cc
|
||||
interfaces: Ci
|
||||
} = Components;
|
||||
|
||||
var instance = Cc["@mozilla.org/moz/jssubscript-loader;1"];
|
||||
var loader = instance.getService(Ci.mozIJSSubScriptLoader);
|
||||
|
||||
function loadScript(url) {
|
||||
loader.loadSubScript(url);
|
||||
}
|
||||
|
||||
When a script is loaded, it is evaluated in the scope of the global object of
|
||||
the script that loaded it. Any property defined on the global object will be
|
||||
accessible from both scripts:
|
||||
|
||||
index.js:
|
||||
loadScript("www.foo.com/a.js");
|
||||
foo; // => 3
|
||||
|
||||
a.js:
|
||||
foo = 3;
|
||||
|
||||
##Exporting Names
|
||||
The script loader we obtained from the `Components` object allows us load
|
||||
scripts from other locations, but its API is rather limited. For instance, it
|
||||
does not know how to handle relative URLs, which is cumbersome if you want to
|
||||
organize your project hierarchically. A more serious problem with the
|
||||
`loadScript` function, however, is that it evaluates all scripts in the scope of
|
||||
the same global object. This becomes a problem when two scripts try to define
|
||||
the same property:
|
||||
|
||||
index.js:
|
||||
loadScript("www.foo.com/a.js");
|
||||
loadScript("www.foo.com/b.js");
|
||||
foo; // => 5
|
||||
|
||||
a.js:
|
||||
foo = 3;
|
||||
|
||||
b.js:
|
||||
foo = 5;
|
||||
|
||||
In the above example, the value of `foo` depends on the order in which the
|
||||
subscripts are loaded: there is no way to access the property foo defined by
|
||||
"a.js", since it is overwritten by "b.js". To prevent scripts from interfering
|
||||
with each other, `loadScript` should evaluate each script to be loaded in the
|
||||
scope of their own global object, and then return the global object as its
|
||||
result. In effect, any properties defined by the script being loaded on its
|
||||
global object are exported to the loading script. The script loader we obtained
|
||||
from `Components` allows us to do just that:
|
||||
|
||||
function loadScript(url) {
|
||||
let global = {};
|
||||
loader.loadSubScript(url, global);
|
||||
return global;
|
||||
}
|
||||
|
||||
If present, the `loadSubScript` function evaluates the script to be loaded in
|
||||
the scope of the second argument. Using this new version of `loadScript`, we can
|
||||
now rewrite our earlier example as follows
|
||||
|
||||
index.js:
|
||||
let a = loadScript("www.foo.com/a.js");
|
||||
let b = loadScript("www.foo.com/b.js");
|
||||
|
||||
a.foo // => 3
|
||||
b.foo; // => 5
|
||||
|
||||
a.js:
|
||||
foo = 3;
|
||||
|
||||
b.js:
|
||||
foo = 5;:
|
||||
|
||||
##Importing Names
|
||||
In addition to exporting properties from the script being loaded to the loading
|
||||
script, we can also import properties from the loading script to the script
|
||||
being loaded:
|
||||
|
||||
function loadScript(url, imports) {
|
||||
let global = {
|
||||
imports: imports,
|
||||
exports: {}
|
||||
};
|
||||
loader.loadSubScript(url, global);
|
||||
return global.exports;
|
||||
}
|
||||
|
||||
Among other things, this allows us to import `loadScript` to scripts being
|
||||
loaded, allowing them to load further scripts:
|
||||
|
||||
index.js:
|
||||
loadScript("www.foo.com/a.js", {
|
||||
loadScript: loadScript
|
||||
}).foo; => 5
|
||||
|
||||
a.js:
|
||||
exports.foo = imports.loadScript("www.foo.com/b.js").bar;
|
||||
|
||||
b.js:
|
||||
exports.bar = 5;
|
||||
|
||||
##Sandboxes and Compartments
|
||||
The `loadScript` function as defined int the previous section still has some
|
||||
serious shortcomings. The object it passed to the `loadSubScript` function is an
|
||||
ordinary object, which has the global object of the loading script as its
|
||||
prototype. This breaks encapsulation, as it allows the script being loaded to
|
||||
access the built-in constructors of the loading script, which are defined on its
|
||||
global object. The problem with breaking encapsulation like this is that
|
||||
malicious scripts can use it to get the loading script to execute arbitrary
|
||||
code, by overriding one of the methods on the built-in constructors. If the
|
||||
loading script has chrome privileges, then so will any methods called by the
|
||||
loading script, even if that method was installed by a malicious script.
|
||||
|
||||
To avoid problems like this, the object passed to `loadSubScript` should be a
|
||||
true global object, having its own instances of the built-in constructors. This
|
||||
is exactly what sandboxes are for. A sandbox is a global object that lives in a
|
||||
separate compartment. Compartments are a fairly recent addition to SpiderMonkey,
|
||||
and can be seen as a separate memory space. Objects living in one compartment
|
||||
cannot be accessed directly from another compartment: they need to be accessed
|
||||
through an intermediate object, known as a wrapper. Compartments are very
|
||||
useful from a security point of view: each compartment has a set of privileges
|
||||
that determines what a script running in that compartment can and cannot do.
|
||||
Compartments with chrome privileges have access to the `Components` object,
|
||||
giving them full access to the host platform. In contrast, compartments with
|
||||
content privileges can only use those features available to ordinary websites.
|
||||
|
||||
The `Sandbox` constructor takes a `URL` parameter, which is used to determine
|
||||
the set of privileges for the compartment in which the sandbox will be created.
|
||||
Passing an XUL URL will result in a compartment with chrome privileges (note,
|
||||
however, that if you ever actually do this in any of your code, Gabor will be
|
||||
forced to hunt you down and kill you). Otherwise, the compartment will have
|
||||
content privileges by default. Rewriting the `loadScript` function using
|
||||
sandboxes, we end up with:
|
||||
|
||||
function loadScript(url, imports) {
|
||||
let global = Components.utils.Sandbox(url);
|
||||
global.imports = imports;
|
||||
global.exports = {};
|
||||
loader.loadSubScript(url, global);
|
||||
return global.exports;
|
||||
}
|
||||
|
||||
Note that the object returned by `Sandbox` is a wrapper to the sandbox, not the
|
||||
sandbox itself. A wrapper behaves exactly like the wrapped object, with one
|
||||
difference: for each property access/function it performs an access check to
|
||||
make sure that the calling script is actually allowed to access/call that
|
||||
property/function. If the script being loaded is less privileged than the
|
||||
loading script, the access is prevented, as the following example shows:
|
||||
|
||||
index.js:
|
||||
let a = loadScript("www.foo.com/a.js", {
|
||||
Components: Components
|
||||
});
|
||||
|
||||
// index.js has chrome privileges
|
||||
Components.utils; // => [object nsXPCComponents_Utils]
|
||||
|
||||
a.js:
|
||||
// a.js has content privileges
|
||||
imports.Components.utils; // => undefined
|
||||
|
||||
##Modules in the Add-on SDK
|
||||
The module system used by the SDK is based on what we learned so far: it follows
|
||||
the CommonJS specification, which attempts to define a standardized module API.
|
||||
A CommonJS module defines three global variables: `require`, which is a function
|
||||
that behaves like `loadScript` in our examples, `exports`, which behaves
|
||||
like the `exports` object, and `module`, which is an object representing
|
||||
the module itself. The `require` function has some extra features not provided
|
||||
by `loadScript`: it solves the problem of resolving relative URLs (which we have
|
||||
left unresolved), and provides a caching mechanism, so that when the same module
|
||||
is loaded twice, it returns the cached module object rather than triggering
|
||||
another download. The module system is implemented using a loader object, which
|
||||
is actually provided as a module itself. It is defined in the module
|
||||
“toolkit/loader”:
|
||||
|
||||
const { Loader } = require('toolkit/loader')
|
||||
|
||||
The `Loader` constructor allows you to create your own custom loader objects. It
|
||||
takes a single argument, which is a named options object. For instance, the
|
||||
option `paths` is used to specify a list of paths to be used by the loader to
|
||||
resolve relative URLs:
|
||||
|
||||
let loader = Loader({
|
||||
paths: ["./": http://www.foo.com/"]
|
||||
});
|
||||
|
||||
CommonJS also defines the notion of a main module. The main module is always the
|
||||
first to be loaded, and differs from ordinary modules in two respects. Firstly,
|
||||
since they do not have a requiring module. Instead, the main module is loaded
|
||||
using a special function, called `main`:
|
||||
|
||||
const { Loader, main } = require('toolkit/loader');
|
||||
|
||||
let loader = Loader({
|
||||
paths: ["./": http://www.foo.com/"]
|
||||
});
|
||||
|
||||
main(loader, "./main.js");
|
||||
|
||||
Secondly, the main module is defined as a property on `require`. This allows
|
||||
modules to check if it they have been loaded as the main module:
|
||||
|
||||
function main() {
|
||||
...
|
||||
}
|
||||
|
||||
if (require.main === module)
|
||||
main();
|
||||
|
||||
##The Cuddlefish Loader
|
||||
The SDK uses its own internal loader, known as Cuddlefish (because we like crazy
|
||||
names). Like any other custom loader, Cuddlefish is created using the `Loader`
|
||||
constructor: Let's take a look at some of the options used by Cuddlefish to
|
||||
customize its behavior. The way module ids are resolved can be customized by
|
||||
passing a custom `resolve` function as an option. This function takes the id to
|
||||
be resolved and the requiring module as an argument, and returns the resolved id
|
||||
as its result. The resolved id is then further resolved using the paths array:
|
||||
|
||||
const { Loader, main } = require('toolkit/loader');
|
||||
|
||||
let loader = Loader({
|
||||
paths: ["./": "http://www.foo.com/"],
|
||||
resolve: function (id, requirer) {
|
||||
// Your code here
|
||||
return id;
|
||||
}
|
||||
});
|
||||
main(loader, "./main.js");
|
||||
|
||||
Cuddlefish uses a custom `resolve` function to implement a form of access
|
||||
control: modules can only require modules for which they have been explicitly
|
||||
granted access. A whitelist of modules is generated statically when the add-on
|
||||
is linked. It is possible to pass a list of predefined modules as an option to
|
||||
the `Loader` constructor. This is useful if the API to be exposed does not have
|
||||
a corresponding JS file, or is written in an incompatible format. Cuddlefish
|
||||
uses this option to expose the `Components` object as a module called `chrome`,
|
||||
in a way similar to the code here below:
|
||||
|
||||
const {
|
||||
classes: Cc,
|
||||
Constructor: CC,
|
||||
interfaces: Ci,
|
||||
utils: Cu,
|
||||
results: Cr,
|
||||
manager: Cm
|
||||
} = Components;
|
||||
|
||||
let loader = Loader({
|
||||
paths: ["./": "http://www.foo.com/"],
|
||||
resolve: function (id, requirer) {
|
||||
// Your logic here
|
||||
return id;
|
||||
},
|
||||
modules: {
|
||||
'chrome': {
|
||||
components: Components,
|
||||
Cc: Cc,
|
||||
CC: bind(CC, Components),
|
||||
Ci: Ci,
|
||||
Cu: Cu,
|
||||
Cr: Cr,
|
||||
Cm: Cm
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
All accesses to the `chrome` module go through this one point. As a result, we
|
||||
don't have to give modules chrome privileges on a case by case basis. More
|
||||
importantly, however, any module that wants access to `Components` has to
|
||||
explicitly express its intent via a call to `require("chrome")`. This makes it
|
||||
possible to reason about which modules have chrome capabilities and which don't.
|
|
@ -0,0 +1,261 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
#Private Properties
|
||||
|
||||
A private property is a property that is only accessible to member
|
||||
functions of instances of the same class. Unlike other languages, JavaScript
|
||||
does not have native support for private properties. However, people have come
|
||||
up with several ways to emulate private properties using existing language
|
||||
features. We will take a look at two different techniques, using prefixes, and
|
||||
closures, respectively.
|
||||
|
||||
Prefixes and closures both have drawbacks in that they are either not
|
||||
restrictive enough or too restrictive, respectively. We will therefore introduce
|
||||
a third technique, based on the use of WeakMaps, that solves both these
|
||||
problems. Note, however, that WeakMaps might not be supported by all
|
||||
implementations yet. Next, we generalize the idea of using WeakMaps from
|
||||
associating one or more private properties with an object to associating one or
|
||||
more namespaces with each object. A namespace is simply an object on which one
|
||||
or more private properties are defined.
|
||||
|
||||
The SDK uses namespaces internally to implement private properties. The last
|
||||
section explains how to work with the particular namespace implementation used
|
||||
by the SDK. It is possible to read this section on its own, but to fully
|
||||
appreciate how namespaces work, and the problem they are supposed to solve, it
|
||||
is recommended that you read the entire article.
|
||||
|
||||
##Using Prefixes
|
||||
|
||||
A common technique to implement private properties is to prefix each private
|
||||
property name with an underscore. Consider the following example:
|
||||
|
||||
function Point(x, y) {
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
}
|
||||
|
||||
The properties `_x` and `_y` are private, and should only be accessed by member
|
||||
functions.
|
||||
|
||||
To make a private property readable/writable from any function, it is common to
|
||||
define a getter/setter function for the property, respectively:
|
||||
|
||||
Point.prototype.getX = function () {
|
||||
return this._x;
|
||||
};
|
||||
|
||||
Point.prototype.setX = function (x) {
|
||||
this._x = x;
|
||||
};
|
||||
|
||||
Point.prototype.getY = function () {
|
||||
return this._y;
|
||||
};
|
||||
|
||||
Point.prototype.setY = function (y) {
|
||||
this._y = y;
|
||||
};
|
||||
|
||||
The above technique is simple, and clearly expresses our intent. However, the
|
||||
use of an underscore prefix is just a coding convention, and is not enforced by
|
||||
the language: there is nothing to prevent a user from directly accessing a
|
||||
property that is supposed to be private.
|
||||
|
||||
##Using Closures
|
||||
Another common technique is to define private properties as variables, and their
|
||||
getter and/or setter function as a closure over these variables:
|
||||
|
||||
function Point(_x, _y) {
|
||||
this.getX = function () {
|
||||
return _x;
|
||||
};
|
||||
|
||||
this.setX = function (x) {
|
||||
_x = x;
|
||||
};
|
||||
|
||||
this.getY = function () {
|
||||
return _y;
|
||||
};
|
||||
|
||||
this.setY = function (y) {
|
||||
_y = y;
|
||||
};
|
||||
}
|
||||
|
||||
Note that this technique requires member functions that need access to private
|
||||
properties to be defined on the object itself, instead of its prototype. This is
|
||||
slightly less efficient, but this is probably acceptable.
|
||||
|
||||
The advantage of this technique is that it offers more protection: there is no
|
||||
way for the user to access a private property except by using its getter and/or
|
||||
setter function. However, the use of closures makes private properties too
|
||||
restrictive: since there is no way to access variables in one closure from
|
||||
within another closure, there is no way for objects of the same class to access
|
||||
each other's private properties.
|
||||
|
||||
##Using WeakMaps
|
||||
|
||||
The techniques we've seen so far ar either not restrictive enough (prefixes) or
|
||||
too restrictive (closures). Until recently, a technique that solves both these
|
||||
problems didn't exist. That changed with the introduction of WeakMaps. WeakMaps
|
||||
were introduced to JavaScript in ES6, and have recently been implemented in
|
||||
SpiderMonkey. Before we explain how WeakMaps work, let's take a look at how
|
||||
ordinary objects can be used as hash maps, by creating a simple image cache:
|
||||
|
||||
let images = {};
|
||||
|
||||
function getImage(name) {
|
||||
let image = images[name];
|
||||
if (!image) {
|
||||
image = loadImage(name);
|
||||
images[name] = image;
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
Now suppose we want to associate a thumbnail with each image. Moreover, we want
|
||||
to create each thumbnail lazily, when it is first required:
|
||||
|
||||
function getThumbnail(image) {
|
||||
let thumbnail = image._thumbnail;
|
||||
if (!thumbnail) {
|
||||
thumbnail = createThumbnail(image);
|
||||
image._thumbnail = thumbnail;
|
||||
}
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
This approach is straightforward, but relies on the use of prefixes. A better
|
||||
approach would be to store thumbnails in their own, separate hash map:
|
||||
|
||||
let thumbnails = {};
|
||||
|
||||
function getThumbnail(image) {
|
||||
let thumbnail = thumbnails[image];
|
||||
if (!thumbnail) {
|
||||
thumbnail = createThumbnail(image);
|
||||
thumbnails[image] = thumbnail;
|
||||
}
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
There are two problems with the above approach. First, it's not possible to use
|
||||
objects as keys. When an object is used as a key, it is converted to a string
|
||||
using its toString method. To make the above code work, we'd have to associate a
|
||||
unique identifier with each image, and override its `toString` method. The
|
||||
second problem is more severe: the thumbnail cache maintains a strong reference
|
||||
to each thumbnail object, so they will never be freed, even when their
|
||||
corresponding image has gone out of scope. This is a memory leak waiting to
|
||||
happen.
|
||||
|
||||
The above two problems are exactly what WeakMaps were designed to solve. A
|
||||
WeakMap is very similar to an ordinary hash map, but differs from it in two
|
||||
crucial ways:
|
||||
|
||||
1. It can use ordinary objects as keys
|
||||
2. It does not maintain a strong reference to its values
|
||||
|
||||
To understand how WeakMaps are used in practice, let's rewrite the thumbnail
|
||||
cache using WeakMaps:
|
||||
|
||||
let thumbnails = new WeakMap();
|
||||
|
||||
function getThumbnail(image) {
|
||||
let thumbnail = thumbnails.get(image);
|
||||
if (!thumbnail) {
|
||||
thumbnail = createThumbnail(image);
|
||||
thumbnails.set(image, thumbnail);
|
||||
}
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
This version suffers of none of the problems we mentioned earlier. When a
|
||||
thumbnail's image goes out of scope, the WeakMap ensures that its entry in the
|
||||
thumbnail cache will eventually be garbage collected. As a final caveat: the
|
||||
image cache we created earlier suffers from the same problem, so for the above
|
||||
code to work properly, we'd have to rewrite the image cache using WeakMaps, too.
|
||||
|
||||
#From WeakMaps to Namespaces
|
||||
In the previous section we used a WeakMap to associate a private property with
|
||||
each object. Note that we need a separate WeakMap for each private property.
|
||||
This is cumbersome if the number of private properties becomes large. A better
|
||||
solution would be to store all private properties on a single object, called a
|
||||
namespace, and then store the namespace as a private property on the original
|
||||
object. Using namespaces, our earlier example can be rewritten as follows:
|
||||
|
||||
let map = new WeakMap();
|
||||
|
||||
let internal = function (object) {
|
||||
if (!map.has(object))
|
||||
map.set(object, {});
|
||||
return map.get(object);
|
||||
}
|
||||
|
||||
function Point(x, y) {
|
||||
internal(this).x = x;
|
||||
internal(this).y = y;
|
||||
}
|
||||
|
||||
Point.prototype.getX = function () {
|
||||
return internal(shape).x;
|
||||
};
|
||||
|
||||
Point.prototype.setX = function (x) {
|
||||
internal(shape).x = x;
|
||||
};
|
||||
|
||||
Point.prototype.getY = function () {
|
||||
return internal(shape).y;
|
||||
};
|
||||
|
||||
Point.prototype.setY = function () {
|
||||
internal(shape).y = y;
|
||||
};
|
||||
|
||||
The only way for a function to access the properties `x` and `y` is if it has a
|
||||
reference to an instance of `Point` and its `internal` namespace. By keeping the
|
||||
namespace hidden from all functions except members of `Point`, we have
|
||||
effectively implemented private properties. Moreover, because members of `Point`
|
||||
have a reference to the `internal` namespace, they can access private properties
|
||||
on other instances of `Point`.
|
||||
|
||||
##Namespaces in the Add-on SDK
|
||||
The Add-on SDK is built on top of XPCOM, the interface between JavaScript and
|
||||
C++ code. Since XPCOM allows the user to do virtually anything, security is very
|
||||
important. Among other things, we don't want add-ons to be able to access
|
||||
variables that are supposed to be private. The SDK uses namespaces internally to
|
||||
ensure this. As always with code that is heavily reused, the SDK defines a
|
||||
helper function to create namespaces. It is defined in the module
|
||||
"core/namespace", and it's usage is straightforward. To illustrate this, let's
|
||||
reimplement the class `Point` using namespaces:
|
||||
|
||||
const { ns } = require("./core/namespace");
|
||||
|
||||
var internal = ns();
|
||||
|
||||
function Point(x, y) {
|
||||
internal(this).x = x;
|
||||
internal(this).y = y;
|
||||
}
|
||||
|
||||
Point.prototype.getX = function () {
|
||||
return internal(shape).x;
|
||||
};
|
||||
|
||||
Point.prototype.setX = function (x) {
|
||||
internal(shape).x = x;
|
||||
};
|
||||
|
||||
Point.prototype.getY = function () {
|
||||
return internal(shape).y;
|
||||
};
|
||||
|
||||
Point.prototype.setY = function () {
|
||||
internal(shape).y = y;
|
||||
};
|
||||
|
||||
As a final note, the function `ns` returns a namespace that uses the namespace
|
||||
associated with the prototype of the object as its prototype.
|
|
@ -8,6 +8,59 @@ This page lists more theoretical in-depth articles about the SDK.
|
|||
|
||||
<hr>
|
||||
|
||||
<h2><a name="contributors-guide">Contributor's Guide</a></h2>
|
||||
|
||||
<table class="catalog">
|
||||
<colgroup>
|
||||
<col width="50%">
|
||||
<col width="50%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td>
|
||||
<h4><a href="dev-guide/guides/contributors-guide/getting-started.html">Getting Started</a></h4>
|
||||
Learn how to contribute to the SDK: getting the code, opening/taking a
|
||||
bug, filing a patch, getting reviews, and getting help.
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<h4><a href="dev-guide/guides/contributors-guide/private-properties.html">Private Properties</a></h4>
|
||||
Learn how private properties can be implemented in JavaScript using
|
||||
prefixes, closures, and WeakMaps, and how the SDK supports private
|
||||
properties by using namespaces (which are a generalization of WeakMaps).
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h4><a href="dev-guide/guides/contributors-guide/modules.html">Modules</a></h4>
|
||||
Learn about the module system used by the SDK (which is based on the
|
||||
CommonJS specification), how sandboxes and compartments can be used to
|
||||
improve security, and about the built-in SDK module loader, known as
|
||||
Cuddlefish.
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<h4><a href="dev-guide/guides/contributors-guide/content-processes.html">Content Processes</a></h4>
|
||||
The SDK was designed to work in an environment where the code to
|
||||
manipulate web content runs in a different process from the main add-on
|
||||
code. This article highlights the main features of that design.
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h4><a href="dev-guide/guides/contributors-guide/classes-and-inheritance.html">Classes and Inheritance</a></h4>
|
||||
Learn how classes and inheritance can be implemented in JavaScript, using
|
||||
constructors and prototypes, and about the helper functions provided by
|
||||
the SDK to simplify this.
|
||||
</td>
|
||||
|
||||
<td>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2><a name="sdk-infrastructure">SDK Infrastructure</a></h2>
|
||||
|
||||
<table class="catalog">
|
||||
|
|
|
@ -31,67 +31,21 @@ SDK-based add-ons.
|
|||
|
||||
## SDK Modules ##
|
||||
|
||||
All the modules supplied with the SDK can be found in the "lib"
|
||||
directory under the SDK root. The following diagram shows a reduced view
|
||||
of the SDK tree, with the "lib" directory highlighted.
|
||||
The modules supplied by the SDK are divided into two sorts:
|
||||
|
||||
<ul class="tree">
|
||||
<li>addon-sdk
|
||||
<ul>
|
||||
<li>app-extension</li>
|
||||
<li>bin</li>
|
||||
<li>data</li>
|
||||
<li>doc</li>
|
||||
<li>examples</li>
|
||||
<li class="highlight-tree-node">lib
|
||||
<ul>
|
||||
<li>sdk
|
||||
<ul>
|
||||
<li>core
|
||||
<ul>
|
||||
<li>heritage.js</li>
|
||||
<li>namespace.js</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>panel.js</li>
|
||||
<li>page-mod.js</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>toolkit
|
||||
<ul>
|
||||
<li>loader.js</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>python-lib</li>
|
||||
<li>test</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
All modules that are specifically intended for users of the
|
||||
SDK are stored under "lib" in the "sdk" directory.
|
||||
|
||||
[High-level modules](dev-guide/high-level-apis.html) like
|
||||
* [High-level modules](dev-guide/high-level-apis.html) like
|
||||
[`panel`](modules/sdk/panel.html) and
|
||||
[`page-mod`](modules/sdk/page-mod.html) are directly underneath
|
||||
the "sdk" directory.
|
||||
|
||||
[Low-level modules](dev-guide/low-level-apis.html) like
|
||||
[`page-mod`](modules/sdk/page-mod.html) provide relatively simple,
|
||||
stable APIs for the most common add-on development tasks.
|
||||
* [Low-level modules](dev-guide/low-level-apis.html) like
|
||||
[`heritage`](modules/sdk/core/heritage.html) and
|
||||
[`namespace`](modules/sdk/core/heritage.html) are grouped in subdirectories
|
||||
of "sdk" such as "core".
|
||||
[`namespace`](modules/sdk/core/heritage.html) provide more
|
||||
powerful functionality, and are typically less stable and more
|
||||
complex.
|
||||
|
||||
Very generic, platform-agnostic modules that are shared with other
|
||||
projects, such as [`loader`](modules/toolkit/loader.html), are stored
|
||||
in "toolkit".
|
||||
|
||||
<div style="clear:both"></div>
|
||||
|
||||
To use SDK modules, you can pass `require` a complete path from
|
||||
(but not including) the "lib" directory to the module you want to use.
|
||||
For high-level modules this is just `sdk/<module_name>`, and for low-level
|
||||
To use SDK modules, you can pass `require()` a complete path, starting with
|
||||
"sdk", to the module you want to use. For high-level modules this is just
|
||||
`sdk/<module_name>`, and for low-level
|
||||
modules it is `sdk/<path_to_module>/<module_name>`:
|
||||
|
||||
// load the high-level "tabs" module
|
||||
|
@ -100,13 +54,19 @@ modules it is `sdk/<path_to_module>/<module_name>`:
|
|||
// load the low-level "uuid" module
|
||||
var uuid = require('sdk/util/uuid');
|
||||
|
||||
For high-level modules only, you can also pass just the name of the module:
|
||||
The path to specify for a low-level module is given along with the module
|
||||
name itself in the title of the module's documentation page (for example,
|
||||
[system/environment](modules/sdk/system/environment.html)).
|
||||
|
||||
var tabs = require("tabs");
|
||||
|
||||
However, this is ambiguous, as it could also refer to a local module in your
|
||||
add-on named `tabs`. For this reason it is better to use the full path from
|
||||
"lib".
|
||||
Although the [SDK repository in GitHub](https://github.com/mozilla/addon-sdk)
|
||||
includes copies of these modules, they are built into Firefox and by
|
||||
default, when you run or build an add-on using
|
||||
[`cfx run`](dev-guide/cfx-tool.html#cfx-run)
|
||||
or [`cfx xpi`](dev-guide/cfx-tool.html#cfx-xpi), it is the versions of
|
||||
the modules in Firefox that are used. If you need to use a different version
|
||||
of the modules, you can do this by checking out the version of the SDK
|
||||
that you need and passing the `-o` or
|
||||
`--overload-modules` option to `cfx run` or `cfx xpi`.
|
||||
|
||||
## Local Modules ##
|
||||
|
||||
|
|
|
@ -77,6 +77,27 @@ Learn about common development techniques, such as
|
|||
<col width="50%">
|
||||
<col width="50%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td>
|
||||
<h4><a href="dev-guide/guides/index.html#contributors-guide">Contributor's Guide</a></h4>
|
||||
Learn
|
||||
<a href="dev-guide/guides/contributors-guide/getting-started.html">how to start contributing</a> to the SDK,
|
||||
and about the most important idioms used in the SDK code, such as
|
||||
<a href="dev-guide/guides/contributors-guide/modules.html">modules</a>,
|
||||
<a href="dev-guide/guides/contributors-guide/classes-and-inheritance.html">classes and inheritance</a>,
|
||||
<a href="dev-guide/guides/contributors-guide/private-properties.html">private properties</a>, and
|
||||
<a href="dev-guide/guides/contributors-guide/content-processes.html">content processes</a>.
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<h4><a href="dev-guide/guides/index.html#sdk-idioms">SDK idioms</a></h4>
|
||||
The SDK's
|
||||
<a href="dev-guide/guides/events.html">event framework</a> and the
|
||||
<a href="dev-guide/guides/two-types-of-scripts.html">distinction between add-on scripts and content scripts</a>.
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<h4><a href="dev-guide/guides/index.html#sdk-infrastructure">SDK infrastructure</a></h4>
|
||||
|
@ -88,10 +109,11 @@ Learn about common development techniques, such as
|
|||
</td>
|
||||
|
||||
<td>
|
||||
<h4><a href="dev-guide/guides/index.html#sdk-idioms">SDK idioms</a></h4>
|
||||
The SDK's
|
||||
<a href="dev-guide/guides/events.html">event framework</a> and the
|
||||
<a href="dev-guide/guides/two-types-of-scripts.html">distinction between add-on scripts and content scripts</a>.
|
||||
<h4><a href="dev-guide/guides/index.html#xul-migration">XUL migration</a></h4>
|
||||
A guide to <a href="dev-guide/guides/xul-migration.html">porting XUL add-ons to the SDK</a>.
|
||||
This guide includes a
|
||||
<a href="dev-guide/guides/sdk-vs-xul.html">comparison of the two toolsets</a> and a
|
||||
<a href="dev-guide/guides/library-detector.html">worked example</a> of porting a XUL add-on.
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
@ -106,11 +128,6 @@ Learn about common development techniques, such as
|
|||
</td>
|
||||
|
||||
<td>
|
||||
<h4><a href="dev-guide/guides/index.html#xul-migration">XUL migration</a></h4>
|
||||
A guide to <a href="dev-guide/guides/xul-migration.html">porting XUL add-ons to the SDK</a>.
|
||||
This guide includes a
|
||||
<a href="dev-guide/guides/sdk-vs-xul.html">comparison of the two toolsets</a> and a
|
||||
<a href="dev-guide/guides/library-detector.html">worked example</a> of porting a XUL add-on.
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
|
|
@ -168,3 +168,24 @@ add-on.
|
|||
|
||||
Now you have the basic `cfx` commands, you can try out the
|
||||
[SDK's features](dev-guide/tutorials/index.html).
|
||||
|
||||
## Overriding the Built-in Modules ##
|
||||
|
||||
The SDK modules you use to implement your add-on are built into Firefox.
|
||||
When you run or package an add-on using `cfx run` or `cfx xpi`, the add-on
|
||||
will use the versions of the modules in the version of Firefox that hosts
|
||||
it.
|
||||
|
||||
As an add-on developer, this is usually what you want. But if you're
|
||||
developing the SDK modules themselves, of course it won't work at all.
|
||||
In this case it's assumed that you will have checked out the SDK from
|
||||
its [GitHub repo](https://github.com/mozilla/addon-sdk) and will have
|
||||
run [`source/activate`](dev-guide/tutorials/installation.html) from
|
||||
the root of your checkout.
|
||||
|
||||
Then when you invoke `cfx run` or `cfx xpi`, you pass the `"-o"` option:
|
||||
|
||||
<pre>cfx run -o</pre>
|
||||
|
||||
This instructs cfx to use the local copies of the SDK modules, not the
|
||||
ones in Firefox.
|
||||
|
|
|
@ -32,20 +32,128 @@ So you can use the `indexed-db` module to access the same API:
|
|||
console.log("success");
|
||||
};
|
||||
|
||||
This module also exports all the other objects that implement
|
||||
the IndexedDB API, listed below under
|
||||
[API Reference](modules/sdk/indexed-db.html#API Reference).
|
||||
Most of the objects that implement the IndexedDB API, such as
|
||||
[IDBTransaction](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBTransaction),
|
||||
[IDBOpenDBRequest](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBOpenDBRequest),
|
||||
and [IDBObjectStore](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBObjectStore),
|
||||
are accessible through the indexedDB object itself.
|
||||
|
||||
The API exposed by `indexed-db` is almost identical to the DOM IndexedDB API,
|
||||
so we haven't repeated its documentation here, but refer you to the
|
||||
[IndexedDB API documentation](https://developer.mozilla.org/en-US/docs/IndexedDB)
|
||||
for all the details.
|
||||
|
||||
The database created will be unique and private per addon, and is not linked to any website database. The module cannot be used to interact with a given website database. See [bug 778197](https://bugzilla.mozilla.org/show_bug.cgi?id=779197) and [bug 786688](https://bugzilla.mozilla.org/show_bug.cgi?id=786688).
|
||||
The database created will be unique and private per add-on, and is not linked
|
||||
to any website database. The module cannot be used to interact with a given
|
||||
website database. See
|
||||
[bug 778197](https://bugzilla.mozilla.org/show_bug.cgi?id=779197) and
|
||||
[bug 786688](https://bugzilla.mozilla.org/show_bug.cgi?id=786688).
|
||||
|
||||
## Example of Usage
|
||||
## Example
|
||||
|
||||
[Promise-based example using indexedDB for record storage](https://github.com/gregglind/micropilot/blob/ec65446d611a65b0646be1806359c463193d5a91/lib/micropilot.js#L80-L198).
|
||||
Here's a complete add-on that adds two widgets to the browser: the widget labeled
|
||||
"Add" add the title of the current tab to a database, while the widget labeled
|
||||
"List" lists all the titles in the database.
|
||||
|
||||
The add-on implements helper functions `open()`, `addItem()` and `getItems()`
|
||||
to open the database, add a new item to the database, and get all items in the
|
||||
database.
|
||||
|
||||
var { indexedDB, IDBKeyRange } = require('sdk/indexed-db');
|
||||
var widgets = require("sdk/widget");
|
||||
|
||||
var database = {};
|
||||
|
||||
database.onerror = function(e) {
|
||||
console.error(e.value)
|
||||
}
|
||||
|
||||
function open(version) {
|
||||
var request = indexedDB.open("stuff", version);
|
||||
|
||||
request.onupgradeneeded = function(e) {
|
||||
var db = e.target.result;
|
||||
e.target.transaction.onerror = database.onerror;
|
||||
|
||||
if(db.objectStoreNames.contains("items")) {
|
||||
db.deleteObjectStore("items");
|
||||
}
|
||||
|
||||
var store = db.createObjectStore("items",
|
||||
{keyPath: "time"});
|
||||
};
|
||||
|
||||
request.onsuccess = function(e) {
|
||||
database.db = e.target.result;
|
||||
};
|
||||
|
||||
request.onerror = database.onerror;
|
||||
};
|
||||
|
||||
function addItem(name) {
|
||||
var db = database.db;
|
||||
var trans = db.transaction(["items"], "readwrite");
|
||||
var store = trans.objectStore("items");
|
||||
var time = new Date().getTime();
|
||||
var request = store.put({
|
||||
"name": name,
|
||||
"time": time
|
||||
});
|
||||
|
||||
request.onerror = database.onerror;
|
||||
};
|
||||
|
||||
function getItems(callback) {
|
||||
var cb = callback;
|
||||
var db = database.db;
|
||||
var trans = db.transaction(["items"], "readwrite");
|
||||
var store = trans.objectStore("items");
|
||||
var items = new Array();
|
||||
|
||||
trans.oncomplete = function() {
|
||||
cb(items);
|
||||
}
|
||||
|
||||
var keyRange = IDBKeyRange.lowerBound(0);
|
||||
var cursorRequest = store.openCursor(keyRange);
|
||||
|
||||
cursorRequest.onsuccess = function(e) {
|
||||
var result = e.target.result;
|
||||
if(!!result == false)
|
||||
return;
|
||||
|
||||
items.push(result.value.name);
|
||||
result.continue();
|
||||
};
|
||||
|
||||
cursorRequest.onerror = database.onerror;
|
||||
};
|
||||
|
||||
function listItems(itemList) {
|
||||
console.log(itemList);
|
||||
}
|
||||
|
||||
open("1");
|
||||
|
||||
widgets.Widget({
|
||||
id: "add-it",
|
||||
width: 50,
|
||||
label: "Add",
|
||||
content: "Add",
|
||||
onClick: function() {
|
||||
addItem(require("sdk/tabs").activeTab.title);
|
||||
}
|
||||
});
|
||||
|
||||
widgets.Widget({
|
||||
id: "list-them",
|
||||
width: 50,
|
||||
label: "List",
|
||||
content: "List",
|
||||
onClick: function() {
|
||||
getItems(listItems);
|
||||
}
|
||||
});
|
||||
|
||||
<api name="indexedDB">
|
||||
@property {object}
|
||||
|
@ -61,71 +169,6 @@ Defines a range of keys.
|
|||
See the [IDBKeyRange documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBKeyRange).
|
||||
</api>
|
||||
|
||||
<api name="IDBCursor">
|
||||
@property {object}
|
||||
|
||||
For traversing or iterating records in a database.
|
||||
See the [IDBCursor documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBCursor).
|
||||
|
||||
</api>
|
||||
|
||||
<api name="IDBTransaction">
|
||||
@property {object}
|
||||
|
||||
Represents a database transaction.
|
||||
See the [IDBTransaction documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBTransaction).
|
||||
</api>
|
||||
|
||||
<api name="IDBOpenDBRequest">
|
||||
@property {object}
|
||||
|
||||
Represents an asynchronous request to open a database.
|
||||
See the [IDBOpenDBRequest documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBOpenDBRequest).
|
||||
</api>
|
||||
|
||||
<api name="IDBVersionChangeEvent">
|
||||
@property {object}
|
||||
|
||||
Event indicating that the database version has changed.
|
||||
See the [IDBVersionChangeEvent documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBVersionChangeEvent).
|
||||
</api>
|
||||
|
||||
<api name="IDBDatabase">
|
||||
@property {object}
|
||||
|
||||
Represents a connection to a database.
|
||||
See the [IDBDatabase documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBDatabase).
|
||||
</api>
|
||||
|
||||
<api name="IDBFactory">
|
||||
@property {object}
|
||||
|
||||
Enables you to create, open, and delete databases.
|
||||
See the [IDBFactory documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBFactory).
|
||||
</api>
|
||||
|
||||
<api name="IDBIndex">
|
||||
@property {object}
|
||||
|
||||
Provides access to a database index.
|
||||
See the [IDBIndex documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBIndex).
|
||||
</api>
|
||||
|
||||
<api name="IDBObjectStore">
|
||||
@property {object}
|
||||
|
||||
Represents an object store in a database.
|
||||
See the [IDBObjectStore documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBObjectStore).
|
||||
</api>
|
||||
|
||||
<api name="IDBRequest">
|
||||
@property {object}
|
||||
|
||||
Provides access to the results of asynchronous requests to databases
|
||||
and database objects.
|
||||
See the [IDBRequest documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBRequest).
|
||||
</api>
|
||||
|
||||
<api name="DOMException">
|
||||
@property {object}
|
||||
|
||||
|
|
|
@ -0,0 +1,450 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.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 `places/bookmarks` module provides functions for creating, modifying and searching bookmark items. It exports:
|
||||
|
||||
* three constructors: [Bookmark](modules/sdk/places/bookmarks.html#Bookmark), [Group](modules/sdk/places/bookmarks.html#Group), and [Separator](modules/sdk/places/bookmarks.html#Separator), corresponding to the types of objects, referred to as **bookmark items**, in the Bookmarks database in Firefox
|
||||
* two additional functions, [`save()`](modules/sdk/places/bookmarks.html#save(bookmarkItems%2C%20options)) to create, update, and remove bookmark items, and [`search()`](modules/sdk/places/bookmarks.html#search(queries%2C%20options)) to retrieve the bookmark items that match a particular set of criteria.
|
||||
|
||||
`save()` and `search()` are both asynchronous functions: they synchronously return a [`PlacesEmitter`](modules/sdk/places/bookmarks.html#PlacesEmitter) object, which then asynchronously emits events as the operation progresses and completes.
|
||||
|
||||
Each retrieved bookmark item represents only a snapshot of state at a specific time. The module does not automatically sync up a `Bookmark` instance with ongoing changes to that item in the database from the same add-on, other add-ons, or the user.
|
||||
|
||||
## Examples
|
||||
|
||||
### Creating a new bookmark
|
||||
|
||||
let { Bookmark, save } = require("sdk/places/bookmarks");
|
||||
|
||||
// Create a new bookmark instance, unsaved
|
||||
let bookmark = Bookmark({ title: "Mozilla", url: "http://mozila.org" });
|
||||
|
||||
// Attempt to save the bookmark instance to the Bookmarks database
|
||||
// and store the emitter
|
||||
let emitter = save(bookmark);
|
||||
|
||||
// Listen for events
|
||||
emitter.on("data", function (saved, inputItem) {
|
||||
// on a "data" event, an item has been updated, passing in the
|
||||
// latest snapshot from the server as `saved` (with properties
|
||||
// such as `updated` and `id`), as well as the initial input
|
||||
// item as `inputItem`
|
||||
console.log(saved.title === inputItem.title); // true
|
||||
console.log(saved !== inputItem); // true
|
||||
console.log(inputItem === bookmark); // true
|
||||
}).on("end", function (savedItems, inputItems) {
|
||||
// Similar to "data" events, except "end" is an aggregate of
|
||||
// all progress events, with ordered arrays as `savedItems`
|
||||
// and `inputItems`
|
||||
});
|
||||
|
||||
### Creating several bookmarks with a new group
|
||||
|
||||
let { Bookmark, Group, save } = require("sdk/places/bookmarks");
|
||||
|
||||
let group = Group({ title: "Guitars" });
|
||||
let bookmarks = [
|
||||
Bookmark({ title: "Ran", url: "http://ranguitars.com", group: group }),
|
||||
Bookmark({ title: "Ibanez", url: "http://ibanez.com", group: group }),
|
||||
Bookmark({ title: "ESP", url: "http://espguitars.com", group: group })
|
||||
];
|
||||
|
||||
// Save `bookmarks` array -- notice we don't have `group` in the array,
|
||||
// although it needs to be saved since all bookmarks are children
|
||||
// of `group`. This will be saved implicitly.
|
||||
|
||||
save(bookmarks).on("data", function (saved, input) {
|
||||
// A data event is called once for each item saved, as well
|
||||
// as implicit items, like `group`
|
||||
console.log(input === group || ~bookmarks.indexOf(input)); // true
|
||||
}).on("end", function (saves, inputs) {
|
||||
// like the previous example, the "end" event returns an
|
||||
// array of all of our updated saves. Only explicitly saved
|
||||
// items are returned in this array -- the `group` won't be
|
||||
// present here.
|
||||
console.log(saves[0].title); // "Ran"
|
||||
console.log(saves[2].group.title); // "Guitars"
|
||||
});
|
||||
|
||||
### Searching for bookmarks
|
||||
|
||||
Bookmarks can be queried with the [`search()`](modules/sdk/places/bookmarks.html#search(queries%2C%20options)) function, which accepts a query object or an array of query objects, as well as a query options object. Query properties are AND'd together within a single query object, but are OR'd together across multiple query objects.
|
||||
|
||||
let { search, UNSORTED } = require("sdk/places/bookmarks");
|
||||
|
||||
// Simple query with one object
|
||||
search(
|
||||
{ query: "firefox" },
|
||||
{ sort: "title" }
|
||||
).on(end, function (results) {
|
||||
// results matching any bookmark that has "firefox"
|
||||
// in its URL, title or tag, sorted by title
|
||||
});
|
||||
|
||||
// Multiple queries are OR'd together
|
||||
search(
|
||||
[{ query: "firefox" }, { group: UNSORTED, tags: ["mozilla"] }],
|
||||
{ sort: "title" }
|
||||
).on("end", function (results) {
|
||||
// Our first query is the same as the simple query above;
|
||||
// all of those results are also returned here. Since multiple
|
||||
// queries are OR'd together, we also get bookmarks that
|
||||
// match the second query. The second query's properties
|
||||
// are AND'd together, so results that are in the platform's unsorted
|
||||
// bookmarks folder, AND are also tagged with 'mozilla', get returned
|
||||
// as well in this query
|
||||
});
|
||||
|
||||
<api name="Bookmark">
|
||||
@class
|
||||
<api name="Bookmark">
|
||||
@constructor
|
||||
|
||||
Creates an unsaved bookmark instance.
|
||||
@param options {object}
|
||||
Options for the bookmark, with the following parameters:
|
||||
@prop title {string}
|
||||
The title for the bookmark. Required.
|
||||
@prop url {string}
|
||||
The URL for the bookmark. Required.
|
||||
@prop [group] {Group}
|
||||
The parent group that the bookmark lives under. Defaults to the [Bookmarks.UNSORTED](modules/sdk/places/bookmarks.html#UNSORTED) group.
|
||||
@prop [index] {number}
|
||||
The index of the bookmark within its group. Last item within the group by default.
|
||||
@prop [tags] {set}
|
||||
A set of tags to be applied to the bookmark.
|
||||
</api>
|
||||
|
||||
<api name="title">
|
||||
@property {string}
|
||||
The bookmark's title.
|
||||
</api>
|
||||
|
||||
<api name="url">
|
||||
@property {string}
|
||||
The bookmark's URL.
|
||||
</api>
|
||||
|
||||
<api name="group">
|
||||
@property {Group}
|
||||
The group instance that the bookmark lives under.
|
||||
</api>
|
||||
|
||||
<api name="index">
|
||||
@property {number}
|
||||
The index of the bookmark within its group.
|
||||
</api>
|
||||
|
||||
<api name="updated">
|
||||
@property {number}
|
||||
A Unix timestamp indicating when the bookmark was last updated on the platform.
|
||||
</api>
|
||||
|
||||
<api name="tags">
|
||||
@property {set}
|
||||
A [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) of tags that the bookmark is tagged with.
|
||||
</api>
|
||||
</api>
|
||||
|
||||
<api name="Group">
|
||||
@class
|
||||
<api name="Group">
|
||||
@constructor
|
||||
|
||||
Creates an unsaved bookmark group instance.
|
||||
@param options {object}
|
||||
Options for the bookmark group, with the following parameters:
|
||||
@prop title {string}
|
||||
The title for the group. Required.
|
||||
@prop [group] {Group}
|
||||
The parent group that the bookmark group lives under. Defaults to the [Bookmarks.UNSORTED](modules/sdk/places/bookmarks.html#UNSORTED) group.
|
||||
@prop [index] {number}
|
||||
The index of the bookmark group within its parent group. Last item within the group by default.
|
||||
</api>
|
||||
|
||||
<api name="title">
|
||||
@property {string}
|
||||
The bookmark group's title.
|
||||
</api>
|
||||
|
||||
<api name="group">
|
||||
@property {Group}
|
||||
The group instance that the bookmark group lives under.
|
||||
</api>
|
||||
|
||||
<api name="index">
|
||||
@property {number}
|
||||
The index of the bookmark group within its group.
|
||||
</api>
|
||||
|
||||
<api name="updated">
|
||||
@property {number}
|
||||
A Unix timestamp indicating when the bookmark was last updated on the platform.
|
||||
</api>
|
||||
</api>
|
||||
|
||||
<api name="Separator">
|
||||
@class
|
||||
<api name="Separator">
|
||||
@constructor
|
||||
|
||||
Creates an unsaved bookmark separator instance.
|
||||
@param options {object}
|
||||
Options for the bookmark group, with the following parameters:
|
||||
@prop [group] {Group}
|
||||
The parent group that the bookmark group lives under. Defaults to the [Bookmarks.UNSORTED](modules/sdk/places/bookmarks.html#UNSORTED) group.
|
||||
@prop [index] {number}
|
||||
The index of the bookmark group within its parent group. Last item within the group by default.
|
||||
</api>
|
||||
|
||||
<api name="group">
|
||||
@property {Group}
|
||||
The group instance that the bookmark group lives under.
|
||||
</api>
|
||||
|
||||
<api name="index">
|
||||
@property {number}
|
||||
The index of the bookmark group within its group.
|
||||
</api>
|
||||
|
||||
<api name="updated">
|
||||
@property {number}
|
||||
A Unix timestamp indicating when the bookmark was last updated on the platform.
|
||||
</api>
|
||||
</api>
|
||||
|
||||
<api name="save">
|
||||
@function
|
||||
|
||||
Creating, saving, and deleting bookmarks are all done with the `save()` function. This function takes in any of:
|
||||
|
||||
* a bookmark item (Bookmark, Group, Separator)
|
||||
* a duck-typed object (the relative properties for a bookmark item, in addition to a `type` property of `'bookmark'`, `'group'`, or `'separator'`)
|
||||
* an array of bookmark items.
|
||||
|
||||
All of the items passed in are pushed to the platform and are either created, updated or deleted.
|
||||
|
||||
* adding items: if passing in freshly instantiated bookmark items or a duck-typed object, the item is created on the platform.
|
||||
* updating items: for an item referenced from a previous `save()` or from the result of a `search()` query, changing the properties and calling `save()` will update the item on the server.
|
||||
* deleting items: to delete a bookmark item, pass in a bookmark item with a property `remove` set to `true`.
|
||||
|
||||
The function returns a [`PlacesEmitter`](modules/sdk/places/bookmarks.html#PlacesEmitter) that emits a `data` event for each item as it is saved, and an `end` event when all items have been saved.
|
||||
|
||||
let { Bookmark, Group } = require("sdk/places/bookmarks");
|
||||
|
||||
let myGroup = Group({ title: "My Group" });
|
||||
let myBookmark = Bookmark({
|
||||
title: "Moz",
|
||||
url: "http://mozilla.com",
|
||||
group: myGroup
|
||||
});
|
||||
|
||||
save(myBookmark).on("data", function (item, inputItem) {
|
||||
// The `data` event returns the latest snapshot from the
|
||||
// host, so this is a new instance of the bookmark item,
|
||||
// where `item !== myBookmark`. To match it with the input item,
|
||||
// use the second argument, so `inputItem === myBookmark`
|
||||
|
||||
// All explicitly saved items have data events called, as
|
||||
// well as implicitly saved items. In this case,
|
||||
// `myGroup` has to be saved before `myBookmark`, since
|
||||
// `myBookmark` is a child of `myGroup`. `myGroup` will
|
||||
// also have a `data` event called for it.
|
||||
}).on("end", function (items, inputItems) {
|
||||
// The `end` event returns all items that are explicitly
|
||||
// saved. So `myGroup` will not be in this array,
|
||||
// but `myBookmark` will be.
|
||||
// `inputItems` matches the initial input as an array,
|
||||
// so `inputItems[0] === myBookmark`
|
||||
});
|
||||
|
||||
// Saving multiple bookmarks, as duck-types in this case
|
||||
|
||||
let bookmarks = [
|
||||
{ title: "mozilla", url: "http://mozilla.org", type: "bookmark" },
|
||||
{ title: "firefox", url: "http://firefox.com", type: "bookmark" },
|
||||
{ title: "twitter", url: "http://twitter.com", type: "bookmark" }
|
||||
];
|
||||
|
||||
save(bookmarks).on("data", function (item, inputItem) {
|
||||
// Each item in `bookmarks` has its own `data` event
|
||||
}).on("end", function (results, inputResults) {
|
||||
// `results` is an array of items saved in the same order
|
||||
// as they were passed in.
|
||||
});
|
||||
|
||||
@param bookmarkItems {bookmark|group|separator|array}
|
||||
A bookmark item ([Bookmark](modules/sdk/places/bookmarks.html#Bookmark), [Group](modules/sdk/places/bookmarks.html#Group), [Separator](modules/sdk/places/bookmarks.html#Separator)), or an array of bookmark items to be saved.
|
||||
|
||||
@param [options] {object}
|
||||
An optional options object that takes the following properties:
|
||||
@prop [resolve] {function}
|
||||
A resolution function that is invoked during an attempt to save
|
||||
a bookmark item that is not derived from the latest state from
|
||||
the platform. Invoked with two arguments, `mine` and `platform`, where
|
||||
`mine` is the item that is being saved, and `platform` is the
|
||||
current state of the item on the item. The object returned from
|
||||
this function is what is saved on the platform. By default, all changes
|
||||
on an outdated bookmark item overwrite the platform's bookmark item.
|
||||
|
||||
@returns {PlacesEmitter}
|
||||
Returns a [PlacesEmitter](modules/sdk/places/bookmarks.html#PlacesEmitter).
|
||||
</api>
|
||||
|
||||
<api name="remove">
|
||||
@function
|
||||
|
||||
A helper function that takes in a bookmark item, or an `Array` of several bookmark items, and sets each item's `remove` property to true. This does not remove the bookmark item from the database: it must be subsequently saved.
|
||||
|
||||
let { search, save, remove } = require("sdk/places/bookmarks");
|
||||
|
||||
search({ tags: ["php"] }).on("end", function (results) {
|
||||
// The search returns us all bookmark items that are
|
||||
// tagged with `"php"`.
|
||||
|
||||
// We then pass `results` into the remove function to mark
|
||||
// all items to be removed, which returns the new modified `Array`
|
||||
// of items, which is passed into save.
|
||||
save(remove(results)).on("end", function (results) {
|
||||
// items tagged with `"php"` are now removed!
|
||||
});
|
||||
})
|
||||
|
||||
@param items {Bookmark|Group|Separator|array}
|
||||
A bookmark item, or `Array` of bookmark items to be transformed to set their `remove` property to `true`.
|
||||
|
||||
@returns {array}
|
||||
An array of the transformed bookmark items.
|
||||
</api>
|
||||
|
||||
<api name="search">
|
||||
@function
|
||||
|
||||
Queries can be performed on bookmark items by passing in one or more query objects, each of which is given one or more properties.
|
||||
|
||||
Within each query object, the properties are AND'd together: so only objects matching all properties are retrieved. Across query objects, the results are OR'd together, meaning that if an item matches any of the query objects, it will be retrieved.
|
||||
|
||||
For example, suppose we called `search()` with two query objects:
|
||||
|
||||
<pre>[{ url: "mozilla.org", tags: ["mobile"]},
|
||||
{ tags: ["firefox-os"]}]</pre>
|
||||
|
||||
This will return:
|
||||
|
||||
* all bookmark items from mozilla.org that are also tagged "mobile"
|
||||
* all bookmark items that are tagged "firefox-os"
|
||||
|
||||
An `options` object may be used to determine overall settings such as sort order and how many objects should be returned.
|
||||
|
||||
@param queries {object|array}
|
||||
An `Object` representing a query, or an `Array` of `Objects` representing queries. Each query object can take several properties, which are queried against the bookmarks database. Each property is AND'd together, meaning that bookmarks must match each property within a query object. Multiple query objects are then OR'd together.
|
||||
@prop [group] {Group}
|
||||
Group instance that should be owners of the returned children bookmarks. If no `group` specified, all bookmarks are under the search space.
|
||||
@prop [tags] {set|array}
|
||||
Bookmarks with corresponding tags. These are AND'd together.
|
||||
@prop [url] {string}
|
||||
A string that matches bookmarks' URL. The following patterns are accepted:
|
||||
|
||||
`'*.mozilla.com'`: matches any URL with 'mozilla.com' as the host, accepting any subhost.
|
||||
|
||||
`'mozilla.com'`: matches any URL with 'mozilla.com' as the host.
|
||||
|
||||
`'http://mozilla.com'`: matches 'http://mozilla.com' exactly.
|
||||
|
||||
`'http://mozilla.com/*'`: matches any URL that starts with 'http://mozilla.com/'.
|
||||
@prop [query] {string}
|
||||
A string that matches bookmarks' URL, title and tags.
|
||||
|
||||
@param [options] {object}
|
||||
An `Object` with options for the search query.
|
||||
@prop [count] {number}
|
||||
The number of bookmark items to return. If left undefined, no limit is set.
|
||||
@prop [sort] {string}
|
||||
A string specifying how the results should be sorted. Possible options are `'title'`, `'date'`, `'url'`, `'visitCount'`, `'dateAdded'` and `'lastModified'`.
|
||||
@prop [descending] {boolean}
|
||||
A boolean specifying whether the results should be in descending order. By default, results are in ascending order.
|
||||
|
||||
</api>
|
||||
|
||||
<api name="PlacesEmitter">
|
||||
@class
|
||||
|
||||
The `PlacesEmitter` is not exported from the module, but returned from the `save` and `search` functions. The `PlacesEmitter` inherits from [`event/target`](modules/sdk/event/target.html), and emits `data`, `error`, and `end`.
|
||||
|
||||
`data` events are emitted for every individual operation (such as: each item saved, or each item found by a search query), whereas `end` events are emitted as the aggregate of an operation, passing an array of objects into the handler.
|
||||
|
||||
<api name="data">
|
||||
@event
|
||||
The `data` event is emitted when a bookmark item that was passed into the `save` method has been saved to the platform. This includes implicit saves that are dependencies of the explicit items saved. For example, when creating a new bookmark group with two bookmark items as its children, and explicitly saving the two bookmark children, the unsaved parent group will also emit a `data` event.
|
||||
|
||||
let { Bookmark, Group, save } = require("sdk/places/bookmarks");
|
||||
|
||||
let group = Group({ title: "my group" });
|
||||
let bookmarks = [
|
||||
Bookmark({ title: "mozilla", url: "http://mozilla.com", group: group }),
|
||||
Bookmark({ title: "w3", url: "http://w3.org", group: group })
|
||||
];
|
||||
|
||||
save(bookmarks).on("data", function (item) {
|
||||
// This function will be called three times:
|
||||
// once for each bookmark saved
|
||||
// once for the new group specified implicitly
|
||||
// as the parent of the two items
|
||||
});
|
||||
|
||||
The `data` event is also called for `search` requests, with each result being passed individually into its own `data` event.
|
||||
|
||||
let { search } = require("sdk/places/bookmarks");
|
||||
|
||||
search({ query: "firefox" }).on("data", function (item) {
|
||||
// each bookmark item that matches the query will
|
||||
// be called in this function
|
||||
});
|
||||
|
||||
@argument {Bookmark|Group|Separator}
|
||||
For the `save` function, this is the saved, latest snapshot of the bookmark item. For `search`, this is a snapshot of a bookmark returned from the search query.
|
||||
|
||||
@argument {Bookmark|Group|Separator|object}
|
||||
Only in `save` data events. The initial instance of the item that was used for the save request.
|
||||
</api>
|
||||
|
||||
<api name="error">
|
||||
@event
|
||||
The `error` event is emitted whenever a bookmark item's save could not be completed.
|
||||
|
||||
@argument {string}
|
||||
A string indicating the error that occurred.
|
||||
|
||||
@argument {Bookmark|Group|Separator|object}
|
||||
Only in `save` error events. The initial instance of the item that was used for the save request.
|
||||
</api>
|
||||
|
||||
<api name="end">
|
||||
@event
|
||||
The `end` event is called when all bookmark items and dependencies
|
||||
have been saved, or an aggregate of all items returned from a search query.
|
||||
|
||||
@argument {array}
|
||||
The array is an ordered list of the input bookmark items, replaced
|
||||
with their updated, latest snapshot instances (the first argument
|
||||
in the `data` handler), or in the case of an error, the initial instance
|
||||
of the item that was used for the save request
|
||||
(the second argument in the `data` or `error` handler).
|
||||
</api>
|
||||
</api>
|
||||
|
||||
<api name="MENU">
|
||||
@property {group}
|
||||
This is a constant, default [`Group`](modules/sdk/places/bookmarks.html#Group) on the Firefox platform, the **Bookmarks Menu**. It can be used in queries or specifying the parent of a bookmark item, but it cannot be modified.
|
||||
</api>
|
||||
|
||||
<api name="TOOLBAR">
|
||||
@property {group}
|
||||
This is a constant, default [`Group`](modules/sdk/places/bookmarks.html#Group) on the Firefox platform, the **Bookmarks Toolbar**. It can be used in queries or specifying the parent of a bookmark item, but it cannot be modified.
|
||||
</api>
|
||||
|
||||
<api name="UNSORTED">
|
||||
@property {group}
|
||||
This is a constant, default [`Group`](modules/sdk/places/bookmarks.html#Group) on the Firefox platform, the **Unsorted Bookmarks** group. It can be used in queries or specifying the parent of a bookmark item, but it cannot be modified.
|
||||
</api>
|
|
@ -0,0 +1,110 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
The `places/history` module provides a single function, [`search()`](modules/sdk/places/history.html#search(queries%2C%20options)), for querying the user's browsing history.
|
||||
|
||||
It synchronously returns a [`PlacesEmitter`](modules/sdk/places/history.html#PlacesEmitter) object which then asynchronously emits [`data`](modules/sdk/places/history.html#data) and [`end`](modules/sdk/places/history.html#end) or [`error`](modules/sdk/places/history.html#error) events that contain information about the state of the operation.
|
||||
|
||||
## Example
|
||||
|
||||
let { search } = require("sdk/places/history");
|
||||
|
||||
// Simple query
|
||||
search(
|
||||
{ url: "https://developers.mozilla.org/*" },
|
||||
{ sort: "visitCount" }
|
||||
).on("end", function (results) {
|
||||
// results is an array of objects containing
|
||||
// data about visits to any site on developers.mozilla.org
|
||||
// ordered by visit count
|
||||
});
|
||||
|
||||
// Complex query
|
||||
// The query objects are OR'd together
|
||||
// Let's say we want to retrieve all visits from before a week ago
|
||||
// with the query of 'ruby', but from last week onwards, we want
|
||||
// all results with 'javascript' in the URL or title.
|
||||
// We'd compose the query with the following options
|
||||
let lastWeek = Date.now - (1000*60*60*24*7);
|
||||
search(
|
||||
// First query looks for all entries before last week with 'ruby'
|
||||
[{ query: "ruby", to: lastWeek },
|
||||
// Second query searches all entries after last week with 'javascript'
|
||||
{ query: "javascript", from: lastWeek }],
|
||||
// We want to order chronologically by visit date
|
||||
{ sort: "date" }
|
||||
).on("end", function (results) {
|
||||
// results is an array of objects containing visit data,
|
||||
// sorted by visit date, with all entries from more than a week ago
|
||||
// that contain 'ruby', *in addition to* entries from this last week
|
||||
// that contain 'javascript'
|
||||
});
|
||||
|
||||
<api name="search">
|
||||
@function
|
||||
|
||||
Queries can be performed on history entries by passing in one or more query options. Each query option can take several properties, which are **AND**'d together to make one complete query. For additional queries within the query, passing more query options in will **OR** the total results. An `options` object may be specified to determine overall settings, like sorting and how many objects should be returned.
|
||||
|
||||
@param queries {object|array}
|
||||
An `Object` representing a query, or an `Array` of `Objects` representing queries. Each query object can take several properties, which are queried against the history database. Each property is **AND**'d together, meaning that bookmarks must match each property within a query object. Multiple query objects are then **OR**'d together.
|
||||
@prop [url] {string}
|
||||
A string that matches bookmarks' URL. The following patterns are accepted:
|
||||
|
||||
`'*.mozilla.com'`: matches any URL with 'mozilla.com' as the host, accepting any subhost.
|
||||
|
||||
`'mozilla.com'`: matches any URL with 'mozilla.com' as the host.
|
||||
|
||||
`'http://mozilla.com'`: matches 'http://mozilla.com' directlry.
|
||||
|
||||
`'http://mozilla.com/*'`: matches any URL that starts with 'http://mozilla.com/'.
|
||||
@prop [query] {string}
|
||||
A string that matches bookmarks' URL, or title.
|
||||
@prop [from] {number|date}
|
||||
Time relative from the [Unix epoch](http://en.wikipedia.org/wiki/Unix_time) that history results should be limited to occuring after. Can accept a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object, or milliseconds from the epoch. Default is to return all items since the epoch (all time).
|
||||
@prop [to] {number|date}
|
||||
Time relative from the [Unix epoch](http://en.wikipedia.org/wiki/Unix_time) that history results should be limited to occuring before. Can accept a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object, or milliseconds from the epoch. Default is the current time.
|
||||
|
||||
@param [options] {object}
|
||||
An `Object` with options for the search query.
|
||||
@prop [count] {number}
|
||||
The number of bookmark items to return. If left undefined, no limit is set.
|
||||
@prop [sort] {string}
|
||||
A string specifying how the results should be sorted. Possible options are `'title'`, `'date'`, `'url'`, `'visitCount'`, `'keyword'`, `'dateAdded'` and `'lastModified'`.
|
||||
@prop [descending] {boolean}
|
||||
A boolean specifying whether the results should be in descending order. By default, results are in ascending order.
|
||||
|
||||
</api>
|
||||
|
||||
|
||||
<api name="PlacesEmitter">
|
||||
@class
|
||||
|
||||
The `PlacesEmitter` is not exposed in the module, but returned from the `search` functions. The `PlacesEmitter` inherits from [`event/target`](modules/sdk/event/target.html), and emits `data`, `error`, and `end`. `data` events are emitted for every individual search result found, whereas `end` events are emitted as an aggregate of an entire search, passing in an array of all results into the handler.
|
||||
|
||||
<api name="data">
|
||||
@event
|
||||
The `data` event is emitted for every item returned from a search.
|
||||
|
||||
@argument {Object}
|
||||
This is an object representing a history entry. Contains `url`, `time`, `accessCount` and `title` of the entry.
|
||||
</api>
|
||||
|
||||
<api name="error">
|
||||
@event
|
||||
The `error` event is emitted whenever a search could not be completed.
|
||||
|
||||
@argument {string}
|
||||
A string indicating the error that occurred.
|
||||
</api>
|
||||
|
||||
<api name="end">
|
||||
@event
|
||||
The `end` event is called when all search results have returned.
|
||||
|
||||
@argument {array}
|
||||
The value passed into the handler is an array of all entries found in the
|
||||
history search. Each entry is an object containing the properties
|
||||
`url`, `time`, `accessCount` and `title`.
|
||||
</api>
|
||||
</api>
|
|
@ -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/. -->
|
||||
|
||||
The `request` module lets you make simple yet powerful network requests.
|
||||
The `request` module lets you make simple yet powerful network requests. For more advanced usage, check out the [net/xhr](modules/sdk/net/xhr.html) module, based on the browser's [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) object.
|
||||
|
||||
<api name="Request">
|
||||
@class
|
||||
|
|
|
@ -100,6 +100,23 @@ These are attributes that all settings *may* have:
|
|||
this may be an integer, string, or boolean value.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><code>hidden</code></td>
|
||||
<td><p>A boolean value which, if present and set to <code>true</code>,
|
||||
means that the setting won't appear in the Add-ons Manager interface,
|
||||
so users of your add-on won't be able to see or alter it.</p>
|
||||
<pre>
|
||||
{
|
||||
"name": "myHiddenInteger",
|
||||
"type": "integer",
|
||||
"title": "How Many?",
|
||||
"hidden": true
|
||||
}</pre>
|
||||
<p>Your add-on's code will still be able to access and modify it,
|
||||
just like any other preference you define.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
### Setting-Specific Attributes ###
|
||||
|
|
|
@ -369,7 +369,7 @@ const WorkerSandbox = EventEmitter.compose({
|
|||
/**
|
||||
* Message-passing facility for communication between code running
|
||||
* in the content and add-on process.
|
||||
* @see https://jetpack.mozillalabs.com/sdk/latest/docs/#module/api-utils/content/worker
|
||||
* @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html
|
||||
*/
|
||||
const Worker = EventEmitter.compose({
|
||||
on: Trait.required,
|
||||
|
|
|
@ -5,12 +5,16 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "deprecated"
|
||||
};
|
||||
|
||||
const memory = require("./memory");
|
||||
|
||||
const { merge } = require("../util/object");
|
||||
const { union } = require("../util/array");
|
||||
const { isNil } = require("../lang/type");
|
||||
|
||||
// The possible return values of getTypeOf.
|
||||
const VALID_TYPES = [
|
||||
"array",
|
||||
|
@ -23,6 +27,8 @@ const VALID_TYPES = [
|
|||
"undefined",
|
||||
];
|
||||
|
||||
const { isArray } = Array;
|
||||
|
||||
/**
|
||||
* Returns a function C that creates instances of privateCtor. C may be called
|
||||
* with or without the new keyword. The prototype of each instance returned
|
||||
|
@ -86,6 +92,7 @@ exports.validateOptions = function validateOptions(options, requirements) {
|
|||
let validatedOptions = {};
|
||||
|
||||
for (let key in requirements) {
|
||||
let isOptional = false;
|
||||
let mapThrew = false;
|
||||
let req = requirements[key];
|
||||
let [optsVal, keyInOpts] = (key in options) ?
|
||||
|
@ -103,17 +110,27 @@ exports.validateOptions = function validateOptions(options, requirements) {
|
|||
}
|
||||
}
|
||||
if (req.is) {
|
||||
// Sanity check the caller's type names.
|
||||
req.is.forEach(function (typ) {
|
||||
if (VALID_TYPES.indexOf(typ) < 0) {
|
||||
let msg = 'Internal error: invalid requirement type "' + typ + '".';
|
||||
throw new Error(msg);
|
||||
}
|
||||
});
|
||||
if (req.is.indexOf(getTypeOf(optsVal)) < 0)
|
||||
throw new RequirementError(key, req);
|
||||
let types = req.is;
|
||||
|
||||
if (!isArray(types) && isArray(types.is))
|
||||
types = types.is;
|
||||
|
||||
if (isArray(types)) {
|
||||
isOptional = ['undefined', 'null'].every(v => ~types.indexOf(v));
|
||||
|
||||
// Sanity check the caller's type names.
|
||||
types.forEach(function (typ) {
|
||||
if (VALID_TYPES.indexOf(typ) < 0) {
|
||||
let msg = 'Internal error: invalid requirement type "' + typ + '".';
|
||||
throw new Error(msg);
|
||||
}
|
||||
});
|
||||
if (types.indexOf(getTypeOf(optsVal)) < 0)
|
||||
throw new RequirementError(key, req);
|
||||
}
|
||||
}
|
||||
if (req.ok && !req.ok(optsVal))
|
||||
|
||||
if (req.ok && ((!isOptional || !isNil(optsVal)) && !req.ok(optsVal)))
|
||||
throw new RequirementError(key, req);
|
||||
|
||||
if (keyInOpts || (req.map && !mapThrew && optsVal !== undefined))
|
||||
|
@ -142,7 +159,7 @@ let getTypeOf = exports.getTypeOf = function getTypeOf(val) {
|
|||
if (typ === "object") {
|
||||
if (!val)
|
||||
return "null";
|
||||
if (Array.isArray(val))
|
||||
if (isArray(val))
|
||||
return "array";
|
||||
}
|
||||
return typ;
|
||||
|
@ -164,3 +181,38 @@ function RequirementError(key, requirement) {
|
|||
this.message = msg;
|
||||
}
|
||||
RequirementError.prototype = Object.create(Error.prototype);
|
||||
|
||||
let string = { is: ['string', 'undefined', 'null'] };
|
||||
exports.string = string;
|
||||
|
||||
let number = { is: ['number', 'undefined', 'null'] };
|
||||
exports.number = number;
|
||||
|
||||
let boolean = { is: ['boolean', 'undefined', 'null'] };
|
||||
exports.boolean = boolean;
|
||||
|
||||
let object = { is: ['object', 'undefined', 'null'] };
|
||||
exports.object = object;
|
||||
|
||||
let isTruthyType = type => !(type === 'undefined' || type === 'null');
|
||||
let findTypes = v => { while (!isArray(v) && v.is) v = v.is; return v };
|
||||
|
||||
function required(req) {
|
||||
let types = (findTypes(req) || VALID_TYPES).filter(isTruthyType);
|
||||
|
||||
return merge({}, req, {is: types});
|
||||
}
|
||||
exports.required = required;
|
||||
|
||||
function optional(req) {
|
||||
req = merge({is: []}, req);
|
||||
req.is = findTypes(req).filter(isTruthyType).concat('undefined', 'null');
|
||||
|
||||
return req;
|
||||
}
|
||||
exports.optional = optional;
|
||||
|
||||
function either(...types) {
|
||||
return union.apply(null, types.map(findTypes));
|
||||
}
|
||||
exports.either = either;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
"stability": "stable"
|
||||
};
|
||||
|
||||
const { deprecateFunction } = require("../util/deprecate");
|
||||
|
@ -33,4 +33,4 @@ function forceAllowThirdPartyCookie(xhr) {
|
|||
exports.forceAllowThirdPartyCookie = forceAllowThirdPartyCookie;
|
||||
|
||||
// No need to handle add-on unloads as addon/window is closed at unload
|
||||
// and it will take down all the associated requests.
|
||||
// and it will take down all the associated requests.
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.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 { Unknown } = require('../platform/xpcom');
|
||||
const { Class } = require('../core/heritage');
|
||||
const { merge } = require('../util/object');
|
||||
const bookmarkService = Cc['@mozilla.org/browser/nav-bookmarks-service;1']
|
||||
.getService(Ci.nsINavBookmarksService);
|
||||
const historyService = Cc['@mozilla.org/browser/nav-history-service;1']
|
||||
.getService(Ci.nsINavHistoryService);
|
||||
const { mapBookmarkItemType } = require('./utils');
|
||||
const { EventTarget } = require('../event/target');
|
||||
const { emit } = require('../event/core');
|
||||
|
||||
const emitter = EventTarget();
|
||||
|
||||
let HISTORY_ARGS = {
|
||||
onBeginUpdateBatch: [],
|
||||
onEndUpdateBatch: [],
|
||||
onClearHistory: [],
|
||||
onDeleteURI: ['url'],
|
||||
onDeleteVisits: ['url', 'visitTime'],
|
||||
onPageChanged: ['url', 'property', 'value'],
|
||||
onTitleChanged: ['url', 'title'],
|
||||
onVisit: [
|
||||
'url', 'visitId', 'time', 'sessionId', 'referringId', 'transitionType'
|
||||
]
|
||||
};
|
||||
|
||||
let HISTORY_EVENTS = {
|
||||
onBeginUpdateBatch: 'history-start-batch',
|
||||
onEndUpdateBatch: 'history-end-batch',
|
||||
onClearHistory: 'history-start-clear',
|
||||
onDeleteURI: 'history-delete-url',
|
||||
onDeleteVisits: 'history-delete-visits',
|
||||
onPageChanged: 'history-page-changed',
|
||||
onTitleChanged: 'history-title-changed',
|
||||
onVisit: 'history-visit'
|
||||
};
|
||||
|
||||
let BOOKMARK_ARGS = {
|
||||
onItemAdded: [
|
||||
'id', 'parentId', 'index', 'type', 'url', 'title', 'dateAdded'
|
||||
],
|
||||
onItemChanged: [
|
||||
'id', 'property', null, 'value', 'lastModified', 'type', 'parentId'
|
||||
],
|
||||
onItemMoved: [
|
||||
'id', 'previousParentId', 'previousIndex', 'currentParentId',
|
||||
'currentIndex', 'type'
|
||||
],
|
||||
onItemRemoved: ['id', 'parentId', 'index', 'type', 'url'],
|
||||
onItemVisited: ['id', 'visitId', 'time', 'transitionType', 'url', 'parentId']
|
||||
};
|
||||
|
||||
let BOOKMARK_EVENTS = {
|
||||
onItemAdded: 'bookmark-item-added',
|
||||
onItemChanged: 'bookmark-item-changed',
|
||||
onItemMoved: 'bookmark-item-moved',
|
||||
onItemRemoved: 'bookmark-item-removed',
|
||||
onItemVisited: 'bookmark-item-visited',
|
||||
};
|
||||
|
||||
function createHandler (type, propNames) {
|
||||
propNames = propNames || [];
|
||||
return function (...args) {
|
||||
let data = propNames.reduce((acc, prop, i) => {
|
||||
if (prop)
|
||||
acc[prop] = formatValue(prop, args[i]);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
emit(emitter, 'data', {
|
||||
type: type,
|
||||
data: data
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Creates an observer, creating handlers based off of
|
||||
* the `events` names, and ordering arguments from `propNames` hash
|
||||
*/
|
||||
function createObserverInstance (events, propNames) {
|
||||
let definition = Object.keys(events).reduce((prototype, eventName) => {
|
||||
prototype[eventName] = createHandler(events[eventName], propNames[eventName]);
|
||||
return prototype;
|
||||
}, {});
|
||||
|
||||
return Class(merge(definition, { extends: Unknown }))();
|
||||
}
|
||||
|
||||
/*
|
||||
* Formats `data` based off of the value of `type`
|
||||
*/
|
||||
function formatValue (type, data) {
|
||||
if (type === 'type')
|
||||
return mapBookmarkItemType(data);
|
||||
if (type === 'url' && data)
|
||||
return data.spec;
|
||||
return data;
|
||||
}
|
||||
|
||||
let historyObserver = createObserverInstance(HISTORY_EVENTS, HISTORY_ARGS);
|
||||
historyService.addObserver(historyObserver, false);
|
||||
|
||||
let bookmarkObserver = createObserverInstance(BOOKMARK_EVENTS, BOOKMARK_ARGS);
|
||||
bookmarkService.addObserver(bookmarkObserver, false);
|
||||
|
||||
exports.events = emitter;
|
|
@ -104,17 +104,22 @@ function saveBookmarkItem (data) {
|
|||
let group = bmsrv.getFolderIdForItem(id);
|
||||
let index = bmsrv.getItemIndex(id);
|
||||
let type = bmsrv.getItemType(id);
|
||||
let title = typeMap(type) !== 'separator' ?
|
||||
bmsrv.getItemTitle(id) :
|
||||
undefined;
|
||||
let url = typeMap(type) === 'bookmark' ?
|
||||
bmsrv.getBookmarkURI(id).spec :
|
||||
undefined;
|
||||
|
||||
if (data.url) {
|
||||
if (url != data.url)
|
||||
bmsrv.changeBookmarkURI(id, newURI(data.url));
|
||||
}
|
||||
else if (typeMap(type) === 'bookmark')
|
||||
data.url = bmsrv.getBookmarkURI(id).spec;
|
||||
data.url = url;
|
||||
|
||||
if (data.title)
|
||||
if (title != data.title)
|
||||
bmsrv.setItemTitle(id, data.title);
|
||||
else if (typeMap(type) !== 'separator')
|
||||
data.title = bmsrv.getItemTitle(id);
|
||||
data.title = title;
|
||||
|
||||
if (data.group && data.group !== group)
|
||||
bmsrv.moveItem(id, data.group, data.index || -1);
|
||||
|
@ -123,7 +128,7 @@ function saveBookmarkItem (data) {
|
|||
// 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.index = index;
|
||||
|
||||
data.updated = bmsrv.getItemLastModified(data.id);
|
||||
|
||||
|
|
|
@ -235,3 +235,16 @@ function createQueryOptions (type, options) {
|
|||
}
|
||||
exports.createQueryOptions = createQueryOptions;
|
||||
|
||||
|
||||
function mapBookmarkItemType (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;
|
||||
}
|
||||
}
|
||||
exports.mapBookmarkItemType = mapBookmarkItemType;
|
||||
|
|
|
@ -26,7 +26,14 @@ exports.EVENTS = EVENTS;
|
|||
Object.keys(EVENTS).forEach(function(name) {
|
||||
EVENTS[name] = {
|
||||
name: name,
|
||||
listener: ON_PREFIX + name.charAt(0).toUpperCase() + name.substr(1),
|
||||
listener: createListenerName(name),
|
||||
dom: EVENTS[name]
|
||||
}
|
||||
});
|
||||
|
||||
function createListenerName (name) {
|
||||
if (name === 'pageshow')
|
||||
return 'onPageShow';
|
||||
else
|
||||
return ON_PREFIX + name.charAt(0).toUpperCase() + name.substr(1);
|
||||
}
|
||||
|
|
|
@ -33,6 +33,9 @@ const Tab = Class({
|
|||
// TabReady
|
||||
let onReady = tabInternals.onReady = onTabReady.bind(this);
|
||||
tab.browser.addEventListener(EVENTS.ready.dom, onReady, false);
|
||||
|
||||
let onPageShow = tabInternals.onPageShow = onTabPageShow.bind(this);
|
||||
tab.browser.addEventListener(EVENTS.pageshow.dom, onPageShow, false);
|
||||
|
||||
// TabClose
|
||||
let onClose = tabInternals.onClose = onTabClose.bind(this);
|
||||
|
@ -180,8 +183,10 @@ function cleanupTab(tab) {
|
|||
|
||||
if (tabInternals.tab.browser) {
|
||||
tabInternals.tab.browser.removeEventListener(EVENTS.ready.dom, tabInternals.onReady, false);
|
||||
tabInternals.tab.browser.removeEventListener(EVENTS.pageshow.dom, tabInternals.onPageShow, false);
|
||||
}
|
||||
tabInternals.onReady = null;
|
||||
tabInternals.onPageShow = null;
|
||||
tabInternals.window.BrowserApp.deck.removeEventListener(EVENTS.close.dom, tabInternals.onClose, false);
|
||||
tabInternals.onClose = null;
|
||||
rawTabNS(tabInternals.tab).tab = null;
|
||||
|
@ -198,6 +203,12 @@ function onTabReady(event) {
|
|||
}
|
||||
}
|
||||
|
||||
function onTabPageShow(event) {
|
||||
let win = event.target.defaultView;
|
||||
if (win === win.top)
|
||||
emit(this, 'pageshow', this, event.persisted);
|
||||
}
|
||||
|
||||
// TabClose
|
||||
function onTabClose(event) {
|
||||
let rawTab = getTabForBrowser(event.target);
|
||||
|
|
|
@ -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";
|
||||
|
||||
module.metadata = {
|
||||
|
@ -277,3 +276,16 @@ let isValidURI = exports.isValidURI = function (uri) {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isLocalURL(url) {
|
||||
if (String.indexOf(url, './') === 0)
|
||||
return true;
|
||||
|
||||
try {
|
||||
return ['resource', 'data', 'chrome'].indexOf(URL(url).scheme) > -1;
|
||||
}
|
||||
catch(e) {}
|
||||
|
||||
return false;
|
||||
}
|
||||
exports.isLocalURL = isLocalURL;
|
||||
|
|
|
@ -72,12 +72,22 @@ exports.remove = function remove(array, element) {
|
|||
* Source array.
|
||||
* @returns {Array}
|
||||
*/
|
||||
exports.unique = function unique(array) {
|
||||
return array.reduce(function(values, element) {
|
||||
add(values, element);
|
||||
return values;
|
||||
function unique(array) {
|
||||
return array.reduce(function(result, item) {
|
||||
add(result, item);
|
||||
return result;
|
||||
}, []);
|
||||
};
|
||||
exports.unique = unique;
|
||||
|
||||
/**
|
||||
* Produce an array that contains the union: each distinct element from all
|
||||
* of the passed-in arrays.
|
||||
*/
|
||||
function union() {
|
||||
return unique(Array.concat.apply(null, arguments));
|
||||
};
|
||||
exports.union = union;
|
||||
|
||||
exports.flatten = function flatten(array){
|
||||
var flat = [];
|
||||
|
|
|
@ -77,6 +77,9 @@ const Tabs = Class({
|
|||
if (options.onReady)
|
||||
tab.on('ready', options.onReady);
|
||||
|
||||
if (options.onPageShow)
|
||||
tab.on('pageshow', options.onPageShow);
|
||||
|
||||
if (options.onActivate)
|
||||
tab.on('activate', options.onActivate);
|
||||
|
||||
|
@ -131,9 +134,12 @@ function onTabOpen(event) {
|
|||
tab.on('ready', function() emit(gTabs, 'ready', tab));
|
||||
tab.once('close', onTabClose);
|
||||
|
||||
tab.on('pageshow', function(_tab, persisted)
|
||||
emit(gTabs, 'pageshow', tab, persisted));
|
||||
|
||||
emit(tab, 'open', tab);
|
||||
emit(gTabs, 'open', tab);
|
||||
};
|
||||
}
|
||||
|
||||
// TabSelect
|
||||
function onTabSelect(event) {
|
||||
|
@ -153,10 +159,10 @@ function onTabSelect(event) {
|
|||
emit(t, 'deactivate', t);
|
||||
emit(gTabs, 'deactivate', t);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// TabClose
|
||||
function onTabClose(tab) {
|
||||
removeTab(tab);
|
||||
emit(gTabs, EVENTS.close.name, tab);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -406,7 +406,8 @@ class Runner(object):
|
|||
def find_binary(self):
|
||||
"""Finds the binary for self.names if one was not provided."""
|
||||
binary = None
|
||||
if sys.platform in ('linux2', 'sunos5', 'solaris'):
|
||||
if sys.platform in ('linux2', 'sunos5', 'solaris') \
|
||||
or sys.platform.startswith('freebsd'):
|
||||
for name in reversed(self.names):
|
||||
binary = findInPath(name)
|
||||
elif os.name == 'nt' or sys.platform == 'cygwin':
|
||||
|
@ -578,7 +579,8 @@ class FirefoxRunner(Runner):
|
|||
def names(self):
|
||||
if sys.platform == 'darwin':
|
||||
return ['firefox', 'nightly', 'shiretoko']
|
||||
if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')):
|
||||
if sys.platform in ('linux2', 'sunos5', 'solaris') \
|
||||
or sys.platform.startswith('freebsd'):
|
||||
return ['firefox', 'mozilla-firefox', 'iceweasel']
|
||||
if os.name == 'nt' or sys.platform == 'cygwin':
|
||||
return ['firefox']
|
||||
|
|
|
@ -257,7 +257,8 @@ class Popen(subprocess.Popen):
|
|||
self.kill(group)
|
||||
|
||||
else:
|
||||
if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')):
|
||||
if sys.platform in ('linux2', 'sunos5', 'solaris') \
|
||||
or sys.platform.startswith('freebsd'):
|
||||
def group_wait(timeout):
|
||||
try:
|
||||
os.waitpid(self.pid, 0)
|
||||
|
|
|
@ -10,15 +10,20 @@ const { isGlobalPBSupported } = require('sdk/private-browsing/utils');
|
|||
merge(module.exports,
|
||||
require('./test-tabs'),
|
||||
require('./test-page-mod'),
|
||||
require('./test-selection'),
|
||||
require('./test-panel'),
|
||||
require('./test-private-browsing'),
|
||||
isGlobalPBSupported ? require('./test-global-private-browsing') : {}
|
||||
);
|
||||
|
||||
// Doesn't make sense to test window-utils and windows on fennec,
|
||||
// as there is only one window which is never private
|
||||
if (!app.is('Fennec'))
|
||||
merge(module.exports, require('./test-windows'));
|
||||
// as there is only one window which is never private. Also ignore
|
||||
// unsupported modules (panel, selection)
|
||||
if (!app.is('Fennec')) {
|
||||
merge(module.exports,
|
||||
require('./test-selection'),
|
||||
require('./test-panel'),
|
||||
require('./test-window-tabs'),
|
||||
require('./test-windows')
|
||||
);
|
||||
}
|
||||
|
||||
require('sdk/test/runner').runTestsFromModule(module);
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
const tabs = require('sdk/tabs');
|
||||
const { is } = require('sdk/system/xul-app');
|
||||
const { isPrivate } = require('sdk/private-browsing');
|
||||
const pbUtils = require('sdk/private-browsing/utils');
|
||||
const { getOwnerWindow } = require('sdk/private-browsing/window/utils');
|
||||
const { promise: windowPromise, close, focus } = require('sdk/window/helpers');
|
||||
const { getMostRecentBrowserWindow } = require('sdk/window/utils');
|
||||
|
||||
exports.testPrivateTabsAreListed = function (assert, done) {
|
||||
let originalTabCount = tabs.length;
|
||||
|
@ -32,82 +29,5 @@ exports.testPrivateTabsAreListed = function (assert, done) {
|
|||
tab.close(done);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.testOpenTabWithPrivateActiveWindowNoIsPrivateOption = function(assert, done) {
|
||||
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true });
|
||||
|
||||
windowPromise(window, 'load').then(focus).then(function (window) {
|
||||
assert.ok(isPrivate(window), 'new window is private');
|
||||
|
||||
tabs.open({
|
||||
url: 'about:blank',
|
||||
onOpen: function(tab) {
|
||||
assert.ok(isPrivate(tab), 'new tab is private');
|
||||
assert.ok(isPrivate(getOwnerWindow(tab)), 'new tab window is private');
|
||||
assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the private window are the same');
|
||||
|
||||
close(window).then(done, assert.fail);
|
||||
}
|
||||
})
|
||||
}, assert.fail).then(null, assert.fail);
|
||||
}
|
||||
|
||||
exports.testOpenTabWithNonPrivateActiveWindowNoIsPrivateOption = function(assert, done) {
|
||||
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: false });
|
||||
|
||||
windowPromise(window, 'load').then(focus).then(function (window) {
|
||||
assert.equal(isPrivate(window), false, 'new window is not private');
|
||||
|
||||
tabs.open({
|
||||
url: 'about:blank',
|
||||
onOpen: function(tab) {
|
||||
assert.equal(isPrivate(tab), false, 'new tab is not private');
|
||||
assert.equal(isPrivate(getOwnerWindow(tab)), false, 'new tab window is not private');
|
||||
assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the new window are the same');
|
||||
|
||||
close(window).then(done, assert.fail);
|
||||
}
|
||||
})
|
||||
}, assert.fail).then(null, assert.fail);
|
||||
}
|
||||
|
||||
exports.testOpenTabWithPrivateActiveWindowWithIsPrivateOptionTrue = function(assert, done) {
|
||||
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true });
|
||||
|
||||
windowPromise(window, 'load').then(focus).then(function (window) {
|
||||
assert.ok(isPrivate(window), 'new window is private');
|
||||
|
||||
tabs.open({
|
||||
url: 'about:blank',
|
||||
isPrivate: true,
|
||||
onOpen: function(tab) {
|
||||
assert.ok(isPrivate(tab), 'new tab is private');
|
||||
assert.ok(isPrivate(getOwnerWindow(tab)), 'new tab window is private');
|
||||
assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the private window are the same');
|
||||
|
||||
close(window).then(done, assert.fail);
|
||||
}
|
||||
})
|
||||
}, assert.fail).then(null, assert.fail);
|
||||
}
|
||||
|
||||
exports.testOpenTabWithNonPrivateActiveWindowWithIsPrivateOptionFalse = function(assert, done) {
|
||||
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: false });
|
||||
|
||||
windowPromise(window, 'load').then(focus).then(function (window) {
|
||||
assert.equal(isPrivate(window), false, 'new window is not private');
|
||||
|
||||
tabs.open({
|
||||
url: 'about:blank',
|
||||
isPrivate: false,
|
||||
onOpen: function(tab) {
|
||||
assert.equal(isPrivate(tab), false, 'new tab is not private');
|
||||
assert.equal(isPrivate(getOwnerWindow(tab)), false, 'new tab window is not private');
|
||||
assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the new window are the same');
|
||||
|
||||
close(window).then(done, assert.fail);
|
||||
}
|
||||
})
|
||||
}, assert.fail).then(null, assert.fail);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
'use strict';
|
||||
|
||||
const tabs = require('sdk/tabs');
|
||||
const { isPrivate } = require('sdk/private-browsing');
|
||||
const { getOwnerWindow } = require('sdk/private-browsing/window/utils');
|
||||
const { promise: windowPromise, close, focus } = require('sdk/window/helpers');
|
||||
const { getMostRecentBrowserWindow } = require('sdk/window/utils');
|
||||
|
||||
exports.testOpenTabWithPrivateActiveWindowNoIsPrivateOption = function(assert, done) {
|
||||
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true });
|
||||
|
||||
windowPromise(window, 'load').then(focus).then(function (window) {
|
||||
assert.ok(isPrivate(window), 'new window is private');
|
||||
|
||||
tabs.open({
|
||||
url: 'about:blank',
|
||||
onOpen: function(tab) {
|
||||
assert.ok(isPrivate(tab), 'new tab is private');
|
||||
assert.ok(isPrivate(getOwnerWindow(tab)), 'new tab window is private');
|
||||
assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the private window are the same');
|
||||
|
||||
close(window).then(done, assert.fail);
|
||||
}
|
||||
})
|
||||
}, assert.fail).then(null, assert.fail);
|
||||
}
|
||||
|
||||
exports.testOpenTabWithNonPrivateActiveWindowNoIsPrivateOption = function(assert, done) {
|
||||
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: false });
|
||||
|
||||
windowPromise(window, 'load').then(focus).then(function (window) {
|
||||
assert.equal(isPrivate(window), false, 'new window is not private');
|
||||
|
||||
tabs.open({
|
||||
url: 'about:blank',
|
||||
onOpen: function(tab) {
|
||||
assert.equal(isPrivate(tab), false, 'new tab is not private');
|
||||
assert.equal(isPrivate(getOwnerWindow(tab)), false, 'new tab window is not private');
|
||||
assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the new window are the same');
|
||||
|
||||
close(window).then(done, assert.fail);
|
||||
}
|
||||
})
|
||||
}, assert.fail).then(null, assert.fail);
|
||||
}
|
||||
|
||||
exports.testOpenTabWithPrivateActiveWindowWithIsPrivateOptionTrue = function(assert, done) {
|
||||
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true });
|
||||
|
||||
windowPromise(window, 'load').then(focus).then(function (window) {
|
||||
assert.ok(isPrivate(window), 'new window is private');
|
||||
|
||||
tabs.open({
|
||||
url: 'about:blank',
|
||||
isPrivate: true,
|
||||
onOpen: function(tab) {
|
||||
assert.ok(isPrivate(tab), 'new tab is private');
|
||||
assert.ok(isPrivate(getOwnerWindow(tab)), 'new tab window is private');
|
||||
assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the private window are the same');
|
||||
|
||||
close(window).then(done, assert.fail);
|
||||
}
|
||||
})
|
||||
}, assert.fail).then(null, assert.fail);
|
||||
}
|
||||
|
||||
exports.testOpenTabWithNonPrivateActiveWindowWithIsPrivateOptionFalse = function(assert, done) {
|
||||
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: false });
|
||||
|
||||
windowPromise(window, 'load').then(focus).then(function (window) {
|
||||
assert.equal(isPrivate(window), false, 'new window is not private');
|
||||
|
||||
tabs.open({
|
||||
url: 'about:blank',
|
||||
isPrivate: false,
|
||||
onOpen: function(tab) {
|
||||
assert.equal(isPrivate(tab), false, 'new tab is not private');
|
||||
assert.equal(isPrivate(getOwnerWindow(tab)), false, 'new tab window is not private');
|
||||
assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the new window are the same');
|
||||
|
||||
close(window).then(done, assert.fail);
|
||||
}
|
||||
})
|
||||
}, assert.fail).then(null, assert.fail);
|
||||
}
|
|
@ -4,10 +4,14 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { Panel } = require("sdk/panel")
|
||||
const { data } = require("sdk/self")
|
||||
const app = require("sdk/system/xul-app");
|
||||
|
||||
exports["test addon globa"] = app.is("Firefox") ? testAddonGlobal : unsupported;
|
||||
|
||||
function testAddonGlobal (assert, done) {
|
||||
const { Panel } = require("sdk/panel")
|
||||
const { data } = require("sdk/self")
|
||||
|
||||
exports["test addon global"] = function(assert, done) {
|
||||
let panel = Panel({
|
||||
contentURL: //"data:text/html,now?",
|
||||
data.url("./index.html"),
|
||||
|
@ -17,10 +21,14 @@ exports["test addon global"] = function(assert, done) {
|
|||
done();
|
||||
},
|
||||
onError: function(error) {
|
||||
asser.fail(Error("failed to recieve message"));
|
||||
assert.fail(Error("failed to recieve message"));
|
||||
done();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function unsupported (assert) {
|
||||
assert.pass("privileged-panel unsupported on platform");
|
||||
}
|
||||
|
||||
require("sdk/test/runner").runTestsFromModule(module);
|
||||
|
|
|
@ -13,42 +13,18 @@ const basePath = pathFor('ProfD');
|
|||
const { atob } = Cu.import("resource://gre/modules/Services.jsm", {});
|
||||
const historyService = Cc["@mozilla.org/browser/nav-history-service;1"]
|
||||
.getService(Ci.nsINavHistoryService);
|
||||
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
|
||||
const ObserverShimMethods = ['onBeginUpdateBatch', 'onEndUpdateBatch',
|
||||
'onVisit', 'onTitleChanged', 'onDeleteURI', 'onClearHistory',
|
||||
'onPageChanged', 'onDeleteVisits'];
|
||||
const { events } = require('sdk/places/events');
|
||||
|
||||
/*
|
||||
* Shims NavHistoryObserver
|
||||
*/
|
||||
|
||||
let noop = function () {}
|
||||
let NavHistoryObserver = function () {};
|
||||
ObserverShimMethods.forEach(function (method) {
|
||||
NavHistoryObserver.prototype[method] = noop;
|
||||
});
|
||||
NavHistoryObserver.prototype.QueryInterface = XPCOMUtils.generateQI([
|
||||
Ci.nsINavHistoryObserver
|
||||
]);
|
||||
|
||||
/*
|
||||
* Uses history observer to watch for an onPageChanged event,
|
||||
* which detects when a favicon is updated in the registry.
|
||||
*/
|
||||
function onFaviconChange (uri, callback) {
|
||||
let observer = Object.create(NavHistoryObserver.prototype, {
|
||||
onPageChanged: {
|
||||
value: function onPageChanged(aURI, aWhat, aValue, aGUID) {
|
||||
if (aWhat !== Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON)
|
||||
return;
|
||||
if (aURI.spec !== uri)
|
||||
return;
|
||||
historyService.removeObserver(this);
|
||||
callback(aValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
historyService.addObserver(observer, false);
|
||||
function onFaviconChange (url, callback) {
|
||||
function handler ({data, type}) {
|
||||
if (type !== 'history-page-changed' ||
|
||||
data.url !== url ||
|
||||
data.property !== Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON)
|
||||
return;
|
||||
events.off('data', handler);
|
||||
callback(data.value);
|
||||
}
|
||||
events.on('data', handler);
|
||||
}
|
||||
exports.onFaviconChange = onFaviconChange;
|
||||
|
||||
|
|
|
@ -106,6 +106,13 @@ function addVisits (urls) {
|
|||
}
|
||||
exports.addVisits = addVisits;
|
||||
|
||||
function removeVisits (urls) {
|
||||
[].concat(urls).map(url => {
|
||||
hsrv.removePage(newURI(url));
|
||||
});
|
||||
}
|
||||
exports.removeVisits = removeVisits;
|
||||
|
||||
// Creates a mozIVisitInfo object
|
||||
function createVisit (url) {
|
||||
let place = {}
|
||||
|
|
|
@ -949,40 +949,6 @@ exports.testOnLoadEventWithImage = function(test) {
|
|||
});
|
||||
};
|
||||
|
||||
exports.testOnPageShowEvent = function (test) {
|
||||
test.waitUntilDone();
|
||||
|
||||
let firstUrl = 'data:text/html;charset=utf-8,First';
|
||||
let secondUrl = 'data:text/html;charset=utf-8,Second';
|
||||
|
||||
openBrowserWindow(function(window, browser) {
|
||||
let counter = 0;
|
||||
tabs.on('pageshow', function onPageShow(tab, persisted) {
|
||||
counter++;
|
||||
if (counter === 1) {
|
||||
test.assert(!persisted, 'page should not be cached on initial load');
|
||||
tab.url = secondUrl;
|
||||
}
|
||||
else if (counter === 2) {
|
||||
test.assert(!persisted, 'second test page should not be cached either');
|
||||
tab.attach({
|
||||
contentScript: 'setTimeout(function () { window.history.back(); }, 0)'
|
||||
});
|
||||
}
|
||||
else {
|
||||
test.assert(persisted, 'when we get back to the fist page, it has to' +
|
||||
'come from cache');
|
||||
tabs.removeListener('pageshow', onPageShow);
|
||||
closeBrowserWindow(window, function() test.done());
|
||||
}
|
||||
});
|
||||
|
||||
tabs.open({
|
||||
url: firstUrl
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.testFaviconGetterDeprecation = function (test) {
|
||||
const { LoaderWithHookedConsole } = require("sdk/test/loader");
|
||||
let { loader, messages } = LoaderWithHookedConsole(module);
|
||||
|
|
|
@ -6,92 +6,94 @@
|
|||
|
||||
const apiUtils = require("sdk/deprecated/api-utils");
|
||||
|
||||
exports.testPublicConstructor = function (test) {
|
||||
exports.testPublicConstructor = function (assert) {
|
||||
function PrivateCtor() {}
|
||||
PrivateCtor.prototype = {};
|
||||
|
||||
let PublicCtor = apiUtils.publicConstructor(PrivateCtor);
|
||||
test.assert(
|
||||
assert.ok(
|
||||
PrivateCtor.prototype.isPrototypeOf(PublicCtor.prototype),
|
||||
"PrivateCtor.prototype should be prototype of PublicCtor.prototype"
|
||||
);
|
||||
|
||||
function testObj(useNew) {
|
||||
let obj = useNew ? new PublicCtor() : PublicCtor();
|
||||
test.assert(obj instanceof PublicCtor,
|
||||
assert.ok(obj instanceof PublicCtor,
|
||||
"Object should be instance of PublicCtor");
|
||||
test.assert(obj instanceof PrivateCtor,
|
||||
assert.ok(obj instanceof PrivateCtor,
|
||||
"Object should be instance of PrivateCtor");
|
||||
test.assert(PublicCtor.prototype.isPrototypeOf(obj),
|
||||
assert.ok(PublicCtor.prototype.isPrototypeOf(obj),
|
||||
"PublicCtor's prototype should be prototype of object");
|
||||
test.assertEqual(obj.constructor, PublicCtor,
|
||||
assert.equal(obj.constructor, PublicCtor,
|
||||
"Object constructor should be PublicCtor");
|
||||
}
|
||||
testObj(true);
|
||||
testObj(false);
|
||||
};
|
||||
|
||||
exports.testValidateOptionsEmpty = function (test) {
|
||||
exports.testValidateOptionsEmpty = function (assert) {
|
||||
let val = apiUtils.validateOptions(null, {});
|
||||
assertObjsEqual(test, val, {});
|
||||
|
||||
assert.deepEqual(val, {});
|
||||
|
||||
val = apiUtils.validateOptions(null, { foo: {} });
|
||||
assertObjsEqual(test, val, {});
|
||||
assert.deepEqual(val, {});
|
||||
|
||||
val = apiUtils.validateOptions({}, {});
|
||||
assertObjsEqual(test, val, {});
|
||||
assert.deepEqual(val, {});
|
||||
|
||||
val = apiUtils.validateOptions({}, { foo: {} });
|
||||
assertObjsEqual(test, val, {});
|
||||
assert.deepEqual(val, {});
|
||||
};
|
||||
|
||||
exports.testValidateOptionsNonempty = function (test) {
|
||||
exports.testValidateOptionsNonempty = function (assert) {
|
||||
let val = apiUtils.validateOptions({ foo: 123 }, {});
|
||||
assertObjsEqual(test, val, {});
|
||||
assert.deepEqual(val, {});
|
||||
|
||||
val = apiUtils.validateOptions({ foo: 123, bar: 456 },
|
||||
{ foo: {}, bar: {}, baz: {} });
|
||||
assertObjsEqual(test, val, { foo: 123, bar: 456 });
|
||||
|
||||
assert.deepEqual(val, { foo: 123, bar: 456 });
|
||||
};
|
||||
|
||||
exports.testValidateOptionsMap = function (test) {
|
||||
exports.testValidateOptionsMap = function (assert) {
|
||||
let val = apiUtils.validateOptions({ foo: 3, bar: 2 }, {
|
||||
foo: { map: function (v) v * v },
|
||||
bar: { map: function (v) undefined }
|
||||
});
|
||||
assertObjsEqual(test, val, { foo: 9, bar: undefined });
|
||||
assert.deepEqual(val, { foo: 9, bar: undefined });
|
||||
};
|
||||
|
||||
exports.testValidateOptionsMapException = function (test) {
|
||||
exports.testValidateOptionsMapException = function (assert) {
|
||||
let val = apiUtils.validateOptions({ foo: 3 }, {
|
||||
foo: { map: function () { throw new Error(); }}
|
||||
});
|
||||
assertObjsEqual(test, val, { foo: 3 });
|
||||
assert.deepEqual(val, { foo: 3 });
|
||||
};
|
||||
|
||||
exports.testValidateOptionsOk = function (test) {
|
||||
exports.testValidateOptionsOk = function (assert) {
|
||||
let val = apiUtils.validateOptions({ foo: 3, bar: 2, baz: 1 }, {
|
||||
foo: { ok: function (v) v },
|
||||
bar: { ok: function (v) v }
|
||||
});
|
||||
assertObjsEqual(test, val, { foo: 3, bar: 2 });
|
||||
assert.deepEqual(val, { foo: 3, bar: 2 });
|
||||
|
||||
test.assertRaises(
|
||||
assert.throws(
|
||||
function () apiUtils.validateOptions({ foo: 2, bar: 2 }, {
|
||||
bar: { ok: function (v) v > 2 }
|
||||
}),
|
||||
'The option "bar" is invalid.',
|
||||
/^The option "bar" is invalid/,
|
||||
"ok should raise exception on invalid option"
|
||||
);
|
||||
|
||||
test.assertRaises(
|
||||
assert.throws(
|
||||
function () apiUtils.validateOptions(null, { foo: { ok: function (v) v }}),
|
||||
'The option "foo" is invalid.',
|
||||
/^The option "foo" is invalid/,
|
||||
"ok should raise exception on invalid option"
|
||||
);
|
||||
};
|
||||
|
||||
exports.testValidateOptionsIs = function (test) {
|
||||
exports.testValidateOptionsIs = function (assert) {
|
||||
let opts = {
|
||||
array: [],
|
||||
boolean: true,
|
||||
|
@ -114,18 +116,137 @@ exports.testValidateOptionsIs = function (test) {
|
|||
undef2: { is: ["undefined"] }
|
||||
};
|
||||
let val = apiUtils.validateOptions(opts, requirements);
|
||||
assertObjsEqual(test, val, opts);
|
||||
assert.deepEqual(val, opts);
|
||||
|
||||
test.assertRaises(
|
||||
assert.throws(
|
||||
function () apiUtils.validateOptions(null, {
|
||||
foo: { is: ["object", "number"] }
|
||||
}),
|
||||
'The option "foo" must be one of the following types: object, number',
|
||||
/^The option "foo" must be one of the following types: object, number/,
|
||||
"Invalid type should raise exception"
|
||||
);
|
||||
};
|
||||
|
||||
exports.testValidateOptionsMapIsOk = function (test) {
|
||||
exports.testValidateOptionsIsWithExportedValue = function (assert) {
|
||||
let { string, number, boolean, object } = apiUtils;
|
||||
|
||||
let opts = {
|
||||
boolean: true,
|
||||
number: 1337,
|
||||
object: {},
|
||||
string: "foo"
|
||||
};
|
||||
let requirements = {
|
||||
string: { is: string },
|
||||
number: { is: number },
|
||||
boolean: { is: boolean },
|
||||
object: { is: object }
|
||||
};
|
||||
let val = apiUtils.validateOptions(opts, requirements);
|
||||
assert.deepEqual(val, opts);
|
||||
|
||||
// Test the types are optional by default
|
||||
val = apiUtils.validateOptions({foo: 'bar'}, requirements);
|
||||
assert.deepEqual(val, {});
|
||||
};
|
||||
|
||||
exports.testValidateOptionsIsWithEither = function (assert) {
|
||||
let { string, number, boolean, either } = apiUtils;
|
||||
let text = { is: either(string, number) };
|
||||
|
||||
let requirements = {
|
||||
text: text,
|
||||
boolOrText: { is: either(text, boolean) }
|
||||
};
|
||||
|
||||
let val = apiUtils.validateOptions({text: 12}, requirements);
|
||||
assert.deepEqual(val, {text: 12});
|
||||
|
||||
val = apiUtils.validateOptions({text: "12"}, requirements);
|
||||
assert.deepEqual(val, {text: "12"});
|
||||
|
||||
val = apiUtils.validateOptions({boolOrText: true}, requirements);
|
||||
assert.deepEqual(val, {boolOrText: true});
|
||||
|
||||
val = apiUtils.validateOptions({boolOrText: "true"}, requirements);
|
||||
assert.deepEqual(val, {boolOrText: "true"});
|
||||
|
||||
val = apiUtils.validateOptions({boolOrText: 1}, requirements);
|
||||
assert.deepEqual(val, {boolOrText: 1});
|
||||
|
||||
assert.throws(
|
||||
() => apiUtils.validateOptions({text: true}, requirements),
|
||||
/^The option "text" must be one of the following types/,
|
||||
"Invalid type should raise exception"
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => apiUtils.validateOptions({boolOrText: []}, requirements),
|
||||
/^The option "boolOrText" must be one of the following types/,
|
||||
"Invalid type should raise exception"
|
||||
);
|
||||
};
|
||||
|
||||
exports.testValidateOptionsWithRequiredAndOptional = function (assert) {
|
||||
let { string, number, required, optional } = apiUtils;
|
||||
|
||||
let opts = {
|
||||
number: 1337,
|
||||
string: "foo"
|
||||
};
|
||||
|
||||
let requirements = {
|
||||
string: required(string),
|
||||
number: number
|
||||
};
|
||||
|
||||
let val = apiUtils.validateOptions(opts, requirements);
|
||||
assert.deepEqual(val, opts);
|
||||
|
||||
val = apiUtils.validateOptions({string: "foo"}, requirements);
|
||||
assert.deepEqual(val, {string: "foo"});
|
||||
|
||||
assert.throws(
|
||||
() => apiUtils.validateOptions({number: 10}, requirements),
|
||||
/^The option "string" must be one of the following types/,
|
||||
"Invalid type should raise exception"
|
||||
);
|
||||
|
||||
// Makes string optional
|
||||
requirements.string = optional(requirements.string);
|
||||
|
||||
val = apiUtils.validateOptions({number: 10}, requirements),
|
||||
assert.deepEqual(val, {number: 10});
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
exports.testValidateOptionsWithExportedValue = function (assert) {
|
||||
let { string, number, boolean, object } = apiUtils;
|
||||
|
||||
let opts = {
|
||||
boolean: true,
|
||||
number: 1337,
|
||||
object: {},
|
||||
string: "foo"
|
||||
};
|
||||
let requirements = {
|
||||
string: string,
|
||||
number: number,
|
||||
boolean: boolean,
|
||||
object: object
|
||||
};
|
||||
let val = apiUtils.validateOptions(opts, requirements);
|
||||
assert.deepEqual(val, opts);
|
||||
|
||||
// Test the types are optional by default
|
||||
val = apiUtils.validateOptions({foo: 'bar'}, requirements);
|
||||
assert.deepEqual(val, {});
|
||||
};
|
||||
|
||||
|
||||
exports.testValidateOptionsMapIsOk = function (assert) {
|
||||
let [map, is, ok] = [false, false, false];
|
||||
let val = apiUtils.validateOptions({ foo: 1337 }, {
|
||||
foo: {
|
||||
|
@ -134,48 +255,48 @@ exports.testValidateOptionsMapIsOk = function (test) {
|
|||
ok: function (v) v.length > 0
|
||||
}
|
||||
});
|
||||
assertObjsEqual(test, val, { foo: "1337" });
|
||||
assert.deepEqual(val, { foo: "1337" });
|
||||
|
||||
let requirements = {
|
||||
foo: {
|
||||
is: ["object"],
|
||||
ok: function () test.fail("is should have caused us to throw by now")
|
||||
ok: function () assert.fail("is should have caused us to throw by now")
|
||||
}
|
||||
};
|
||||
test.assertRaises(
|
||||
assert.throws(
|
||||
function () apiUtils.validateOptions(null, requirements),
|
||||
'The option "foo" must be one of the following types: object',
|
||||
/^The option "foo" must be one of the following types: object/,
|
||||
"is should be used before ok is called"
|
||||
);
|
||||
};
|
||||
|
||||
exports.testValidateOptionsErrorMsg = function (test) {
|
||||
test.assertRaises(
|
||||
exports.testValidateOptionsErrorMsg = function (assert) {
|
||||
assert.throws(
|
||||
function () apiUtils.validateOptions(null, {
|
||||
foo: { ok: function (v) v, msg: "foo!" }
|
||||
}),
|
||||
"foo!",
|
||||
/^foo!/,
|
||||
"ok should raise exception with customized message"
|
||||
);
|
||||
};
|
||||
|
||||
exports.testValidateMapWithMissingKey = function (test) {
|
||||
exports.testValidateMapWithMissingKey = function (assert) {
|
||||
let val = apiUtils.validateOptions({ }, {
|
||||
foo: {
|
||||
map: function (v) v || "bar"
|
||||
}
|
||||
});
|
||||
assertObjsEqual(test, val, { foo: "bar" });
|
||||
assert.deepEqual(val, { foo: "bar" });
|
||||
|
||||
val = apiUtils.validateOptions({ }, {
|
||||
foo: {
|
||||
map: function (v) { throw "bar" }
|
||||
}
|
||||
});
|
||||
assertObjsEqual(test, val, { });
|
||||
assert.deepEqual(val, { });
|
||||
};
|
||||
|
||||
exports.testValidateMapWithMissingKeyAndThrown = function (test) {
|
||||
exports.testValidateMapWithMissingKeyAndThrown = function (assert) {
|
||||
let val = apiUtils.validateOptions({}, {
|
||||
bar: {
|
||||
map: function(v) { throw "bar" }
|
||||
|
@ -184,10 +305,10 @@ exports.testValidateMapWithMissingKeyAndThrown = function (test) {
|
|||
map: function(v) "foo"
|
||||
}
|
||||
});
|
||||
assertObjsEqual(test, val, { baz: "foo" });
|
||||
assert.deepEqual(val, { baz: "foo" });
|
||||
};
|
||||
|
||||
exports.testAddIterator = function testAddIterator(test) {
|
||||
exports.testAddIterator = function testAddIterator (assert) {
|
||||
let obj = {};
|
||||
let keys = ["foo", "bar", "baz"];
|
||||
let vals = [1, 2, 3];
|
||||
|
@ -203,34 +324,20 @@ exports.testAddIterator = function testAddIterator(test) {
|
|||
let keysItr = [];
|
||||
for (let key in obj)
|
||||
keysItr.push(key);
|
||||
test.assertEqual(keysItr.length, keys.length,
|
||||
|
||||
assert.equal(keysItr.length, keys.length,
|
||||
"the keys iterator returns the correct number of items");
|
||||
for (let i = 0; i < keys.length; i++)
|
||||
test.assertEqual(keysItr[i], keys[i], "the key is correct");
|
||||
assert.equal(keysItr[i], keys[i], "the key is correct");
|
||||
|
||||
let valsItr = [];
|
||||
for each (let val in obj)
|
||||
valsItr.push(val);
|
||||
test.assertEqual(valsItr.length, vals.length,
|
||||
assert.equal(valsItr.length, vals.length,
|
||||
"the vals iterator returns the correct number of items");
|
||||
for (let i = 0; i < vals.length; i++)
|
||||
test.assertEqual(valsItr[i], vals[i], "the val is correct");
|
||||
assert.equal(valsItr[i], vals[i], "the val is correct");
|
||||
|
||||
};
|
||||
|
||||
function assertObjsEqual(test, obj1, obj2) {
|
||||
var items = 0;
|
||||
for (let key in obj1) {
|
||||
items++;
|
||||
test.assert(key in obj2, "obj1 key should be present in obj2");
|
||||
test.assertEqual(obj2[key], obj1[key], "obj1 value should match obj2 value");
|
||||
}
|
||||
for (let key in obj2) {
|
||||
items++;
|
||||
test.assert(key in obj1, "obj2 key should be present in obj1");
|
||||
test.assertEqual(obj1[key], obj2[key], "obj2 value should match obj1 value");
|
||||
}
|
||||
if (!items)
|
||||
test.assertEqual(JSON.stringify(obj1), JSON.stringify(obj2),
|
||||
"obj1 should have same JSON representation as obj2");
|
||||
}
|
||||
require('test').run(exports);
|
||||
|
|
|
@ -5,67 +5,67 @@
|
|||
|
||||
const array = require('sdk/util/array');
|
||||
|
||||
exports.testHas = function(test) {
|
||||
exports.testHas = function(assert) {
|
||||
var testAry = [1, 2, 3];
|
||||
test.assertEqual(array.has([1, 2, 3], 1), true);
|
||||
test.assertEqual(testAry.length, 3);
|
||||
test.assertEqual(testAry[0], 1);
|
||||
test.assertEqual(testAry[1], 2);
|
||||
test.assertEqual(testAry[2], 3);
|
||||
test.assertEqual(array.has(testAry, 2), true);
|
||||
test.assertEqual(array.has(testAry, 3), true);
|
||||
test.assertEqual(array.has(testAry, 4), false);
|
||||
test.assertEqual(array.has(testAry, '1'), false);
|
||||
assert.equal(array.has([1, 2, 3], 1), true);
|
||||
assert.equal(testAry.length, 3);
|
||||
assert.equal(testAry[0], 1);
|
||||
assert.equal(testAry[1], 2);
|
||||
assert.equal(testAry[2], 3);
|
||||
assert.equal(array.has(testAry, 2), true);
|
||||
assert.equal(array.has(testAry, 3), true);
|
||||
assert.equal(array.has(testAry, 4), false);
|
||||
assert.equal(array.has(testAry, '1'), false);
|
||||
};
|
||||
exports.testHasAny = function(test) {
|
||||
exports.testHasAny = function(assert) {
|
||||
var testAry = [1, 2, 3];
|
||||
test.assertEqual(array.hasAny([1, 2, 3], [1]), true);
|
||||
test.assertEqual(array.hasAny([1, 2, 3], [1, 5]), true);
|
||||
test.assertEqual(array.hasAny([1, 2, 3], [5, 1]), true);
|
||||
test.assertEqual(array.hasAny([1, 2, 3], [5, 2]), true);
|
||||
test.assertEqual(array.hasAny([1, 2, 3], [5, 3]), true);
|
||||
test.assertEqual(array.hasAny([1, 2, 3], [5, 4]), false);
|
||||
test.assertEqual(testAry.length, 3);
|
||||
test.assertEqual(testAry[0], 1);
|
||||
test.assertEqual(testAry[1], 2);
|
||||
test.assertEqual(testAry[2], 3);
|
||||
test.assertEqual(array.hasAny(testAry, [2]), true);
|
||||
test.assertEqual(array.hasAny(testAry, [3]), true);
|
||||
test.assertEqual(array.hasAny(testAry, [4]), false);
|
||||
test.assertEqual(array.hasAny(testAry), false);
|
||||
test.assertEqual(array.hasAny(testAry, '1'), false);
|
||||
assert.equal(array.hasAny([1, 2, 3], [1]), true);
|
||||
assert.equal(array.hasAny([1, 2, 3], [1, 5]), true);
|
||||
assert.equal(array.hasAny([1, 2, 3], [5, 1]), true);
|
||||
assert.equal(array.hasAny([1, 2, 3], [5, 2]), true);
|
||||
assert.equal(array.hasAny([1, 2, 3], [5, 3]), true);
|
||||
assert.equal(array.hasAny([1, 2, 3], [5, 4]), false);
|
||||
assert.equal(testAry.length, 3);
|
||||
assert.equal(testAry[0], 1);
|
||||
assert.equal(testAry[1], 2);
|
||||
assert.equal(testAry[2], 3);
|
||||
assert.equal(array.hasAny(testAry, [2]), true);
|
||||
assert.equal(array.hasAny(testAry, [3]), true);
|
||||
assert.equal(array.hasAny(testAry, [4]), false);
|
||||
assert.equal(array.hasAny(testAry), false);
|
||||
assert.equal(array.hasAny(testAry, '1'), false);
|
||||
};
|
||||
|
||||
exports.testAdd = function(test) {
|
||||
exports.testAdd = function(assert) {
|
||||
var testAry = [1];
|
||||
test.assertEqual(array.add(testAry, 1), false);
|
||||
test.assertEqual(testAry.length, 1);
|
||||
test.assertEqual(testAry[0], 1);
|
||||
test.assertEqual(array.add(testAry, 2), true);
|
||||
test.assertEqual(testAry.length, 2);
|
||||
test.assertEqual(testAry[0], 1);
|
||||
test.assertEqual(testAry[1], 2);
|
||||
assert.equal(array.add(testAry, 1), false);
|
||||
assert.equal(testAry.length, 1);
|
||||
assert.equal(testAry[0], 1);
|
||||
assert.equal(array.add(testAry, 2), true);
|
||||
assert.equal(testAry.length, 2);
|
||||
assert.equal(testAry[0], 1);
|
||||
assert.equal(testAry[1], 2);
|
||||
};
|
||||
|
||||
exports.testRemove = function(test) {
|
||||
exports.testRemove = function(assert) {
|
||||
var testAry = [1, 2];
|
||||
test.assertEqual(array.remove(testAry, 3), false);
|
||||
test.assertEqual(testAry.length, 2);
|
||||
test.assertEqual(testAry[0], 1);
|
||||
test.assertEqual(testAry[1], 2);
|
||||
test.assertEqual(array.remove(testAry, 2), true);
|
||||
test.assertEqual(testAry.length, 1);
|
||||
test.assertEqual(testAry[0], 1);
|
||||
assert.equal(array.remove(testAry, 3), false);
|
||||
assert.equal(testAry.length, 2);
|
||||
assert.equal(testAry[0], 1);
|
||||
assert.equal(testAry[1], 2);
|
||||
assert.equal(array.remove(testAry, 2), true);
|
||||
assert.equal(testAry.length, 1);
|
||||
assert.equal(testAry[0], 1);
|
||||
};
|
||||
|
||||
exports.testFlatten = function(test) {
|
||||
test.assertEqual(array.flatten([1, 2, 3]).length, 3);
|
||||
test.assertEqual(array.flatten([1, [2, 3]]).length, 3);
|
||||
test.assertEqual(array.flatten([1, [2, [3]]]).length, 3);
|
||||
test.assertEqual(array.flatten([[1], [[2, [3]]]]).length, 3);
|
||||
exports.testFlatten = function(assert) {
|
||||
assert.equal(array.flatten([1, 2, 3]).length, 3);
|
||||
assert.equal(array.flatten([1, [2, 3]]).length, 3);
|
||||
assert.equal(array.flatten([1, [2, [3]]]).length, 3);
|
||||
assert.equal(array.flatten([[1], [[2, [3]]]]).length, 3);
|
||||
};
|
||||
|
||||
exports.testUnique = function(test) {
|
||||
exports.testUnique = function(assert) {
|
||||
var Class = function () {};
|
||||
var A = {};
|
||||
var B = new Class();
|
||||
|
@ -73,23 +73,31 @@ exports.testUnique = function(test) {
|
|||
var D = {};
|
||||
var E = new Class();
|
||||
|
||||
compareArray(array.unique([1,2,3,1,2]), [1,2,3]);
|
||||
compareArray(array.unique([1,1,1,4,9,5,5]), [1,4,9,5]);
|
||||
compareArray(array.unique([A, A, A, B, B, D]), [A,B,D]);
|
||||
compareArray(array.unique([A, D, A, E, E, D, A, A, C]), [A, D, E, C])
|
||||
|
||||
function compareArray (a, b) {
|
||||
test.assertEqual(a.length, b.length);
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
test.assertEqual(a[i], b[i]);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(array.unique([1,2,3,1,2]), [1,2,3]);
|
||||
assert.deepEqual(array.unique([1,1,1,4,9,5,5]), [1,4,9,5]);
|
||||
assert.deepEqual(array.unique([A, A, A, B, B, D]), [A,B,D]);
|
||||
assert.deepEqual(array.unique([A, D, A, E, E, D, A, A, C]), [A, D, E, C])
|
||||
};
|
||||
|
||||
exports.testFind = function(test) {
|
||||
exports.testUnion = function(assert) {
|
||||
var Class = function () {};
|
||||
var A = {};
|
||||
var B = new Class();
|
||||
var C = [ 1, 2, 3 ];
|
||||
var D = {};
|
||||
var E = new Class();
|
||||
|
||||
assert.deepEqual(array.union([1, 2, 3],[7, 1, 2]), [1, 2, 3, 7]);
|
||||
assert.deepEqual(array.union([1, 1, 1, 4, 9, 5, 5], [10, 1, 5]), [1, 4, 9, 5, 10]);
|
||||
assert.deepEqual(array.union([A, B], [A, D]), [A, B, D]);
|
||||
assert.deepEqual(array.union([A, D], [A, E], [E, D, A], [A, C]), [A, D, E, C]);
|
||||
};
|
||||
|
||||
exports.testFind = function(assert) {
|
||||
let isOdd = (x) => x % 2;
|
||||
test.assertEqual(array.find([2, 4, 5, 7, 8, 9], isOdd), 5);
|
||||
test.assertEqual(array.find([2, 4, 6, 8], isOdd), undefined);
|
||||
test.assertEqual(array.find([2, 4, 6, 8], isOdd, null), null);
|
||||
assert.equal(array.find([2, 4, 5, 7, 8, 9], isOdd), 5);
|
||||
assert.equal(array.find([2, 4, 6, 8], isOdd), undefined);
|
||||
assert.equal(array.find([2, 4, 6, 8], isOdd, null), null);
|
||||
};
|
||||
|
||||
require('test').run(exports);
|
||||
|
|
|
@ -4,6 +4,12 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
engines: {
|
||||
"Firefox": "*"
|
||||
}
|
||||
};
|
||||
|
||||
const { Loader } = require("sdk/test/loader");
|
||||
const { open, getMostRecentBrowserWindow, getOuterId } = require("sdk/window/utils");
|
||||
const { setTimeout } = require("sdk/timers");
|
||||
|
|
|
@ -44,13 +44,17 @@ exports["test multiple tabs"] = function(assert, done) {
|
|||
on(events, "data", function({type, target, timeStamp}) {
|
||||
// ignore about:blank pages and *-document-global-created
|
||||
// events that are not very consistent.
|
||||
// ignore http:// requests, as Fennec's `about:home` page
|
||||
// displays add-ons a user could install
|
||||
if (target.URL !== "about:blank" &&
|
||||
target.URL !== "about:home" &&
|
||||
!target.URL.match(/^https?:\/\//i) &&
|
||||
type !== "chrome-document-global-created" &&
|
||||
type !== "content-document-global-created")
|
||||
actual.push(type + " -> " + target.URL)
|
||||
});
|
||||
|
||||
let window = getMostRecentBrowserWindow();
|
||||
let window = getMostRecentBrowserWindow();
|
||||
let firstTab = open("data:text/html,first-tab", window);
|
||||
|
||||
when("pageshow", firstTab).
|
||||
|
|
|
@ -429,8 +429,3 @@ if (isWindows) {
|
|||
};
|
||||
|
||||
require('test').run(exports);
|
||||
|
||||
// Test disabled on OSX because of bug 891698
|
||||
if (require("sdk/system/runtime").OS == "Darwin")
|
||||
module.exports = {};
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
module.metadata = {
|
||||
'engines': {
|
||||
'Firefox': '*'
|
||||
}
|
||||
};
|
||||
|
||||
const { Cc, Ci } = require('chrome');
|
||||
const { defer, all } = require('sdk/core/promise');
|
||||
const { filter } = require('sdk/event/utils');
|
||||
const { on, off } = require('sdk/event/core');
|
||||
const { events } = require('sdk/places/events');
|
||||
const { setTimeout } = require('sdk/timers');
|
||||
const { before, after } = require('sdk/test/utils');
|
||||
const {
|
||||
search
|
||||
} = require('sdk/places/history');
|
||||
const {
|
||||
invalidResolve, invalidReject, createTree, createBookmark,
|
||||
compareWithHost, addVisits, resetPlaces, createBookmarkItem,
|
||||
removeVisits
|
||||
} = require('./places-helper');
|
||||
const { save, MENU, UNSORTED } = require('sdk/places/bookmarks');
|
||||
const { promisedEmitter } = require('sdk/places/utils');
|
||||
|
||||
exports['test bookmark-item-added'] = function (assert, done) {
|
||||
function handler ({type, data}) {
|
||||
if (type !== 'bookmark-item-added') return;
|
||||
if (data.title !== 'bookmark-added-title') return;
|
||||
|
||||
assert.equal(type, 'bookmark-item-added', 'correct type in bookmark-added event');
|
||||
assert.equal(data.type, 'bookmark', 'correct data in bookmark-added event');
|
||||
assert.ok(data.id != null, 'correct data in bookmark-added event');
|
||||
assert.ok(data.parentId != null, 'correct data in bookmark-added event');
|
||||
assert.ok(data.index != null, 'correct data in bookmark-added event');
|
||||
assert.equal(data.url, 'http://moz.com/', 'correct data in bookmark-added event');
|
||||
assert.ok(data.dateAdded != null, 'correct data in bookmark-added event');
|
||||
events.off('data', handler);
|
||||
done();
|
||||
}
|
||||
events.on('data', handler);
|
||||
createBookmark({ title: 'bookmark-added-title' });
|
||||
};
|
||||
|
||||
exports['test bookmark-item-changed'] = function (assert, done) {
|
||||
let id;
|
||||
let complete = makeCompleted(done);
|
||||
function handler ({type, data}) {
|
||||
if (type !== 'bookmark-item-changed') return;
|
||||
if (data.id !== id) return;
|
||||
assert.equal(type, 'bookmark-item-changed',
|
||||
'correct type in bookmark-item-changed event');
|
||||
assert.equal(data.type, 'bookmark',
|
||||
'correct data in bookmark-item-changed event');
|
||||
assert.equal(data.property, 'title',
|
||||
'correct property in bookmark-item-changed event');
|
||||
assert.equal(data.value, 'bookmark-changed-title-2',
|
||||
'correct value in bookmark-item-changed event');
|
||||
assert.ok(data.id === id, 'correct id in bookmark-item-changed event');
|
||||
assert.ok(data.parentId != null, 'correct data in bookmark-added event');
|
||||
|
||||
events.off('data', handler);
|
||||
complete();
|
||||
}
|
||||
events.on('data', handler);
|
||||
|
||||
createBookmarkItem({ title: 'bookmark-changed-title' }).then(item => {
|
||||
id = item.id;
|
||||
item.title = 'bookmark-changed-title-2';
|
||||
return saveP(item);
|
||||
}).then(complete);
|
||||
};
|
||||
|
||||
exports['test bookmark-item-moved'] = function (assert, done) {
|
||||
let id;
|
||||
let complete = makeCompleted(done);
|
||||
function handler ({type, data}) {
|
||||
if (type !== 'bookmark-item-moved') return;
|
||||
if (data.id !== id) return;
|
||||
assert.equal(type, 'bookmark-item-moved',
|
||||
'correct type in bookmark-item-moved event');
|
||||
assert.equal(data.type, 'bookmark',
|
||||
'correct data in bookmark-item-moved event');
|
||||
assert.ok(data.id === id, 'correct id in bookmark-item-moved event');
|
||||
assert.equal(data.previousParentId, UNSORTED.id,
|
||||
'correct previousParentId');
|
||||
assert.equal(data.currentParentId, MENU.id,
|
||||
'correct currentParentId');
|
||||
assert.equal(data.previousIndex, 0, 'correct previousIndex');
|
||||
assert.equal(data.currentIndex, 0, 'correct currentIndex');
|
||||
|
||||
events.off('data', handler);
|
||||
complete();
|
||||
}
|
||||
events.on('data', handler);
|
||||
|
||||
createBookmarkItem({
|
||||
title: 'bookmark-moved-title',
|
||||
group: UNSORTED
|
||||
}).then(item => {
|
||||
id = item.id;
|
||||
item.group = MENU;
|
||||
return saveP(item);
|
||||
}).then(complete);
|
||||
};
|
||||
|
||||
exports['test bookmark-item-removed'] = function (assert, done) {
|
||||
let id;
|
||||
let complete = makeCompleted(done);
|
||||
function handler ({type, data}) {
|
||||
if (type !== 'bookmark-item-removed') return;
|
||||
if (data.id !== id) return;
|
||||
assert.equal(type, 'bookmark-item-removed',
|
||||
'correct type in bookmark-item-removed event');
|
||||
assert.equal(data.type, 'bookmark',
|
||||
'correct data in bookmark-item-removed event');
|
||||
assert.ok(data.id === id, 'correct id in bookmark-item-removed event');
|
||||
assert.equal(data.parentId, UNSORTED.id,
|
||||
'correct parentId in bookmark-item-removed');
|
||||
assert.equal(data.url, 'http://moz.com/',
|
||||
'correct url in bookmark-item-removed event');
|
||||
assert.equal(data.index, 0,
|
||||
'correct index in bookmark-item-removed event');
|
||||
|
||||
events.off('data', handler);
|
||||
complete();
|
||||
}
|
||||
events.on('data', handler);
|
||||
|
||||
createBookmarkItem({
|
||||
title: 'bookmark-item-remove-title',
|
||||
group: UNSORTED
|
||||
}).then(item => {
|
||||
id = item.id;
|
||||
item.remove = true;
|
||||
return saveP(item);
|
||||
}).then(complete);
|
||||
};
|
||||
|
||||
exports['test bookmark-item-visited'] = function (assert, done) {
|
||||
let id;
|
||||
let complete = makeCompleted(done);
|
||||
function handler ({type, data}) {
|
||||
if (type !== 'bookmark-item-visited') return;
|
||||
if (data.id !== id) return;
|
||||
assert.equal(type, 'bookmark-item-visited',
|
||||
'correct type in bookmark-item-visited event');
|
||||
assert.ok(data.id === id, 'correct id in bookmark-item-visited event');
|
||||
assert.equal(data.parentId, UNSORTED.id,
|
||||
'correct parentId in bookmark-item-visited');
|
||||
assert.ok(data.transitionType != null,
|
||||
'has a transition type in bookmark-item-visited event');
|
||||
assert.ok(data.time != null,
|
||||
'has a time in bookmark-item-visited event');
|
||||
assert.ok(data.visitId != null,
|
||||
'has a visitId in bookmark-item-visited event');
|
||||
assert.equal(data.url, 'http://bookmark-item-visited.com/',
|
||||
'correct url in bookmark-item-visited event');
|
||||
|
||||
events.off('data', handler);
|
||||
complete();
|
||||
}
|
||||
events.on('data', handler);
|
||||
|
||||
createBookmarkItem({
|
||||
title: 'bookmark-item-visited',
|
||||
url: 'http://bookmark-item-visited.com/'
|
||||
}).then(item => {
|
||||
id = item.id;
|
||||
return addVisits('http://bookmark-item-visited.com/');
|
||||
}).then(complete);
|
||||
};
|
||||
|
||||
exports['test history-start-batch, history-end-batch, history-start-clear'] = function (assert, done) {
|
||||
let complete = makeCompleted(done, 4);
|
||||
let startEvent = filter(events, ({type}) => type === 'history-start-batch');
|
||||
let endEvent = filter(events, ({type}) => type === 'history-end-batch');
|
||||
let clearEvent = filter(events, ({type}) => type === 'history-start-clear');
|
||||
function startHandler ({type, data}) {
|
||||
assert.pass('history-start-batch called');
|
||||
assert.equal(type, 'history-start-batch',
|
||||
'history-start-batch has correct type');
|
||||
off(startEvent, 'data', startHandler);
|
||||
on(endEvent, 'data', endHandler);
|
||||
complete();
|
||||
}
|
||||
function endHandler ({type, data}) {
|
||||
assert.pass('history-end-batch called');
|
||||
assert.equal(type, 'history-end-batch',
|
||||
'history-end-batch has correct type');
|
||||
off(endEvent, 'data', endHandler);
|
||||
complete();
|
||||
}
|
||||
function clearHandler ({type, data}) {
|
||||
assert.pass('history-start-clear called');
|
||||
assert.equal(type, 'history-start-clear',
|
||||
'history-start-clear has correct type');
|
||||
off(clearEvent, 'data', clearHandler);
|
||||
complete();
|
||||
}
|
||||
|
||||
on(startEvent, 'data', startHandler);
|
||||
on(clearEvent, 'data', clearHandler);
|
||||
|
||||
createBookmark().then(() => {
|
||||
resetPlaces(complete);
|
||||
})
|
||||
};
|
||||
|
||||
exports['test history-visit, history-title-changed'] = function (assert, done) {
|
||||
let complete = makeCompleted(() => {
|
||||
off(titleEvents, 'data', titleHandler);
|
||||
off(visitEvents, 'data', visitHandler);
|
||||
done();
|
||||
}, 6);
|
||||
let visitEvents = filter(events, ({type}) => type === 'history-visit');
|
||||
let titleEvents = filter(events, ({type}) => type === 'history-title-changed');
|
||||
|
||||
let urls = ['http://moz.com/', 'http://firefox.com/', 'http://mdn.com/'];
|
||||
|
||||
function visitHandler ({type, data}) {
|
||||
assert.equal(type, 'history-visit', 'correct type in history-visit');
|
||||
assert.ok(~urls.indexOf(data.url), 'history-visit has correct url');
|
||||
assert.ok(data.visitId != null, 'history-visit has a visitId');
|
||||
assert.ok(data.time != null, 'history-visit has a time');
|
||||
assert.ok(data.sessionId != null, 'history-visit has a sessionId');
|
||||
assert.ok(data.referringId != null, 'history-visit has a referringId');
|
||||
assert.ok(data.transitionType != null, 'history-visit has a transitionType');
|
||||
complete();
|
||||
}
|
||||
|
||||
function titleHandler ({type, data}) {
|
||||
assert.equal(type, 'history-title-changed',
|
||||
'correct type in history-title-changed');
|
||||
assert.ok(~urls.indexOf(data.url),
|
||||
'history-title-changed has correct url');
|
||||
assert.ok(data.title, 'history-title-changed has title');
|
||||
complete();
|
||||
}
|
||||
|
||||
on(titleEvents, 'data', titleHandler);
|
||||
on(visitEvents, 'data', visitHandler);
|
||||
addVisits(urls);
|
||||
}
|
||||
|
||||
exports['test history-delete-url'] = function (assert, done) {
|
||||
let complete = makeCompleted(() => {
|
||||
events.off('data', handler);
|
||||
done();
|
||||
}, 3);
|
||||
let urls = ['http://moz.com/', 'http://firefox.com/', 'http://mdn.com/'];
|
||||
function handler({type, data}) {
|
||||
if (type !== 'history-delete-url') return;
|
||||
assert.equal(type, 'history-delete-url',
|
||||
'history-delete-url has correct type');
|
||||
assert.ok(~urls.indexOf(data.url), 'history-delete-url has correct url');
|
||||
complete();
|
||||
}
|
||||
|
||||
events.on('data', handler);
|
||||
addVisits(urls).then(() => {
|
||||
removeVisits(urls);
|
||||
});
|
||||
};
|
||||
|
||||
exports['test history-page-changed'] = function (assert) {
|
||||
assert.pass('history-page-changed tested in test-places-favicons');
|
||||
};
|
||||
|
||||
exports['test history-delete-visits'] = function (assert) {
|
||||
assert.pass('TODO test history-delete-visits');
|
||||
};
|
||||
|
||||
before(exports, (name, assert, done) => resetPlaces(done));
|
||||
after(exports, (name, assert, done) => resetPlaces(done));
|
||||
|
||||
function saveP () {
|
||||
return promisedEmitter(save.apply(null, Array.slice(arguments)));
|
||||
}
|
||||
|
||||
function makeCompleted (done, countTo) {
|
||||
let count = 0;
|
||||
countTo = countTo || 2;
|
||||
return function () {
|
||||
if (++count === countTo) done();
|
||||
};
|
||||
}
|
||||
require('sdk/test').run(exports);
|
|
@ -35,7 +35,7 @@ const tagsrv = Cc['@mozilla.org/browser/tagging-service;1'].
|
|||
exports.testBookmarksCreate = function (assert, done) {
|
||||
let items = [{
|
||||
title: 'my title',
|
||||
url: 'http://moz.com',
|
||||
url: 'http://test-places-host.com/testBookmarksCreate/',
|
||||
tags: ['some', 'tags', 'yeah'],
|
||||
type: 'bookmark'
|
||||
}, {
|
||||
|
@ -71,26 +71,26 @@ exports.testBookmarksCreateFail = function (assert, done) {
|
|||
return send('sdk-places-bookmarks-create', item).then(null, function (reason) {
|
||||
assert.ok(reason, 'bookmark create should fail');
|
||||
});
|
||||
})).then(function () {
|
||||
done();
|
||||
});
|
||||
})).then(done);
|
||||
};
|
||||
|
||||
exports.testBookmarkLastUpdated = function (assert, done) {
|
||||
let timestamp;
|
||||
let item;
|
||||
createBookmark().then(function (data) {
|
||||
createBookmark({
|
||||
url: 'http://test-places-host.com/testBookmarkLastUpdated'
|
||||
}).then(function (data) {
|
||||
item = data;
|
||||
timestamp = item.updated;
|
||||
return send('sdk-places-bookmarks-last-updated', { id: item.id });
|
||||
}).then(function (updated) {
|
||||
let { resolve, promise } = defer();
|
||||
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;
|
||||
});
|
||||
setTimeout(() => {
|
||||
resolve(send('sdk-places-bookmarks-save', item));
|
||||
}, 100);
|
||||
return promise;
|
||||
}).then(function (data) {
|
||||
assert.ok(data.updated > timestamp, 'time has elapsed and updated the updated property');
|
||||
done();
|
||||
|
@ -99,7 +99,9 @@ exports.testBookmarkLastUpdated = function (assert, done) {
|
|||
|
||||
exports.testBookmarkRemove = function (assert, done) {
|
||||
let id;
|
||||
createBookmark().then(function (data) {
|
||||
createBookmark({
|
||||
url: 'http://test-places-host.com/testBookmarkRemove/'
|
||||
}).then(function (data) {
|
||||
id = data.id;
|
||||
compareWithHost(assert, data); // ensure bookmark exists
|
||||
bmsrv.getItemTitle(id); // does not throw an error
|
||||
|
@ -114,7 +116,9 @@ exports.testBookmarkRemove = function (assert, done) {
|
|||
|
||||
exports.testBookmarkGet = function (assert, done) {
|
||||
let bookmark;
|
||||
createBookmark().then(function (data) {
|
||||
createBookmark({
|
||||
url: 'http://test-places-host.com/testBookmarkGet/'
|
||||
}).then(function (data) {
|
||||
bookmark = data;
|
||||
return send('sdk-places-bookmarks-get', { id: data.id });
|
||||
}).then(function (data) {
|
||||
|
@ -136,7 +140,9 @@ exports.testBookmarkGet = function (assert, done) {
|
|||
|
||||
exports.testTagsTag = function (assert, done) {
|
||||
let url;
|
||||
createBookmark().then(function (data) {
|
||||
createBookmark({
|
||||
url: 'http://test-places-host.com/testTagsTag/',
|
||||
}).then(function (data) {
|
||||
url = data.url;
|
||||
return send('sdk-places-tags-tag', {
|
||||
url: data.url, tags: ['mozzerella', 'foxfire']
|
||||
|
@ -153,7 +159,10 @@ exports.testTagsTag = function (assert, done) {
|
|||
|
||||
exports.testTagsUntag = function (assert, done) {
|
||||
let item;
|
||||
createBookmark({tags: ['tag1', 'tag2', 'tag3']}).then(function (data) {
|
||||
createBookmark({
|
||||
url: 'http://test-places-host.com/testTagsUntag/',
|
||||
tags: ['tag1', 'tag2', 'tag3']
|
||||
}).then(data => {
|
||||
item = data;
|
||||
return send('sdk-places-tags-untag', {
|
||||
url: item.url,
|
||||
|
@ -172,7 +181,9 @@ exports.testTagsUntag = function (assert, done) {
|
|||
|
||||
exports.testTagsGetURLsByTag = function (assert, done) {
|
||||
let item;
|
||||
createBookmark().then(function (data) {
|
||||
createBookmark({
|
||||
url: 'http://test-places-host.com/testTagsGetURLsByTag/'
|
||||
}).then(function (data) {
|
||||
item = data;
|
||||
return send('sdk-places-tags-get-urls-by-tag', {
|
||||
tag: 'firefox'
|
||||
|
@ -186,7 +197,10 @@ exports.testTagsGetURLsByTag = function (assert, done) {
|
|||
|
||||
exports.testTagsGetTagsByURL = function (assert, done) {
|
||||
let item;
|
||||
createBookmark({ tags: ['firefox', 'mozilla', 'metal']}).then(function (data) {
|
||||
createBookmark({
|
||||
url: 'http://test-places-host.com/testTagsGetURLsByTag/',
|
||||
tags: ['firefox', 'mozilla', 'metal']
|
||||
}).then(function (data) {
|
||||
item = data;
|
||||
return send('sdk-places-tags-get-tags-by-url', {
|
||||
url: data.url,
|
||||
|
@ -202,9 +216,15 @@ exports.testTagsGetTagsByURL = function (assert, 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' })
|
||||
createBookmark({
|
||||
url: 'http://firefox.com/testHostQuery/',
|
||||
tags: ['firefox', 'mozilla']
|
||||
}),
|
||||
createBookmark({
|
||||
url: 'http://mozilla.com/testHostQuery/',
|
||||
tags: ['mozilla']
|
||||
}),
|
||||
createBookmark({ url: 'http://thunderbird.com/testHostQuery/' })
|
||||
]).then(data => {
|
||||
return send('sdk-places-query', {
|
||||
queries: { tags: ['mozilla'] },
|
||||
|
@ -212,34 +232,44 @@ exports.testHostQuery = function (assert, done) {
|
|||
});
|
||||
}).then(results => {
|
||||
assert.equal(results.length, 2, 'should only return two');
|
||||
assert.equal(results[0].url, 'http://mozilla.com/', 'is sorted by URI asc');
|
||||
assert.equal(results[0].url,
|
||||
'http://mozilla.com/testHostQuery/', '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');
|
||||
assert.equal(results[0].url,
|
||||
'http://firefox.com/testHostQuery/', 'is sorted by URI desc');
|
||||
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' })
|
||||
createBookmark({
|
||||
url: 'http://firefox.com/testHostMultiQuery/',
|
||||
tags: ['firefox', 'mozilla']
|
||||
}),
|
||||
createBookmark({
|
||||
url: 'http://mozilla.com/testHostMultiQuery/',
|
||||
tags: ['mozilla']
|
||||
}),
|
||||
createBookmark({ url: 'http://thunderbird.com/testHostMultiQuery/' })
|
||||
]).then(data => {
|
||||
return send('sdk-places-query', {
|
||||
queries: [{ tags: ['firefox'] }, { uri: 'http://thunderbird.com/' }],
|
||||
queries: [{ tags: ['firefox'] }, { uri: 'http://thunderbird.com/testHostMultiQuery/' }],
|
||||
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');
|
||||
assert.equal(results[0].url,
|
||||
'http://firefox.com/testHostMultiQuery/', 'should match URL or tag');
|
||||
assert.equal(results[1].url,
|
||||
'http://thunderbird.com/testHostMultiQuery/', 'should match URL or tag');
|
||||
return send('sdk-places-query', {
|
||||
queries: [{ tags: ['firefox'], url: 'http://mozilla.com/' }],
|
||||
queries: [{ tags: ['firefox'], url: 'http://mozilla.com/testHostMultiQuery/' }],
|
||||
options: { sortingMode: 5, queryType: 1 } // sort by URI descending, bookmarks only
|
||||
});
|
||||
}).then(results => {
|
||||
|
@ -269,7 +299,6 @@ exports.testGetAllChildren = function (assert, done) {
|
|||
});
|
||||
};
|
||||
|
||||
|
||||
before(exports, (name, assert, done) => resetPlaces(done));
|
||||
after(exports, (name, assert, done) => resetPlaces(done));
|
||||
|
||||
|
|
|
@ -457,3 +457,100 @@ exports.testTabReload = function(test) {
|
|||
}
|
||||
});
|
||||
};
|
||||
|
||||
exports.testOnPageShowEvent = function (test) {
|
||||
test.waitUntilDone();
|
||||
|
||||
let events = [];
|
||||
let firstUrl = 'data:text/html;charset=utf-8,First';
|
||||
let secondUrl = 'data:text/html;charset=utf-8,Second';
|
||||
|
||||
let counter = 0;
|
||||
function onPageShow (tab, persisted) {
|
||||
events.push('pageshow');
|
||||
counter++;
|
||||
if (counter === 1) {
|
||||
test.assertEqual(persisted, false, 'page should not be cached on initial load');
|
||||
tab.url = secondUrl;
|
||||
}
|
||||
else if (counter === 2) {
|
||||
test.assertEqual(persisted, false, 'second test page should not be cached either');
|
||||
tab.attach({
|
||||
contentScript: 'setTimeout(function () { window.history.back(); }, 0)'
|
||||
});
|
||||
}
|
||||
else {
|
||||
test.assertEqual(persisted, true, 'when we get back to the fist page, it has to' +
|
||||
'come from cache');
|
||||
tabs.removeListener('pageshow', onPageShow);
|
||||
tabs.removeListener('open', onOpen);
|
||||
tabs.removeListener('ready', onReady);
|
||||
tab.close(() => {
|
||||
['open', 'ready', 'pageshow', 'ready',
|
||||
'pageshow', 'pageshow'].map((type, i) => {
|
||||
test.assertEqual(type, events[i], 'correct ordering of events');
|
||||
});
|
||||
test.done()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onOpen () events.push('open');
|
||||
function onReady () events.push('ready');
|
||||
|
||||
tabs.on('pageshow', onPageShow);
|
||||
tabs.on('open', onOpen);
|
||||
tabs.on('ready', onReady);
|
||||
tabs.open({
|
||||
url: firstUrl
|
||||
});
|
||||
};
|
||||
|
||||
exports.testOnPageShowEventDeclarative = function (test) {
|
||||
test.waitUntilDone();
|
||||
|
||||
let events = [];
|
||||
let firstUrl = 'data:text/html;charset=utf-8,First';
|
||||
let secondUrl = 'data:text/html;charset=utf-8,Second';
|
||||
|
||||
let counter = 0;
|
||||
function onPageShow (tab, persisted) {
|
||||
events.push('pageshow');
|
||||
counter++;
|
||||
if (counter === 1) {
|
||||
test.assertEqual(persisted, false, 'page should not be cached on initial load');
|
||||
tab.url = secondUrl;
|
||||
}
|
||||
else if (counter === 2) {
|
||||
test.assertEqual(persisted, false, 'second test page should not be cached either');
|
||||
tab.attach({
|
||||
contentScript: 'setTimeout(function () { window.history.back(); }, 0)'
|
||||
});
|
||||
}
|
||||
else {
|
||||
test.assertEqual(persisted, true, 'when we get back to the fist page, it has to' +
|
||||
'come from cache');
|
||||
tabs.removeListener('pageshow', onPageShow);
|
||||
tabs.removeListener('open', onOpen);
|
||||
tabs.removeListener('ready', onReady);
|
||||
tab.close(() => {
|
||||
['open', 'ready', 'pageshow', 'ready',
|
||||
'pageshow', 'pageshow'].map((type, i) => {
|
||||
test.assertEqual(type, events[i], 'correct ordering of events');
|
||||
});
|
||||
test.done()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onOpen () events.push('open');
|
||||
function onReady () events.push('ready');
|
||||
|
||||
tabs.open({
|
||||
url: firstUrl,
|
||||
onPageShow: onPageShow,
|
||||
onOpen: onOpen,
|
||||
onReady: onReady
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -3,7 +3,15 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
'use strict';
|
||||
|
||||
const { URL, toFilename, fromFilename, isValidURI, getTLD, DataURL } = require('sdk/url');
|
||||
const {
|
||||
URL,
|
||||
toFilename,
|
||||
fromFilename,
|
||||
isValidURI,
|
||||
getTLD,
|
||||
DataURL,
|
||||
isLocalURL } = require('sdk/url');
|
||||
|
||||
const { pathFor } = require('sdk/system');
|
||||
const file = require('sdk/io/file');
|
||||
const tabs = require('sdk/tabs');
|
||||
|
@ -63,11 +71,11 @@ 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('https://www.sub.moz.com/page.html#justhash');
|
||||
assert.equal(hashOnly.search, '');
|
||||
assert.equal(hashOnly.hash, '#justhash');
|
||||
|
||||
|
||||
var queryOnly = URL('https://www.sub.moz.com/page.html?my=query');
|
||||
assert.equal(queryOnly.search, '?my=query');
|
||||
assert.equal(queryOnly.hash, '');
|
||||
|
@ -75,11 +83,11 @@ exports.testParseHttpSearchAndHash = function (assert) {
|
|||
var qMark = URL('http://www.moz.org?');
|
||||
assert.equal(qMark.search, '');
|
||||
assert.equal(qMark.hash, '');
|
||||
|
||||
|
||||
var hash = URL('http://www.moz.org#');
|
||||
assert.equal(hash.search, '');
|
||||
assert.equal(hash.hash, '');
|
||||
|
||||
|
||||
var empty = URL('http://www.moz.org?#');
|
||||
assert.equal(hash.search, '');
|
||||
assert.equal(hash.hash, '');
|
||||
|
@ -347,6 +355,39 @@ exports.testWindowLocationMatch = function (assert, done) {
|
|||
})
|
||||
};
|
||||
|
||||
exports.testURLInRegExpTest = function(assert) {
|
||||
let url = 'https://mozilla.org';
|
||||
assert.equal((new RegExp(url).test(URL(url))), true, 'URL instances work in a RegExp test');
|
||||
}
|
||||
|
||||
exports.testLocalURL = function(assert) {
|
||||
[
|
||||
'data:text/html;charset=utf-8,foo and bar',
|
||||
'data:text/plain,foo and bar',
|
||||
'resource://gre/modules/commonjs/',
|
||||
'chrome://browser/content/browser.xul'
|
||||
].forEach(aUri => {
|
||||
assert.ok(isLocalURL(aUri), aUri + ' is a Local URL');
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
exports.testLocalURLwithRemoteURL = function(assert) {
|
||||
validURIs().filter(url => !url.startsWith('data:')).forEach(aUri => {
|
||||
assert.ok(!isLocalURL(aUri), aUri + ' is an invalid Local URL');
|
||||
});
|
||||
}
|
||||
|
||||
exports.testLocalURLwithInvalidURL = function(assert) {
|
||||
invalidURIs().concat([
|
||||
'data:foo and bar',
|
||||
'resource:// must fail',
|
||||
'chrome:// here too'
|
||||
]).forEach(aUri => {
|
||||
assert.ok(!isLocalURL(aUri), aUri + ' is an invalid Local URL');
|
||||
});
|
||||
}
|
||||
|
||||
function validURIs() {
|
||||
return [
|
||||
'http://foo.com/blah_blah',
|
||||
|
|
Загрузка…
Ссылка в новой задаче