Bug 901239 - Uplift Add-on SDK to Firefox r=me

This commit is contained in:
Dave Townsend 2013-08-09 13:17:14 -07:00
Родитель 3c077e5bb9
Коммит 9b6727d39c
44 изменённых файлов: 3275 добавлений и 501 удалений

Просмотреть файл

@ -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 dont 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 wont 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 Bugzillas 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',