Merge m-c to inbound. a=merge

This commit is contained in:
Ryan VanderMeulen 2014-09-09 19:29:28 -04:00
commit 9a0967f21f
296 changed files with 2641 additions and 2313 deletions

View File

@ -0,0 +1,63 @@
## Overview
- Changes should follow the [design guidelines], as well as [coding style guide] for Jetpack
- All changes must be accompanied by tests
- In order to land, changes must have review from a core Jetpack developer
- Changes should have additional API review when needed
- Changes should have additional review from a Mozilla platform domain-expert when needed
If you have questions, ask in [#jetpack on IRC][jetpack irc channel] or on the [Jetpack mailing list].
## How to Make Code Contributions
If you have code that you'd like to contribute the Jetpack project, follow these steps:
1. Look for your issue in the [bugs already filed][open bugs]
2. If no bug exists, [submit one][submit bug]
3. Make your changes, per the Overview
4. Write a test ([intro][test intro], [API][test API])
5. Submit pull request with changes and a title in a form of `Bug XXX - description`
6. Copy the pull request link from GitHub and paste it in as an attachment to the bug
7. Each pull request should idealy contain only one commit, so squash the commits if necessary.
8. Flag the attachment for code review from one of the Jetpack reviewers listed below.
This step is optional, but could speed things up.
9. Address any nits (ie style changes), or other issues mentioned in the review.
10. Finally, once review is approved, a team member will do the merging
## Good First Bugs
There is a list of [good first bugs here](https://bugzilla.mozilla.org/buglist.cgi?list_id=7345714&columnlist=bug_severity%2Cpriority%2Cassigned_to%2Cbug_status%2Ctarget_milestone%2Cresolution%2Cshort_desc%2Cchangeddate&query_based_on=jetpack-good-1st-bugs&status_whiteboard_type=allwordssubstr&query_format=advanced&status_whiteboard=[good%20first%20bug]&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&bug_status=VERIFIED&product=Add-on%20SDK&known_name=jetpack-good-1st-bugs).
## Reviewers
All changes must be reviewed by someone on the Jetpack review crew:
- [@mossop]
- [@gozala]
- [@wbamberg]
- [@ZER0]
- [@erikvold]
- [@jsantell]
- [@zombie]
For review of Mozilla platform usage and best practices, ask [@autonome],
[@0c0w3], or [@mossop] to find the domain expert.
For API and developer ergonomics review, ask [@gozala].
[design guidelines]:https://wiki.mozilla.org/Labs/Jetpack/Design_Guidelines
[jetpack irc channel]:irc://irc.mozilla.org/#jetpack
[Jetpack mailing list]:http://groups.google.com/group/mozilla-labs-jetpack
[open bugs]:https://bugzilla.mozilla.org/buglist.cgi?quicksearch=product%3ASDK
[submit bug]:https://bugzilla.mozilla.org/enter_bug.cgi?product=Add-on%20SDK&component=general
[test intro]:https://jetpack.mozillalabs.com/sdk/latest/docs/#guide/implementing-reusable-module
[test API]:https://jetpack.mozillalabs.com/sdk/latest/docs/#module/api-utils/unit-test
[coding style guide]:https://github.com/mozilla/addon-sdk/wiki/Coding-style-guide
[@mossop]:https://github.com/mossop/
[@gozala]:https://github.com/Gozala/
[@wbamberg]:https://github.com/wbamberg/
[@ZER0]:https://github.com/ZER0/
[@erikvold]:https://github.com/erikvold/
[@jsantell]:https://github.com/jsantell
[@zombie]:https://github.com/zombie

View File

@ -135,9 +135,19 @@ TestFinder.prototype = {
let { fileFilter, testFilter } = makeFilters({ filter: this.filter }); let { fileFilter, testFilter } = makeFilters({ filter: this.filter });
return getSuites({ id: id, filter: fileFilter }).then(suites => { return getSuites({ id: id, filter: fileFilter }).then(suites => {
let tests = []; let testsRemaining = [];
let getNextTest = () => {
if (testsRemaining.length) {
return testsRemaining.shift();
}
if (!suites.length) {
return null;
}
let suite = suites.shift();
suites.forEach(suite => {
// Load each test file as a main module in its own loader instance // Load each test file as a main module in its own loader instance
// `suite` is defined by cuddlefish/manifest.py:ManifestBuilder.build // `suite` is defined by cuddlefish/manifest.py:ManifestBuilder.build
let suiteModule; let suiteModule;
@ -162,7 +172,7 @@ TestFinder.prototype = {
if (this.testInProcess) { if (this.testInProcess) {
for (let name of Object.keys(suiteModule).sort()) { for (let name of Object.keys(suiteModule).sort()) {
if (NOT_TESTS.indexOf(name) === -1 && testFilter(name)) { if (NOT_TESTS.indexOf(name) === -1 && testFilter(name)) {
tests.push({ testsRemaining.push({
setup: suiteModule.setup, setup: suiteModule.setup,
teardown: suiteModule.teardown, teardown: suiteModule.teardown,
testFunction: suiteModule[name], testFunction: suiteModule[name],
@ -171,9 +181,13 @@ TestFinder.prototype = {
} }
} }
} }
})
return tests; return getNextTest();
};
return {
getNext: () => resolve(getNextTest())
};
}); });
} }
}; };

View File

@ -11,8 +11,9 @@ const memory = require("./memory");
const timer = require("../timers"); const timer = require("../timers");
const cfxArgs = require("../test/options"); const cfxArgs = require("../test/options");
const { getTabs, closeTab, getURI } = require("../tabs/utils"); const { getTabs, closeTab, getURI } = require("../tabs/utils");
const { windows, isBrowser } = require("../window/utils"); const { windows, isBrowser, getMostRecentBrowserWindow } = require("../window/utils");
const { defer, all, Debugging: PromiseDebugging } = require("../core/promise"); const { defer, all, Debugging: PromiseDebugging } = require("../core/promise");
const { getInnerId } = require("../window/utils");
const findAndRunTests = function findAndRunTests(options) { const findAndRunTests = function findAndRunTests(options) {
var TestFinder = require("./unit-test-finder").TestFinder; var TestFinder = require("./unit-test-finder").TestFinder;
@ -32,11 +33,13 @@ const findAndRunTests = function findAndRunTests(options) {
}; };
exports.findAndRunTests = findAndRunTests; exports.findAndRunTests = findAndRunTests;
let runnerWindows = new WeakMap();
const TestRunner = function TestRunner(options) { const TestRunner = function TestRunner(options) {
if (options) { options = options || {};
this.fs = options.fs; runnerWindows.set(this, getInnerId(getMostRecentBrowserWindow()));
} this.fs = options.fs;
this.console = (options && "console" in options) ? options.console : console; this.console = options.console || console;
memory.track(this); memory.track(this);
this.passed = 0; this.passed = 0;
this.failed = 0; this.failed = 0;
@ -314,7 +317,7 @@ TestRunner.prototype = {
} }
let leftover = tabs.slice(1); let leftover = tabs.slice(1);
if (wins.length != 1) if (wins.length != 1 || getInnerId(wins[0]) !== runnerWindows.get(this))
this.fail("Should not be any unexpected windows open"); this.fail("Should not be any unexpected windows open");
if (tabs.length != 1) if (tabs.length != 1)
this.fail("Should not be any unexpected tabs open"); this.fail("Should not be any unexpected tabs open");
@ -483,17 +486,23 @@ TestRunner.prototype = {
startMany: function startMany(options) { startMany: function startMany(options) {
function runNextTest(self) { function runNextTest(self) {
var test = options.tests.shift(); let { tests, onDone } = options;
if (options.stopOnError && self.test && self.test.failed) {
self.console.error("aborted: test failed and --stop-on-error was specified"); return tests.getNext().then((test) => {
options.onDone(self); if (options.stopOnError && self.test && self.test.failed) {
} else if (test) { self.console.error("aborted: test failed and --stop-on-error was specified");
self.start({test: test, onDone: runNextTest}); onDone(self);
} else { }
options.onDone(self); else if (test) {
} self.start({test: test, onDone: runNextTest});
}
else {
onDone(self);
}
});
} }
runNextTest(this);
return runNextTest(this).catch(console.exception);
}, },
start: function start(options) { start: function start(options) {

View File

@ -10,6 +10,7 @@ module.metadata = {
var { exit, stdout } = require("../system"); var { exit, stdout } = require("../system");
var cfxArgs = require("../test/options"); var cfxArgs = require("../test/options");
var events = require("../system/events"); var events = require("../system/events");
const { resolve } = require("../core/promise");
function runTests(findAndRunTests) { function runTests(findAndRunTests) {
var harness = require("./harness"); var harness = require("./harness");
@ -120,7 +121,9 @@ exports.runTestsFromModule = function runTestsFromModule(module) {
var { TestRunner } = loader.require("../deprecated/unit-test"); var { TestRunner } = loader.require("../deprecated/unit-test");
var runner = new TestRunner(); var runner = new TestRunner();
runner.startMany({ runner.startMany({
tests: tests, tests: {
getNext: () => resolve(tests.shift())
},
stopOnError: cfxArgs.stopOnError, stopOnError: cfxArgs.stopOnError,
onDone: nextIteration onDone: nextIteration
}); });

View File

@ -22,8 +22,8 @@ DEFAULT_ICON = 'icon.png'
DEFAULT_ICON64 = 'icon64.png' DEFAULT_ICON64 = 'icon64.png'
METADATA_PROPS = ['name', 'description', 'keywords', 'author', 'version', METADATA_PROPS = ['name', 'description', 'keywords', 'author', 'version',
'translators', 'contributors', 'license', 'homepage', 'icon', 'developers', 'translators', 'contributors', 'license', 'homepage',
'icon64', 'main', 'directories', 'permissions', 'preferences'] 'icon', 'icon64', 'main', 'directories', 'permissions', 'preferences']
RESOURCE_HOSTNAME_RE = re.compile(r'^[a-z0-9_\-]+$') RESOURCE_HOSTNAME_RE = re.compile(r'^[a-z0-9_\-]+$')

View File

@ -138,6 +138,11 @@ def gen_manifest(template_root_dir, target_cfg, jid,
elem.appendChild(dom.createTextNode(translator)) elem.appendChild(dom.createTextNode(translator))
dom.documentElement.getElementsByTagName("Description")[0].appendChild(elem) dom.documentElement.getElementsByTagName("Description")[0].appendChild(elem)
for developer in target_cfg.get("developers", [ ]):
elem = dom.createElement("em:developer");
elem.appendChild(dom.createTextNode(developer))
dom.documentElement.getElementsByTagName("Description")[0].appendChild(elem)
for contributor in target_cfg.get("contributors", [ ]): for contributor in target_cfg.get("contributors", [ ]):
elem = dom.createElement("em:contributor"); elem = dom.createElement("em:contributor");
elem.appendChild(dom.createTextNode(contributor)) elem.appendChild(dom.createTextNode(contributor))
@ -150,7 +155,7 @@ def gen_manifest(template_root_dir, target_cfg, jid,
if target_cfg.get("preferences"): if target_cfg.get("preferences"):
manifest.set("em:optionsType", "2") manifest.set("em:optionsType", "2")
# workaround until bug 971249 is fixed # workaround until bug 971249 is fixed
# https://bugzilla.mozilla.org/show_bug.cgi?id=971249 # https://bugzilla.mozilla.org/show_bug.cgi?id=971249
manifest.set("em:optionsURL", "data:text/xml,<placeholder/>") manifest.set("em:optionsURL", "data:text/xml,<placeholder/>")

View File

@ -0,0 +1,23 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
const { Cu } = require('chrome');
const { id } = require('sdk/self');
const { AddonManager } = Cu.import('resource://gre/modules/AddonManager.jsm', {});
exports.testDevelopers = function(assert, done) {
AddonManager.getAddonByID(id, (addon) => {
let count = 0;
addon.developers.forEach(({ name }) => {
count++;
assert.equal(name, count == 1 ? 'A' : 'B', 'The developers keys are correct');
});
assert.equal(count, 2, 'The key count is correct');
assert.equal(addon.developers.length, 2, 'The key length is correct');
done();
});
}
require('sdk/test/runner').runTestsFromModule(module);

View File

@ -0,0 +1,6 @@
{
"id": "test-developers@jetpack",
"title": "Test developers package key",
"author": "Erik Vold",
"developers": [ "A", "B" ]
}

View File

@ -93,39 +93,29 @@ exports.testIsPrivateOnWindowOpenFromPrivate = function(assert, done) {
then(done, assert.fail); then(done, assert.fail);
}; };
exports.testOpenTabWithPrivateWindow = function(assert, done) { exports.testOpenTabWithPrivateWindow = function*(assert) {
function start() { let { promise, resolve } = defer();
openPromise(null, {
features: {
private: true,
toolbar: true
}
}).then(focus).then(function(window) {
let { promise, resolve } = defer();
assert.equal(isPrivate(window), true, 'the focused window is private');
tabs.open({ let window = yield openPromise(null, {
url: 'about:blank', features: {
onOpen: function(tab) { private: true,
assert.equal(isPrivate(tab), false, 'the opened tab is not private'); toolbar: true
// not closing this tab on purpose.. for now...
// we keep this tab open because we closed all windows
// and must keep a non-private window open at end of this test for next ones.
resolve(window);
}
});
return promise;
}).then(close).then(done, assert.fail);
}
(function closeWindows() {
if (windows.length > 0) {
return windows.activeWindow.close(closeWindows);
} }
assert.pass('all pre test windows have been closed'); });
return start(); yield focus(window);
})()
assert.equal(isPrivate(window), true, 'the focused window is private');
tabs.open({
url: 'about:blank',
onOpen: (tab) => {
assert.equal(isPrivate(tab), false, 'the opened tab is not private');
tab.close(resolve);
}
});
yield promise;
yield close(window);
}; };
exports.testIsPrivateOnWindowOff = function(assert, done) { exports.testIsPrivateOnWindowOff = function(assert, done) {

View File

@ -1,7 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict'; 'use strict';
module.metadata = { module.metadata = {
@ -25,8 +24,7 @@ const FRAME_URL = "data:text/html;charset=utf-8," + encodeURIComponent(FRAME_HTM
const { defer } = require("sdk/core/promise"); const { defer } = require("sdk/core/promise");
const tabs = require("sdk/tabs"); const tabs = require("sdk/tabs");
const { setTabURL } = require("sdk/tabs/utils"); const { getActiveTab, getTabContentWindow, closeTab, setTabURL } = require("sdk/tabs/utils")
const { getActiveTab, getTabContentWindow, closeTab } = require("sdk/tabs/utils")
const { getMostRecentBrowserWindow } = require("sdk/window/utils"); const { getMostRecentBrowserWindow } = require("sdk/window/utils");
const { open: openNewWindow, close: closeWindow } = require("sdk/window/helpers"); const { open: openNewWindow, close: closeWindow } = require("sdk/window/helpers");
const { Loader } = require("sdk/test/loader"); const { Loader } = require("sdk/test/loader");
@ -293,403 +291,390 @@ function createEmptySelections(window) {
// Test cases // Test cases
exports["test No Selection"] = function(assert, done) { exports["test No Selection"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
let window = yield open(URL);
open(URL).then(function() { assert.equal(selection.isContiguous, false,
"selection.isContiguous without selection works.");
assert.equal(selection.isContiguous, false, assert.strictEqual(selection.text, null,
"selection.isContiguous without selection works."); "selection.text without selection works.");
assert.strictEqual(selection.text, null, assert.strictEqual(selection.html, null,
"selection.text without selection works."); "selection.html without selection works.");
assert.strictEqual(selection.html, null, let selectionCount = 0;
"selection.html without selection works."); for (let sel of selection)
selectionCount++;
let selectionCount = 0; assert.equal(selectionCount, 0, "No iterable selections");
for each (let sel in selection)
selectionCount++;
assert.equal(selectionCount, 0, yield close(window);
"No iterable selections"); loader.unload();
}).then(close).then(loader.unload).then(done, assert.fail);
}; };
exports["test Single DOM Selection"] = function(assert, done) { exports["test Single DOM Selection"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
let window = yield open(URL);
open(URL).then(selectFirstDiv).then(function() { selectFirstDiv(window)
assert.equal(selection.isContiguous, true, assert.equal(selection.isContiguous, true,
"selection.isContiguous with single DOM Selection works."); "selection.isContiguous with single DOM Selection works.");
assert.equal(selection.text, "foo", assert.equal(selection.text, "foo",
"selection.text with single DOM Selection works."); "selection.text with single DOM Selection works.");
assert.equal(selection.html, "<div>foo</div>", assert.equal(selection.html, "<div>foo</div>",
"selection.html with single DOM Selection works."); "selection.html with single DOM Selection works.");
let selectionCount = 0; let selectionCount = 0;
for each (let sel in selection) { for (let sel of selection) {
selectionCount++; selectionCount++;
assert.equal(sel.text, "foo", assert.equal(sel.text, "foo",
"iterable selection.text with single DOM Selection works."); "iterable selection.text with single DOM Selection works.");
assert.equal(sel.html, "<div>foo</div>", assert.equal(sel.html, "<div>foo</div>",
"iterable selection.html with single DOM Selection works."); "iterable selection.html with single DOM Selection works.");
} }
assert.equal(selectionCount, 1, assert.equal(selectionCount, 1, "One iterable selection");
"One iterable selection");
}).then(close).then(loader.unload).then(done, assert.fail); yield close(window);
loader.unload();
}; };
exports["test Multiple DOM Selection"] = function(assert, done) { exports["test Multiple DOM Selection"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
let expectedText = ["foo", "and"];
let expectedHTML = ["<div>foo</div>", "<div>and</div>"];
let window = yield open(URL);
open(URL).then(selectAllDivs).then(function() { selectAllDivs(window);
let expectedText = ["foo", "and"];
let expectedHTML = ["<div>foo</div>", "<div>and</div>"];
assert.equal(selection.isContiguous, false, assert.equal(selection.isContiguous, false,
"selection.isContiguous with multiple DOM Selection works."); "selection.isContiguous with multiple DOM Selection works.");
assert.equal(selection.text, expectedText[0], assert.equal(selection.text, expectedText[0],
"selection.text with multiple DOM Selection works."); "selection.text with multiple DOM Selection works.");
assert.equal(selection.html, expectedHTML[0], assert.equal(selection.html, expectedHTML[0],
"selection.html with multiple DOM Selection works."); "selection.html with multiple DOM Selection works.");
let selectionCount = 0; let selectionCount = 0;
for each (let sel in selection) { for (let sel of selection) {
assert.equal(sel.text, expectedText[selectionCount], assert.equal(sel.text, expectedText[selectionCount],
"iterable selection.text with multiple DOM Selection works."); "iterable selection.text with multiple DOM Selection works.");
assert.equal(sel.html, expectedHTML[selectionCount], assert.equal(sel.html, expectedHTML[selectionCount],
"iterable selection.text with multiple DOM Selection works."); "iterable selection.text with multiple DOM Selection works.");
selectionCount++; selectionCount++;
} }
assert.equal(selectionCount, 2, assert.equal(selectionCount, 2, "Two iterable selections");
"Two iterable selections");
}).then(close).then(loader.unload).then(done, assert.fail); yield close(window);
loader.unload();
}; };
exports["test Textarea Selection"] = function(assert, done) { exports["test Textarea Selection"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
let window = yield open(URL);
open(URL).then(selectTextarea).then(function() { selectTextarea(window);
assert.equal(selection.isContiguous, true, assert.equal(selection.isContiguous, true,
"selection.isContiguous with Textarea Selection works."); "selection.isContiguous with Textarea Selection works.");
assert.equal(selection.text, "noodles", assert.equal(selection.text, "noodles",
"selection.text with Textarea Selection works."); "selection.text with Textarea Selection works.");
assert.strictEqual(selection.html, null, assert.strictEqual(selection.html, null,
"selection.html with Textarea Selection works."); "selection.html with Textarea Selection works.");
let selectionCount = 0; let selectionCount = 0;
for each (let sel in selection) { for (let sel of selection) {
selectionCount++; selectionCount++;
assert.equal(sel.text, "noodles", assert.equal(sel.text, "noodles",
"iterable selection.text with Textarea Selection works."); "iterable selection.text with Textarea Selection works.");
assert.strictEqual(sel.html, null, assert.strictEqual(sel.html, null,
"iterable selection.html with Textarea Selection works."); "iterable selection.html with Textarea Selection works.");
} }
assert.equal(selectionCount, 1, assert.equal(selectionCount, 1, "One iterable selection");
"One iterable selection");
}).then(close).then(loader.unload).then(done, assert.fail); yield close(window);
loader.unload();
}; };
exports["test Set Text in Multiple DOM Selection"] = function(assert, done) { exports["test Set Text in Multiple DOM Selection"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
let expectedText = ["bar", "and"];
let expectedHTML = ["bar", "<div>and</div>"];
let window = yield open(URL);
open(URL).then(selectAllDivs).then(function() { selectAllDivs(window);
let expectedText = ["bar", "and"];
let expectedHTML = ["bar", "<div>and</div>"];
selection.text = "bar"; selection.text = "bar";
assert.equal(selection.text, expectedText[0], assert.equal(selection.text, expectedText[0],
"set selection.text with single DOM Selection works."); "set selection.text with single DOM Selection works.");
assert.equal(selection.html, expectedHTML[0], assert.equal(selection.html, expectedHTML[0],
"selection.html with single DOM Selection works."); "selection.html with single DOM Selection works.");
let selectionCount = 0; let selectionCount = 0;
for each (let sel in selection) { for (let sel of selection) {
assert.equal(sel.text, expectedText[selectionCount],
"iterable selection.text with multiple DOM Selection works.");
assert.equal(sel.text, expectedText[selectionCount], assert.equal(sel.html, expectedHTML[selectionCount],
"iterable selection.text with multiple DOM Selection works."); "iterable selection.html with multiple DOM Selection works.");
assert.equal(sel.html, expectedHTML[selectionCount], selectionCount++;
"iterable selection.html with multiple DOM Selection works."); }
selectionCount++; assert.equal(selectionCount, 2, "Two iterable selections");
}
assert.equal(selectionCount, 2, yield close(window);
"Two iterable selections"); loader.unload();
}).then(close).then(loader.unload).then(done, assert.fail);
}; };
exports["test Set HTML in Multiple DOM Selection"] = function(assert, done) { exports["test Set HTML in Multiple DOM Selection"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
let html = "<span>b<b>a</b>r</span>";
let expectedText = ["bar", "and"];
let expectedHTML = [html, "<div>and</div>"];
let window = yield open(URL);
open(URL).then(selectAllDivs).then(function() { selectAllDivs(window);
let html = "<span>b<b>a</b>r</span>";
let expectedText = ["bar", "and"]; selection.html = html;
let expectedHTML = [html, "<div>and</div>"];
selection.html = html; assert.equal(selection.text, expectedText[0],
"set selection.text with DOM Selection works.");
assert.equal(selection.text, expectedText[0], assert.equal(selection.html, expectedHTML[0],
"set selection.text with DOM Selection works."); "selection.html with DOM Selection works.");
assert.equal(selection.html, expectedHTML[0], let selectionCount = 0;
"selection.html with DOM Selection works."); for (let sel of selection) {
assert.equal(sel.text, expectedText[selectionCount],
"iterable selection.text with multiple DOM Selection works.");
let selectionCount = 0; assert.equal(sel.html, expectedHTML[selectionCount],
for each (let sel in selection) { "iterable selection.html with multiple DOM Selection works.");
assert.equal(sel.text, expectedText[selectionCount], selectionCount++;
"iterable selection.text with multiple DOM Selection works."); }
assert.equal(sel.html, expectedHTML[selectionCount], assert.equal(selectionCount, 2, "Two iterable selections");
"iterable selection.html with multiple DOM Selection works.");
selectionCount++; yield close(window);
} loader.unload();
assert.equal(selectionCount, 2,
"Two iterable selections");
}).then(close).then(loader.unload).then(done, assert.fail);
}; };
exports["test Set HTML as text in Multiple DOM Selection"] = function(assert, done) { exports["test Set HTML as text in Multiple DOM Selection"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
let text = "<span>b<b>a</b>r</span>";
let html = "&lt;span&gt;b&lt;b&gt;a&lt;/b&gt;r&lt;/span&gt;";
let expectedText = [text, "and"];
let expectedHTML = [html, "<div>and</div>"];
let window = yield open(URL);
open(URL).then(selectAllDivs).then(function() { selectAllDivs(window);
let text = "<span>b<b>a</b>r</span>";
let html = "&lt;span&gt;b&lt;b&gt;a&lt;/b&gt;r&lt;/span&gt;";
let expectedText = [text, "and"]; selection.text = text;
let expectedHTML = [html, "<div>and</div>"];
selection.text = text; assert.equal(selection.text, expectedText[0],
"set selection.text with DOM Selection works.");
assert.equal(selection.text, expectedText[0], assert.equal(selection.html, expectedHTML[0],
"set selection.text with DOM Selection works."); "selection.html with DOM Selection works.");
assert.equal(selection.html, expectedHTML[0], let selectionCount = 0;
"selection.html with DOM Selection works."); for (let sel of selection) {
assert.equal(sel.text, expectedText[selectionCount],
"iterable selection.text with multiple DOM Selection works.");
let selectionCount = 0; assert.equal(sel.html, expectedHTML[selectionCount],
for each (let sel in selection) { "iterable selection.html with multiple DOM Selection works.");
assert.equal(sel.text, expectedText[selectionCount], selectionCount++;
"iterable selection.text with multiple DOM Selection works."); }
assert.equal(sel.html, expectedHTML[selectionCount], assert.equal(selectionCount, 2, "Two iterable selections");
"iterable selection.html with multiple DOM Selection works.");
selectionCount++; yield close(window);
} loader.unload();
assert.equal(selectionCount, 2,
"Two iterable selections");
}).then(close).then(loader.unload).then(done, assert.fail);
}; };
exports["test Set Text in Textarea Selection"] = function(assert, done) { exports["test Set Text in Textarea Selection"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
let text = "bar";
let window = yield open(URL);
open(URL).then(selectTextarea).then(function() { selectTextarea(window);
let text = "bar"; selection.text = text;
selection.text = text; assert.equal(selection.text, text,
"set selection.text with Textarea Selection works.");
assert.equal(selection.text, text, assert.strictEqual(selection.html, null,
"set selection.text with Textarea Selection works."); "selection.html with Textarea Selection works.");
assert.strictEqual(selection.html, null, let selectionCount = 0;
"selection.html with Textarea Selection works."); for (let sel of selection) {
selectionCount++;
let selectionCount = 0; assert.equal(sel.text, text,
for each (let sel in selection) { "iterable selection.text with Textarea Selection works.");
selectionCount++;
assert.equal(sel.text, text, assert.strictEqual(sel.html, null,
"iterable selection.text with Textarea Selection works."); "iterable selection.html with Textarea Selection works.");
}
assert.strictEqual(sel.html, null, assert.equal(selectionCount, 1, "One iterable selection");
"iterable selection.html with Textarea Selection works.");
}
assert.equal(selectionCount, 1, yield close(window);
"One iterable selection"); loader.unload();
}).then(close).then(loader.unload).then(done, assert.fail);
}; };
exports["test Set HTML in Textarea Selection"] = function(assert, done) { exports["test Set HTML in Textarea Selection"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
let html = "<span>b<b>a</b>r</span>";
let window = yield open(URL);
open(URL).then(selectTextarea).then(function() { selectTextarea(window);
let html = "<span>b<b>a</b>r</span>"; // Textarea can't have HTML so set `html` property is equals to set `text`
// property
selection.html = html;
// Textarea can't have HTML so set `html` property is equals to set `text` assert.equal(selection.text, html,
// property "set selection.text with Textarea Selection works.");
selection.html = html;
assert.equal(selection.text, html, assert.strictEqual(selection.html, null,
"set selection.text with Textarea Selection works."); "selection.html with Textarea Selection works.");
assert.strictEqual(selection.html, null, let selectionCount = 0;
"selection.html with Textarea Selection works."); for (let sel of selection) {
selectionCount++;
let selectionCount = 0; assert.equal(sel.text, html,
for each (let sel in selection) { "iterable selection.text with Textarea Selection works.");
selectionCount++;
assert.equal(sel.text, html, assert.strictEqual(sel.html, null,
"iterable selection.text with Textarea Selection works."); "iterable selection.html with Textarea Selection works.");
}
assert.strictEqual(sel.html, null, assert.equal(selectionCount, 1, "One iterable selection");
"iterable selection.html with Textarea Selection works.");
}
assert.equal(selectionCount, 1, yield close(window);
"One iterable selection"); loader.unload();
}).then(close).then(loader.unload).then(done, assert.fail);
}; };
exports["test Empty Selections"] = function(assert, done) { exports["test Empty Selections"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
let window = yield open(URL);
open(URL).then(createEmptySelections).then(function(){ createEmptySelections(window);
assert.equal(selection.isContiguous, false,
"selection.isContiguous with empty selections works.");
assert.strictEqual(selection.text, null, assert.equal(selection.isContiguous, false,
"selection.text with empty selections works."); "selection.isContiguous with empty selections works.");
assert.strictEqual(selection.html, null, assert.strictEqual(selection.text, null,
"selection.html with empty selections works."); "selection.text with empty selections works.");
let selectionCount = 0; assert.strictEqual(selection.html, null,
for each (let sel in selection) "selection.html with empty selections works.");
selectionCount++;
assert.equal(selectionCount, 0, let selectionCount = 0;
"No iterable selections"); for (let sel of selection)
selectionCount++;
}).then(close).then(loader.unload).then(done, assert.fail); assert.equal(selectionCount, 0, "No iterable selections");
yield close(window);
loader.unload();
} }
exports["test No Selection Exception"] = function(assert, done) { exports["test No Selection Exception"] = function*(assert) {
const NO_SELECTION = /It isn't possible to change the selection/; const NO_SELECTION = /It isn't possible to change the selection/;
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
let window = yield open(URL);
open(URL).then(function() { // We're trying to change a selection when there is no selection
assert.throws(function() {
selection.text = "bar";
}, NO_SELECTION);
// We're trying to change a selection when there is no selection assert.throws(function() {
assert.throws(function() { selection.html = "bar";
selection.text = "bar"; }, NO_SELECTION);
}, NO_SELECTION);
assert.throws(function() { yield close(window);
selection.html = "bar"; loader.unload();
}, NO_SELECTION);
}).then(close).then(loader.unload).then(done, assert.fail);
}; };
exports["test for...of without selections"] = function(assert, done) { exports["test for...of without selections"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
let window = yield open(URL);
let selectionCount = 0;
open(URL).then(function() { for (let sel of selection)
let selectionCount = 0; selectionCount++;
for (let sel of selection) assert.equal(selectionCount, 0, "No iterable selections");
selectionCount++;
assert.equal(selectionCount, 0, yield close(window);
"No iterable selections"); loader.unload();
}).then(close).then(loader.unload).then(null, function(error) {
// iterable are not supported yet in Firefox 16, for example, but
// they are in Firefox 17.
if (error.message.indexOf("is not iterable") > -1)
assert.pass("`iterable` method not supported in this application");
else
assert.fail(error);
}).then(done, assert.fail);
} }
exports["test for...of with selections"] = function(assert, done) { exports["test for...of with selections"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
let expectedText = ["foo", "and"];
let expectedHTML = ["<div>foo</div>", "<div>and</div>"];
let window = yield open(URL);
open(URL).then(selectAllDivs).then(function(){ selectAllDivs(window);
let expectedText = ["foo", "and"];
let expectedHTML = ["<div>foo</div>", "<div>and</div>"];
let selectionCount = 0; let selectionCount = 0;
for (let sel of selection) { for (let sel of selection) {
assert.equal(sel.text, expectedText[selectionCount], assert.equal(sel.text, expectedText[selectionCount],
"iterable selection.text with for...of works."); "iterable selection.text with for...of works.");
assert.equal(sel.html, expectedHTML[selectionCount], assert.equal(sel.html, expectedHTML[selectionCount],
"iterable selection.text with for...of works."); "iterable selection.text with for...of works.");
selectionCount++; selectionCount++;
} }
assert.equal(selectionCount, 2, assert.equal(selectionCount, 2, "Two iterable selections");
"Two iterable selections");
}).then(close).then(loader.unload).then(null, function(error) { yield close(window);
// iterable are not supported yet in Firefox 16, for example, but loader.unload();
// they are in Firefox 17.
if (error.message.indexOf("is not iterable") > -1)
assert.pass("`iterable` method not supported in this application");
else
assert.fail(error);
}).then(done, assert.fail)
} }
exports["test Selection Listener"] = function(assert, done) { exports["test Selection Listener"] = function(assert, done) {
@ -722,7 +707,7 @@ exports["test Textarea OnSelect Listener"] = function(assert, done) {
then(dispatchOnSelectEvent, assert.fail); then(dispatchOnSelectEvent, assert.fail);
}; };
exports["test Selection listener removed on unload"] = function(assert, done) { exports["test Selection listener removed on unload"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
@ -731,17 +716,16 @@ exports["test Selection listener removed on unload"] = function(assert, done) {
}); });
loader.unload(); loader.unload();
assert.pass("unload was a success");
assert.pass(); yield open(URL).
open(URL).
then(selectContentFirstDiv). then(selectContentFirstDiv).
then(dispatchSelectionEvent). then(dispatchSelectionEvent).
then(close). then(close).
then(done, assert.fail); catch(assert.fail);
}; };
exports["test Textarea onSelect Listener removed on unload"] = function(assert, done) { exports["test Textarea onSelect Listener removed on unload"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
@ -750,14 +734,13 @@ exports["test Textarea onSelect Listener removed on unload"] = function(assert,
}); });
loader.unload(); loader.unload();
assert.pass("unload was a success");
assert.pass(); yield open(URL).
open(URL).
then(selectTextarea). then(selectTextarea).
then(dispatchOnSelectEvent). then(dispatchOnSelectEvent).
then(close). then(close).
then(done, assert.fail); catch(assert.fail);
}; };
@ -848,7 +831,7 @@ exports["test Selection Listener on frame"] = function(assert, done) {
then(getFrameWindow). then(getFrameWindow).
then(selectContentFirstDiv). then(selectContentFirstDiv).
then(dispatchSelectionEvent). then(dispatchSelectionEvent).
then(null, assert.fail); catch(assert.fail);
}; };
exports["test Textarea onSelect Listener on frame"] = function(assert, done) { exports["test Textarea onSelect Listener on frame"] = function(assert, done) {
@ -867,11 +850,11 @@ exports["test Textarea onSelect Listener on frame"] = function(assert, done) {
then(getFrameWindow). then(getFrameWindow).
then(selectTextarea). then(selectTextarea).
then(dispatchOnSelectEvent). then(dispatchOnSelectEvent).
then(null, assert.fail); catch(assert.fail);
}; };
exports["test PBPW Selection Listener"] = function(assert, done) { exports["test PBPW Selection Listener"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
@ -881,15 +864,16 @@ exports["test PBPW Selection Listener"] = function(assert, done) {
assert.pass(); assert.pass();
open(URL, {private: true}). yield open(URL, { private: true }).
then(selectContentFirstDiv). then(selectContentFirstDiv).
then(dispatchSelectionEvent). then(dispatchSelectionEvent).
then(closeWindow). then(closeWindow).
then(loader.unload). catch(assert.fail);
then(done, assert.fail);
loader.unload();
}; };
exports["test PBPW Textarea OnSelect Listener"] = function(assert, done) { exports["test PBPW Textarea OnSelect Listener"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
@ -899,72 +883,73 @@ exports["test PBPW Textarea OnSelect Listener"] = function(assert, done) {
assert.pass(); assert.pass();
open(URL, {private: true}). yield open(URL, { private: true }).
then(selectTextarea). then(selectTextarea).
then(dispatchOnSelectEvent). then(dispatchOnSelectEvent).
then(closeWindow). then(closeWindow).
then(loader.unload). catch(assert.fail);
then(done, assert.fail);
loader.unload();
}; };
exports["test PBPW Single DOM Selection"] = function(assert, done) { exports["test PBPW Single DOM Selection"] = function*(assert) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
let window = yield open(URL, { private: true });
open(URL, {private: true}).then(selectFirstDiv).then(function(window) { selectFirstDiv(window);
assert.equal(selection.isContiguous, false, assert.equal(selection.isContiguous, false,
"selection.isContiguous with single DOM Selection in PBPW works."); "selection.isContiguous with single DOM Selection in PBPW works.");
assert.equal(selection.text, null, assert.equal(selection.text, null,
"selection.text with single DOM Selection in PBPW works."); "selection.text with single DOM Selection in PBPW works.");
assert.equal(selection.html, null, assert.equal(selection.html, null,
"selection.html with single DOM Selection in PBPW works."); "selection.html with single DOM Selection in PBPW works.");
let selectionCount = 0; let selectionCount = 0;
for each (let sel in selection) for (let sel of selection)
selectionCount++; selectionCount++;
assert.equal(selectionCount, 0, assert.equal(selectionCount, 0, "No iterable selection in PBPW");
"No iterable selection in PBPW");
return window; yield closeWindow(window);
}).then(closeWindow).then(loader.unload).then(done, assert.fail); loader.unload();
}; };
exports["test PBPW Textarea Selection"] = function(assert, done) { exports["test PBPW Textarea Selection"] = function(assert, done) {
let loader = Loader(module); let loader = Loader(module);
let selection = loader.require("sdk/selection"); let selection = loader.require("sdk/selection");
let window = yield open(URL, { private: true });
open(URL, {private: true}).then(selectTextarea).then(function(window) { selectTextarea(window);
assert.equal(selection.isContiguous, false, assert.equal(selection.isContiguous, false,
"selection.isContiguous with Textarea Selection in PBPW works."); "selection.isContiguous with Textarea Selection in PBPW works.");
assert.equal(selection.text, null, assert.equal(selection.text, null,
"selection.text with Textarea Selection in PBPW works."); "selection.text with Textarea Selection in PBPW works.");
assert.strictEqual(selection.html, null, assert.strictEqual(selection.html, null,
"selection.html with Textarea Selection in PBPW works."); "selection.html with Textarea Selection in PBPW works.");
let selectionCount = 0; let selectionCount = 0;
for each (let sel in selection) { for (let sel of selection) {
selectionCount++; selectionCount++;
assert.equal(sel.text, null, assert.equal(sel.text, null,
"iterable selection.text with Textarea Selection in PBPW works."); "iterable selection.text with Textarea Selection in PBPW works.");
assert.strictEqual(sel.html, null, assert.strictEqual(sel.html, null,
"iterable selection.html with Textarea Selection in PBPW works."); "iterable selection.html with Textarea Selection in PBPW works.");
} }
assert.equal(selectionCount, 0, assert.equal(selectionCount, 0, "No iterable selection in PBPW");
"No iterable selection in PBPW");
return window; yield closeWindow(window);
}).then(closeWindow).then(loader.unload).then(done, assert.fail); loader.unload();
}; };
// TODO: test Selection Listener on long-held connection (Bug 661884) // TODO: test Selection Listener on long-held connection (Bug 661884)
@ -985,7 +970,7 @@ exports["test Selection Listener on long-held connection"] = function(assert, do
// If the platform doesn't support the PBPW, we're replacing PBPW tests // If the platform doesn't support the PBPW, we're replacing PBPW tests
if (!require("sdk/private-browsing/utils").isWindowPBSupported) { if (!require("sdk/private-browsing/utils").isWindowPBSupported) {
Object.keys(module.exports).forEach(function(key) { Object.keys(module.exports).forEach((key) => {
if (key.indexOf("test PBPW") === 0) { if (key.indexOf("test PBPW") === 0) {
module.exports[key] = function Unsupported (assert) { module.exports[key] = function Unsupported (assert) {
assert.pass("Private Window Per Browsing is not supported on this platform."); assert.pass("Private Window Per Browsing is not supported on this platform.");
@ -994,4 +979,4 @@ if (!require("sdk/private-browsing/utils").isWindowPBSupported) {
}); });
} }
require("test").run(exports) require("sdk/test").run(exports);

View File

@ -211,7 +211,7 @@ exports.testActiveWindow = function(assert, done) {
let rawWindow2, rawWindow3; let rawWindow2, rawWindow3;
let testSteps = [ let testSteps = [
function() { () => {
assert.equal(windows.length, 3, "Correct number of browser windows"); assert.equal(windows.length, 3, "Correct number of browser windows");
let count = 0; let count = 0;
@ -223,20 +223,17 @@ exports.testActiveWindow = function(assert, done) {
continueAfterFocus(rawWindow2); continueAfterFocus(rawWindow2);
rawWindow2.focus(); rawWindow2.focus();
}, }, () => {
function() {
assert.equal(windows.activeWindow.title, window2.title, "Correct active window - 2"); assert.equal(windows.activeWindow.title, window2.title, "Correct active window - 2");
continueAfterFocus(rawWindow2); continueAfterFocus(rawWindow2);
window2.activate(); window2.activate();
}, }, () => {
function() {
assert.equal(windows.activeWindow.title, window2.title, "Correct active window - 2"); assert.equal(windows.activeWindow.title, window2.title, "Correct active window - 2");
continueAfterFocus(rawWindow3); continueAfterFocus(rawWindow3);
window3.activate(); window3.activate();
}, }, () => {
function() {
assert.equal(windows.activeWindow.title, window3.title, "Correct active window - 3"); assert.equal(windows.activeWindow.title, window3.title, "Correct active window - 3");
finishTest(); finishTest();
} }
@ -251,10 +248,10 @@ exports.testActiveWindow = function(assert, done) {
windows.open({ windows.open({
url: "data:text/html;charset=utf-8,<title>window 2</title>", url: "data:text/html;charset=utf-8,<title>window 2</title>",
onOpen: function(window) { onOpen: (window) => {
assert.pass('window 2 open'); assert.pass('window 2 open');
window.tabs.activeTab.on('ready', function() { window.tabs.activeTab.once('ready', () => {
assert.pass('window 2 tab activated'); assert.pass('window 2 tab activated');
window2 = window; window2 = window;
@ -267,10 +264,10 @@ exports.testActiveWindow = function(assert, done) {
windows.open({ windows.open({
url: "data:text/html;charset=utf-8,<title>window 3</title>", url: "data:text/html;charset=utf-8,<title>window 3</title>",
onOpen: function(window) { onOpen: (window) => {
assert.pass('window 3 open'); assert.pass('window 3 open');
window.tabs.activeTab.on('ready', function onReady() { window.tabs.activeTab.once('ready', () => {
assert.pass('window 3 tab activated'); assert.pass('window 3 tab activated');
window3 = window; window3 = window;
@ -282,7 +279,6 @@ exports.testActiveWindow = function(assert, done) {
assert.equal(rawWindow3.document.title, window3.title, "Saw correct title on window 3"); assert.equal(rawWindow3.document.title, window3.title, "Saw correct title on window 3");
continueAfterFocus(rawWindow3); continueAfterFocus(rawWindow3);
rawWindow3.focus();
}); });
} }
}); });
@ -290,24 +286,16 @@ exports.testActiveWindow = function(assert, done) {
} }
}); });
function nextStep() { let nextStep = () => testSteps.length ? setTimeout(testSteps.shift()) : null;
if (testSteps.length) { let continueAfterFocus = (w) => onFocus(w).then(nextStep);
setTimeout(testSteps.shift())
}
}
let continueAfterFocus = function(w) onFocus(w).then(nextStep);
function finishTest() { function finishTest() {
// close unactive window first to avoid unnecessary focus changing // close unactive window first to avoid unnecessary focus changing
window2.close(function() { window2.close(() => window3.close(() => {
window3.close(function() { assert.equal(rawWindow2.closed, true, 'window 2 is closed');
assert.equal(rawWindow2.closed, true, 'window 2 is closed'); assert.equal(rawWindow3.closed, true, 'window 3 is closed');
assert.equal(rawWindow3.closed, true, 'window 3 is closed'); done();
}));
done();
});
});
} }
}; };

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68"> <project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68">
<copyfile dest="Makefile" src="core/root.mk"/> <copyfile dest="Makefile" src="core/root.mk"/>
</project> </project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="4abb193ddae0f9780ad12ffe5e31772feee3926a"/> <project name="gaia" path="gaia" remote="mozillaorg" revision="8e02f689b0fc39cb6ccdc22d02ed7e219c58faa7"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/> <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/> <copyfile dest="Makefile" src="core/root.mk"/>
</project> </project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="4abb193ddae0f9780ad12ffe5e31772feee3926a"/> <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e02f689b0fc39cb6ccdc22d02ed7e219c58faa7"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/> <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>

View File

@ -17,7 +17,7 @@
</project> </project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="4abb193ddae0f9780ad12ffe5e31772feee3926a"/> <project name="gaia" path="gaia" remote="mozillaorg" revision="8e02f689b0fc39cb6ccdc22d02ed7e219c58faa7"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/> <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/>
@ -130,8 +130,8 @@
<project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="3a9a17613cc685aa232432566ad6cc607eab4ec1"/> <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="3a9a17613cc685aa232432566ad6cc607eab4ec1"/>
<project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="197cd9492b9fadaa915c5daf36ff557f8f4a8d1c"/> <project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="197cd9492b9fadaa915c5daf36ff557f8f4a8d1c"/>
<project name="platform/external/libnfc-nci" path="external/libnfc-nci" revision="7d33aaf740bbf6c7c6e9c34a92b371eda311b66b"/> <project name="platform/external/libnfc-nci" path="external/libnfc-nci" revision="7d33aaf740bbf6c7c6e9c34a92b371eda311b66b"/>
<project name="libnfcemu" path="external/libnfcemu" remote="b2g" revision="682e556367e0049bb3ae127cec6e6c459abca1b0"/> <project name="libnfcemu" path="external/libnfcemu" remote="b2g" revision="c7ccf6eff27f99e39a9eca94cde48aaece5e47db"/>
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="57b16fcb790bdf0b53b3c6435a37ccc8ca90ed36"/> <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="d259117b4976decbe2f76eeed85218bf0109190f"/>
<project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="0e56e450367cd802241b27164a2979188242b95f"/> <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="0e56e450367cd802241b27164a2979188242b95f"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="9f28c4faea3b2f01db227b2467b08aeba96d9bec"/> <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="9f28c4faea3b2f01db227b2467b08aeba96d9bec"/>
<project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="7e5ae3ef2fe1cbc1c9a4fc718b01b7f28b1c5486"/> <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="7e5ae3ef2fe1cbc1c9a4fc718b01b7f28b1c5486"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68"> <project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68">
<copyfile dest="Makefile" src="core/root.mk"/> <copyfile dest="Makefile" src="core/root.mk"/>
</project> </project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="4abb193ddae0f9780ad12ffe5e31772feee3926a"/> <project name="gaia" path="gaia" remote="mozillaorg" revision="8e02f689b0fc39cb6ccdc22d02ed7e219c58faa7"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/> <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/> <copyfile dest="Makefile" src="core/root.mk"/>
</project> </project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="4abb193ddae0f9780ad12ffe5e31772feee3926a"/> <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e02f689b0fc39cb6ccdc22d02ed7e219c58faa7"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/> <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68"> <project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68">
<copyfile dest="Makefile" src="core/root.mk"/> <copyfile dest="Makefile" src="core/root.mk"/>
</project> </project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="4abb193ddae0f9780ad12ffe5e31772feee3926a"/> <project name="gaia" path="gaia" remote="mozillaorg" revision="8e02f689b0fc39cb6ccdc22d02ed7e219c58faa7"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/> <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -17,7 +17,7 @@
</project> </project>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/> <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="4abb193ddae0f9780ad12ffe5e31772feee3926a"/> <project name="gaia" path="gaia" remote="mozillaorg" revision="8e02f689b0fc39cb6ccdc22d02ed7e219c58faa7"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/> <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/>

View File

@ -4,6 +4,6 @@
"remote": "", "remote": "",
"branch": "" "branch": ""
}, },
"revision": "d43b2e2dba496f624d62c1b72a22dc5e34953fbd", "revision": "7c8bbd0eb4543c729b029339cb79bfaa4ec23069",
"repo_path": "/integration/gaia-central" "repo_path": "/integration/gaia-central"
} }

View File

@ -17,7 +17,7 @@
<copyfile dest="Makefile" src="core/root.mk"/> <copyfile dest="Makefile" src="core/root.mk"/>
</project> </project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="4abb193ddae0f9780ad12ffe5e31772feee3926a"/> <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e02f689b0fc39cb6ccdc22d02ed7e219c58faa7"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/> <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -15,7 +15,7 @@
<copyfile dest="Makefile" src="core/root.mk"/> <copyfile dest="Makefile" src="core/root.mk"/>
</project> </project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="4abb193ddae0f9780ad12ffe5e31772feee3926a"/> <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e02f689b0fc39cb6ccdc22d02ed7e219c58faa7"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/> <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -17,7 +17,7 @@
</project> </project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="4abb193ddae0f9780ad12ffe5e31772feee3926a"/> <project name="gaia" path="gaia" remote="mozillaorg" revision="8e02f689b0fc39cb6ccdc22d02ed7e219c58faa7"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/> <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/>

View File

@ -17,7 +17,7 @@
<copyfile dest="Makefile" src="core/root.mk"/> <copyfile dest="Makefile" src="core/root.mk"/>
</project> </project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="4abb193ddae0f9780ad12ffe5e31772feee3926a"/> <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e02f689b0fc39cb6ccdc22d02ed7e219c58faa7"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/> <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -298,16 +298,28 @@ function ensureSnippetsMapThen(aCallback)
function onSearchSubmit(aEvent) function onSearchSubmit(aEvent)
{ {
let searchTerms = document.getElementById("searchText").value; let searchText = document.getElementById("searchText");
let searchTerms = searchText.value;
let engineName = document.documentElement.getAttribute("searchEngineName"); let engineName = document.documentElement.getAttribute("searchEngineName");
if (engineName && searchTerms.length > 0) { if (engineName && searchTerms.length > 0) {
// Send an event that will perform a search and Firefox Health Report will // Send an event that will perform a search and Firefox Health Report will
// record that a search from about:home has occurred. // record that a search from about:home has occurred.
let eventData = JSON.stringify({
let eventData = {
engineName: engineName, engineName: engineName,
searchTerms: searchTerms searchTerms: searchTerms
}); };
if (searchText.hasAttribute("selection-index")) {
eventData.selection = {
index: searchText.getAttribute("selection-index"),
kind: searchText.getAttribute("selection-kind")
};
}
eventData = JSON.stringify(eventData);
let event = new CustomEvent("AboutHomeSearchEvent", {detail: eventData}); let event = new CustomEvent("AboutHomeSearchEvent", {detail: eventData});
document.dispatchEvent(event); document.dispatchEvent(event);
} }

View File

@ -3125,9 +3125,13 @@ const BrowserSearch = {
* @param source * @param source
* (string) Where the search originated from. See the FHR * (string) Where the search originated from. See the FHR
* SearchesProvider for allowed values. * SearchesProvider for allowed values.
* @param selection [optional]
* ({index: The selected index, kind: "key" or "mouse"}) If
* the search was a suggested search, this indicates where the
* item was in the suggestion list and how the user selected it.
*/ */
recordSearchInHealthReport: function (engine, source) { recordSearchInHealthReport: function (engine, source, selection) {
BrowserUITelemetry.countSearchEvent(source); BrowserUITelemetry.countSearchEvent(source, null, selection);
#ifdef MOZ_SERVICES_HEALTHREPORT #ifdef MOZ_SERVICES_HEALTHREPORT
let reporter = Cc["@mozilla.org/datareporting/service;1"] let reporter = Cc["@mozilla.org/datareporting/service;1"]
.getService() .getService()

View File

@ -33,13 +33,24 @@ let gSearch = {
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
} }
let searchStr = this._nodes.text.value; let searchText = this._nodes.text;
let searchStr = searchText.value;
if (this.currentEngineName && searchStr.length) { if (this.currentEngineName && searchStr.length) {
this._send("Search", {
let eventData = {
engineName: this.currentEngineName, engineName: this.currentEngineName,
searchString: searchStr, searchString: searchStr,
whence: "newtab", whence: "newtab",
}); }
if (searchText.hasAttribute("selection-index")) {
eventData.selection = {
index: searchText.getAttribute("selection-index"),
kind: searchText.getAttribute("selection-kind")
};
}
this._send("Search", eventData);
} }
this._suggestionController.addInputValueToFormHistory(); this._suggestionController.addInputValueToFormHistory();
}, },

View File

@ -179,6 +179,13 @@ SearchSuggestionUIController.prototype = {
case event.DOM_VK_RETURN: case event.DOM_VK_RETURN:
if (this.selectedIndex >= 0) { if (this.selectedIndex >= 0) {
this.input.value = this.suggestionAtIndex(this.selectedIndex); this.input.value = this.suggestionAtIndex(this.selectedIndex);
this.input.setAttribute("selection-index", this.selectedIndex);
this.input.setAttribute("selection-kind", "key");
} else {
// If we didn't select anything, make sure to remove the attributes
// in case they were populated last time.
this.input.removeAttribute("selection-index");
this.input.removeAttribute("selection-kind");
} }
this._stickyInputValue = this.input.value; this._stickyInputValue = this.input.value;
this._hideSuggestions(); this._hideSuggestions();
@ -228,6 +235,8 @@ SearchSuggestionUIController.prototype = {
let suggestion = this.suggestionAtIndex(idx); let suggestion = this.suggestionAtIndex(idx);
this._stickyInputValue = suggestion; this._stickyInputValue = suggestion;
this.input.value = suggestion; this.input.value = suggestion;
this.input.setAttribute("selection-index", idx);
this.input.setAttribute("selection-kind", "mouse");
this._hideSuggestions(); this._hideSuggestions();
if (this.onClick) { if (this.onClick) {
this.onClick.call(null); this.onClick.call(null);

View File

@ -151,7 +151,7 @@ loop.conversation = (function(OT, mozL10n) {
* *
* Required options: * Required options:
* - {loop.shared.models.ConversationModel} conversation Conversation model. * - {loop.shared.models.ConversationModel} conversation Conversation model.
* - {loop.shared.components.Notifier} notifier Notifier component. * - {loop.shared.models.NotificationCollection} notifications
* *
* @type {loop.shared.router.BaseConversationRouter} * @type {loop.shared.router.BaseConversationRouter}
*/ */
@ -206,7 +206,7 @@ loop.conversation = (function(OT, mozL10n) {
console.error("Failed to get the sessionData", err); console.error("Failed to get the sessionData", err);
// XXX Not the ideal response, but bug 1047410 will be replacing // XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI. // this by better "call failed" UI.
this._notifier.errorL10n("cannot_start_call_session_not_ready"); this._notifications.errorL10n("cannot_start_call_session_not_ready");
return; return;
} }
@ -331,7 +331,7 @@ loop.conversation = (function(OT, mozL10n) {
_handleSessionError: function() { _handleSessionError: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing // XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI. // this by better "call failed" UI.
this._notifier.errorL10n("cannot_start_call_session_not_ready"); this._notifications.errorL10n("cannot_start_call_session_not_ready");
}, },
/** /**
@ -376,7 +376,7 @@ loop.conversation = (function(OT, mozL10n) {
conversation: new loop.shared.models.ConversationModel( conversation: new loop.shared.models.ConversationModel(
{}, // Model attributes {}, // Model attributes
{sdk: OT}), // Model dependencies {sdk: OT}), // Model dependencies
notifier: new sharedViews.NotificationListView({el: "#messages"}) notifications: new loop.shared.models.NotificationCollection()
}); });
Backbone.history.start(); Backbone.history.start();
} }

View File

@ -151,7 +151,7 @@ loop.conversation = (function(OT, mozL10n) {
* *
* Required options: * Required options:
* - {loop.shared.models.ConversationModel} conversation Conversation model. * - {loop.shared.models.ConversationModel} conversation Conversation model.
* - {loop.shared.components.Notifier} notifier Notifier component. * - {loop.shared.models.NotificationCollection} notifications
* *
* @type {loop.shared.router.BaseConversationRouter} * @type {loop.shared.router.BaseConversationRouter}
*/ */
@ -206,7 +206,7 @@ loop.conversation = (function(OT, mozL10n) {
console.error("Failed to get the sessionData", err); console.error("Failed to get the sessionData", err);
// XXX Not the ideal response, but bug 1047410 will be replacing // XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI. // this by better "call failed" UI.
this._notifier.errorL10n("cannot_start_call_session_not_ready"); this._notifications.errorL10n("cannot_start_call_session_not_ready");
return; return;
} }
@ -331,7 +331,7 @@ loop.conversation = (function(OT, mozL10n) {
_handleSessionError: function() { _handleSessionError: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing // XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI. // this by better "call failed" UI.
this._notifier.errorL10n("cannot_start_call_session_not_ready"); this._notifications.errorL10n("cannot_start_call_session_not_ready");
}, },
/** /**
@ -376,7 +376,7 @@ loop.conversation = (function(OT, mozL10n) {
conversation: new loop.shared.models.ConversationModel( conversation: new loop.shared.models.ConversationModel(
{}, // Model attributes {}, // Model attributes
{sdk: OT}), // Model dependencies {sdk: OT}), // Model dependencies
notifier: new sharedViews.NotificationListView({el: "#messages"}) notifications: new loop.shared.models.NotificationCollection()
}); });
Backbone.history.start(); Backbone.history.start();
} }

View File

@ -12,6 +12,7 @@ loop.panel = (function(_, mozL10n) {
"use strict"; "use strict";
var sharedViews = loop.shared.views, var sharedViews = loop.shared.views,
sharedModels = loop.shared.models,
// aliasing translation function as __ for concision // aliasing translation function as __ for concision
__ = mozL10n.get; __ = mozL10n.get;
@ -261,7 +262,7 @@ loop.panel = (function(_, mozL10n) {
propTypes: { propTypes: {
callUrl: React.PropTypes.string, callUrl: React.PropTypes.string,
callUrlExpiry: React.PropTypes.number, callUrlExpiry: React.PropTypes.number,
notifier: React.PropTypes.object.isRequired, notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired client: React.PropTypes.object.isRequired
}, },
@ -296,10 +297,10 @@ loop.panel = (function(_, mozL10n) {
}, },
_onCallUrlReceived: function(err, callUrlData) { _onCallUrlReceived: function(err, callUrlData) {
this.props.notifier.clear(); this.props.notifications.reset();
if (err) { if (err) {
this.props.notifier.errorL10n("unable_retrieve_url"); this.props.notifications.errorL10n("unable_retrieve_url");
this.setState(this.getInitialState()); this.setState(this.getInitialState());
} else { } else {
try { try {
@ -314,7 +315,7 @@ loop.panel = (function(_, mozL10n) {
callUrlExpiry: callUrlData.expiresAt}); callUrlExpiry: callUrlData.expiresAt});
} catch(e) { } catch(e) {
console.log(e); console.log(e);
this.props.notifier.errorL10n("unable_retrieve_url"); this.props.notifications.errorL10n("unable_retrieve_url");
this.setState(this.getInitialState()); this.setState(this.getInitialState());
} }
} }
@ -411,17 +412,20 @@ loop.panel = (function(_, mozL10n) {
*/ */
var PanelView = React.createClass({displayName: 'PanelView', var PanelView = React.createClass({displayName: 'PanelView',
propTypes: { propTypes: {
notifier: React.PropTypes.object.isRequired, notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired, client: React.PropTypes.object.isRequired,
// Mostly used for UI components showcase and unit tests // Mostly used for UI components showcase and unit tests
callUrl: React.PropTypes.string callUrl: React.PropTypes.string
}, },
render: function() { render: function() {
var NotificationListView = sharedViews.NotificationListView;
return ( return (
React.DOM.div(null, React.DOM.div(null,
NotificationListView({notifications: this.props.notifications}),
CallUrlResult({client: this.props.client, CallUrlResult({client: this.props.client,
notifier: this.props.notifier, notifications: this.props.notifications,
callUrl: this.props.callUrl}), callUrl: this.props.callUrl}),
ToSView(null), ToSView(null),
React.DOM.div({className: "footer"}, React.DOM.div({className: "footer"},
@ -454,7 +458,6 @@ loop.panel = (function(_, mozL10n) {
this._registerVisibilityChangeEvent(); this._registerVisibilityChangeEvent();
this.on("panel:open panel:closed", this.clearNotifications, this);
this.on("panel:open", this.reset, this); this.on("panel:open", this.reset, this);
}, },
@ -483,20 +486,16 @@ loop.panel = (function(_, mozL10n) {
this.reset(); this.reset();
}, },
clearNotifications: function() {
this._notifier.clear();
},
/** /**
* Resets this router to its initial state. * Resets this router to its initial state.
*/ */
reset: function() { reset: function() {
this._notifier.clear(); this._notifications.reset();
var client = new loop.Client({ var client = new loop.Client({
baseServerUrl: navigator.mozLoop.serverUrl baseServerUrl: navigator.mozLoop.serverUrl
}); });
this.loadReactComponent(PanelView({client: client, this.loadReactComponent(
notifier: this._notifier})); PanelView({client: client, notifications: this._notifications}));
} }
}); });
@ -510,7 +509,7 @@ loop.panel = (function(_, mozL10n) {
router = new PanelRouter({ router = new PanelRouter({
document: document, document: document,
notifier: new sharedViews.NotificationListView({el: "#messages"}) notifications: new sharedModels.NotificationCollection()
}); });
Backbone.history.start(); Backbone.history.start();

View File

@ -12,6 +12,7 @@ loop.panel = (function(_, mozL10n) {
"use strict"; "use strict";
var sharedViews = loop.shared.views, var sharedViews = loop.shared.views,
sharedModels = loop.shared.models,
// aliasing translation function as __ for concision // aliasing translation function as __ for concision
__ = mozL10n.get; __ = mozL10n.get;
@ -261,7 +262,7 @@ loop.panel = (function(_, mozL10n) {
propTypes: { propTypes: {
callUrl: React.PropTypes.string, callUrl: React.PropTypes.string,
callUrlExpiry: React.PropTypes.number, callUrlExpiry: React.PropTypes.number,
notifier: React.PropTypes.object.isRequired, notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired client: React.PropTypes.object.isRequired
}, },
@ -296,10 +297,10 @@ loop.panel = (function(_, mozL10n) {
}, },
_onCallUrlReceived: function(err, callUrlData) { _onCallUrlReceived: function(err, callUrlData) {
this.props.notifier.clear(); this.props.notifications.reset();
if (err) { if (err) {
this.props.notifier.errorL10n("unable_retrieve_url"); this.props.notifications.errorL10n("unable_retrieve_url");
this.setState(this.getInitialState()); this.setState(this.getInitialState());
} else { } else {
try { try {
@ -314,7 +315,7 @@ loop.panel = (function(_, mozL10n) {
callUrlExpiry: callUrlData.expiresAt}); callUrlExpiry: callUrlData.expiresAt});
} catch(e) { } catch(e) {
console.log(e); console.log(e);
this.props.notifier.errorL10n("unable_retrieve_url"); this.props.notifications.errorL10n("unable_retrieve_url");
this.setState(this.getInitialState()); this.setState(this.getInitialState());
} }
} }
@ -411,17 +412,20 @@ loop.panel = (function(_, mozL10n) {
*/ */
var PanelView = React.createClass({ var PanelView = React.createClass({
propTypes: { propTypes: {
notifier: React.PropTypes.object.isRequired, notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired, client: React.PropTypes.object.isRequired,
// Mostly used for UI components showcase and unit tests // Mostly used for UI components showcase and unit tests
callUrl: React.PropTypes.string callUrl: React.PropTypes.string
}, },
render: function() { render: function() {
var NotificationListView = sharedViews.NotificationListView;
return ( return (
<div> <div>
<NotificationListView notifications={this.props.notifications} />
<CallUrlResult client={this.props.client} <CallUrlResult client={this.props.client}
notifier={this.props.notifier} notifications={this.props.notifications}
callUrl={this.props.callUrl} /> callUrl={this.props.callUrl} />
<ToSView /> <ToSView />
<div className="footer"> <div className="footer">
@ -454,7 +458,6 @@ loop.panel = (function(_, mozL10n) {
this._registerVisibilityChangeEvent(); this._registerVisibilityChangeEvent();
this.on("panel:open panel:closed", this.clearNotifications, this);
this.on("panel:open", this.reset, this); this.on("panel:open", this.reset, this);
}, },
@ -483,20 +486,16 @@ loop.panel = (function(_, mozL10n) {
this.reset(); this.reset();
}, },
clearNotifications: function() {
this._notifier.clear();
},
/** /**
* Resets this router to its initial state. * Resets this router to its initial state.
*/ */
reset: function() { reset: function() {
this._notifier.clear(); this._notifications.reset();
var client = new loop.Client({ var client = new loop.Client({
baseServerUrl: navigator.mozLoop.serverUrl baseServerUrl: navigator.mozLoop.serverUrl
}); });
this.loadReactComponent(<PanelView client={client} this.loadReactComponent(
notifier={this._notifier} />); <PanelView client={client} notifications={this._notifications}/>);
} }
}); });
@ -510,7 +509,7 @@ loop.panel = (function(_, mozL10n) {
router = new PanelRouter({ router = new PanelRouter({
document: document, document: document,
notifier: new sharedViews.NotificationListView({el: "#messages"}) notifications: new sharedModels.NotificationCollection()
}); });
Backbone.history.start(); Backbone.history.start();

View File

@ -224,6 +224,7 @@ p {
background: #eee; background: #eee;
padding: .2em 1em; padding: .2em 1em;
margin-bottom: 1em; margin-bottom: 1em;
border-bottom: 2px solid #E9E9E9;
} }
.alert p.message { .alert p.message {
@ -232,8 +233,13 @@ p {
} }
.alert.alert-error { .alert.alert-error {
background: #f99; display: flex;
border: 1px solid #f77; align-content: center;
padding: 5px;
font-size: 10px;
justify-content: center;
color: #FFF;
background: repeating-linear-gradient(-45deg, #D74345, #D74345 10px, #D94B4D 10px, #D94B4D 20px) repeat scroll 0% 0% transparent;
} }
.alert.alert-warning { .alert.alert-warning {

View File

@ -393,15 +393,14 @@
background-image: url("../img/sad.png"); background-image: url("../img/sad.png");
} }
.feedback button.back { .fx-embedded-btn-back {
margin-bottom: 1rem;
padding: .2rem .8rem;
border: 1px solid #aaa;
border-radius: 2px; border-radius: 2px;
border: 1px solid #CCC; background: transparent;
color: #CCC; color: #777;
font-size: 11px;
cursor: pointer; cursor: pointer;
padding: 3px 10px;
display: inline;
margin-bottom: 14px;
} }
.feedback label { .feedback label {

View File

@ -6,7 +6,7 @@
var loop = loop || {}; var loop = loop || {};
loop.shared = loop.shared || {}; loop.shared = loop.shared || {};
loop.shared.models = (function() { loop.shared.models = (function(l10n) {
"use strict"; "use strict";
/** /**
@ -375,7 +375,43 @@ loop.shared.models = (function() {
* Notification collection * Notification collection
*/ */
var NotificationCollection = Backbone.Collection.extend({ var NotificationCollection = Backbone.Collection.extend({
model: NotificationModel model: NotificationModel,
/**
* Adds a warning notification to the stack and renders it.
*
* @return {String} message
*/
warn: function(message) {
this.add({level: "warning", message: message});
},
/**
* Adds a l10n warning notification to the stack and renders it.
*
* @param {String} messageId L10n message id
*/
warnL10n: function(messageId) {
this.warn(l10n.get(messageId));
},
/**
* Adds an error notification to the stack and renders it.
*
* @return {String} message
*/
error: function(message) {
this.add({level: "error", message: message});
},
/**
* Adds a l10n rror notification to the stack and renders it.
*
* @param {String} messageId L10n message id
*/
errorL10n: function(messageId) {
this.error(l10n.get(messageId));
}
}); });
return { return {
@ -383,4 +419,4 @@ loop.shared.models = (function() {
NotificationCollection: NotificationCollection, NotificationCollection: NotificationCollection,
NotificationModel: NotificationModel NotificationModel: NotificationModel
}; };
})(); })(navigator.mozL10n || document.mozL10n);

View File

@ -23,25 +23,25 @@ loop.shared.router = (function() {
_activeView: undefined, _activeView: undefined,
/** /**
* Notifications dispatcher. * Notifications collection.
* @type {loop.shared.views.NotificationListView} * @type {loop.shared.models.NotificationCollection}
*/ */
_notifier: undefined, _notifications: undefined,
/** /**
* Constructor. * Constructor.
* *
* Required options: * Required options:
* - {loop.shared.views.NotificationListView} notifier Notifier view. * - {loop.shared.models.NotificationCollection} notifications
* *
* @param {Object} options Options object. * @param {Object} options Options object.
*/ */
constructor: function(options) { constructor: function(options) {
options = options || {}; options = options || {};
if (!options.notifier) { if (!options.notifications) {
throw new Error("missing required notifier"); throw new Error("missing required notifications");
} }
this._notifier = options.notifier; this._notifications = options.notifications;
Backbone.Router.apply(this, arguments); Backbone.Router.apply(this, arguments);
}, },
@ -144,7 +144,7 @@ loop.shared.router = (function() {
*/ */
_notifyError: function(error) { _notifyError: function(error) {
console.log(error); console.log(error);
this._notifier.errorL10n("connection_error_see_console_notification"); this._notifications.errorL10n("connection_error_see_console_notification");
this.endCall(); this.endCall();
}, },
@ -169,7 +169,7 @@ loop.shared.router = (function() {
* @param {Object} event * @param {Object} event
*/ */
_onPeerHungup: function() { _onPeerHungup: function() {
this._notifier.warnL10n("peer_ended_conversation2"); this._notifications.warnL10n("peer_ended_conversation2");
this.endCall(); this.endCall();
}, },
@ -177,7 +177,7 @@ loop.shared.router = (function() {
* Network disconnected. Notifies the user and ends the call. * Network disconnected. Notifies the user and ends the call.
*/ */
_onNetworkDisconnected: function() { _onNetworkDisconnected: function() {
this._notifier.warnL10n("network_disconnected"); this._notifications.warnL10n("network_disconnected");
this.endCall(); this.endCall();
} }
}); });

View File

@ -407,7 +407,8 @@ loop.shared.views = (function(_, OT, l10n) {
var backButton = React.DOM.div(null); var backButton = React.DOM.div(null);
if (this.props.reset) { if (this.props.reset) {
backButton = ( backButton = (
React.DOM.button({className: "back", type: "button", onClick: this.props.reset}, React.DOM.button({className: "fx-embedded-btn-back", type: "button",
onClick: this.props.reset},
"« ", l10n.get("feedback_back_button") "« ", l10n.get("feedback_back_button")
) )
); );
@ -651,133 +652,55 @@ loop.shared.views = (function(_, OT, l10n) {
/** /**
* Notification view. * Notification view.
*/ */
var NotificationView = BaseView.extend({ var NotificationView = React.createClass({
template: _.template([ displayName: 'NotificationView',
'<div class="alert alert-<%- level %>">', mixins: [Backbone.Events],
' <button class="close"></button>',
' <p class="message"><%- message %></p>',
'</div>'
].join("")),
events: { propTypes: {
"click .close": "dismiss" notification: React.PropTypes.object.isRequired,
}, key: React.PropTypes.number.isRequired
dismiss: function(event) {
event.preventDefault();
this.$el.addClass("fade-out");
setTimeout(function() {
this.collection.remove(this.model);
this.remove();
}.bind(this), 500); // XXX make timeout value configurable
}, },
render: function() { render: function() {
this.$el.html(this.template(this.model.toJSON())); var notification = this.props.notification;
return this; return (
React.DOM.div({key: this.props.key,
className: "alert alert-" + notification.get("level")},
React.DOM.span({className: "message"}, notification.get("message"))
)
);
} }
}); });
/** /**
* Notification list view. * Notification list view.
*/ */
var NotificationListView = Backbone.View.extend({ var NotificationListView = React.createClass({displayName: 'NotificationListView',
/** mixins: [Backbone.Events],
* Constructor.
* propTypes: {
* Available options: notifications: React.PropTypes.object.isRequired
* - {loop.shared.models.NotificationCollection} collection Notifications
* collection
*
* @param {Object} options Options object
*/
initialize: function(options) {
options = options || {};
if (!options.collection) {
this.collection = new sharedModels.NotificationCollection();
}
this.listenTo(this.collection, "reset add remove", this.render);
}, },
/** componentDidMount: function() {
* Clears the notification stack. this.listenTo(this.props.notifications, "reset add remove", function() {
*/ this.forceUpdate();
clear: function() { }.bind(this));
this.collection.reset();
}, },
/** componentWillUnmount: function() {
* Adds a new notification to the stack, triggering rendering of it. this.stopListening(this.props.notifications);
*
* @param {Object|NotificationModel} notification Notification data.
*/
notify: function(notification) {
this.collection.add(notification);
}, },
/**
* Adds a new notification to the stack using an l10n message identifier,
* triggering rendering of it.
*
* @param {String} messageId L10n message id
* @param {String} level Notification level
*/
notifyL10n: function(messageId, level) {
this.notify({
message: l10n.get(messageId),
level: level
});
},
/**
* Adds a warning notification to the stack and renders it.
*
* @return {String} message
*/
warn: function(message) {
this.notify({level: "warning", message: message});
},
/**
* Adds a l10n warning notification to the stack and renders it.
*
* @param {String} messageId L10n message id
*/
warnL10n: function(messageId) {
this.warn(l10n.get(messageId));
},
/**
* Adds an error notification to the stack and renders it.
*
* @return {String} message
*/
error: function(message) {
this.notify({level: "error", message: message});
},
/**
* Adds a l10n rror notification to the stack and renders it.
*
* @param {String} messageId L10n message id
*/
errorL10n: function(messageId) {
this.error(l10n.get(messageId));
},
/**
* Renders this view.
*
* @return {loop.shared.views.NotificationListView}
*/
render: function() { render: function() {
this.$el.html(this.collection.map(function(notification) { return (
return new NotificationView({ React.DOM.div({id: "messages"},
model: notification, this.props.notifications.map(function(notification, key) {
collection: this.collection return NotificationView({key: key, notification: notification});
}).render().$el; })
}.bind(this)));
return this; )
);
} }
}); });
@ -817,7 +740,6 @@ loop.shared.views = (function(_, OT, l10n) {
FeedbackView: FeedbackView, FeedbackView: FeedbackView,
MediaControlButton: MediaControlButton, MediaControlButton: MediaControlButton,
NotificationListView: NotificationListView, NotificationListView: NotificationListView,
NotificationView: NotificationView,
UnsupportedBrowserView: UnsupportedBrowserView, UnsupportedBrowserView: UnsupportedBrowserView,
UnsupportedDeviceView: UnsupportedDeviceView UnsupportedDeviceView: UnsupportedDeviceView
}; };

View File

@ -407,7 +407,8 @@ loop.shared.views = (function(_, OT, l10n) {
var backButton = <div />; var backButton = <div />;
if (this.props.reset) { if (this.props.reset) {
backButton = ( backButton = (
<button className="back" type="button" onClick={this.props.reset}> <button className="fx-embedded-btn-back" type="button"
onClick={this.props.reset}>
&laquo;&nbsp;{l10n.get("feedback_back_button")} &laquo;&nbsp;{l10n.get("feedback_back_button")}
</button> </button>
); );
@ -651,133 +652,55 @@ loop.shared.views = (function(_, OT, l10n) {
/** /**
* Notification view. * Notification view.
*/ */
var NotificationView = BaseView.extend({ var NotificationView = React.createClass({
template: _.template([ displayName: 'NotificationView',
'<div class="alert alert-<%- level %>">', mixins: [Backbone.Events],
' <button class="close"></button>',
' <p class="message"><%- message %></p>',
'</div>'
].join("")),
events: { propTypes: {
"click .close": "dismiss" notification: React.PropTypes.object.isRequired,
}, key: React.PropTypes.number.isRequired
dismiss: function(event) {
event.preventDefault();
this.$el.addClass("fade-out");
setTimeout(function() {
this.collection.remove(this.model);
this.remove();
}.bind(this), 500); // XXX make timeout value configurable
}, },
render: function() { render: function() {
this.$el.html(this.template(this.model.toJSON())); var notification = this.props.notification;
return this; return (
<div key={this.props.key}
className={"alert alert-" + notification.get("level")}>
<span className="message">{notification.get("message")}</span>
</div>
);
} }
}); });
/** /**
* Notification list view. * Notification list view.
*/ */
var NotificationListView = Backbone.View.extend({ var NotificationListView = React.createClass({
/** mixins: [Backbone.Events],
* Constructor.
* propTypes: {
* Available options: notifications: React.PropTypes.object.isRequired
* - {loop.shared.models.NotificationCollection} collection Notifications
* collection
*
* @param {Object} options Options object
*/
initialize: function(options) {
options = options || {};
if (!options.collection) {
this.collection = new sharedModels.NotificationCollection();
}
this.listenTo(this.collection, "reset add remove", this.render);
}, },
/** componentDidMount: function() {
* Clears the notification stack. this.listenTo(this.props.notifications, "reset add remove", function() {
*/ this.forceUpdate();
clear: function() { }.bind(this));
this.collection.reset();
}, },
/** componentWillUnmount: function() {
* Adds a new notification to the stack, triggering rendering of it. this.stopListening(this.props.notifications);
*
* @param {Object|NotificationModel} notification Notification data.
*/
notify: function(notification) {
this.collection.add(notification);
}, },
/**
* Adds a new notification to the stack using an l10n message identifier,
* triggering rendering of it.
*
* @param {String} messageId L10n message id
* @param {String} level Notification level
*/
notifyL10n: function(messageId, level) {
this.notify({
message: l10n.get(messageId),
level: level
});
},
/**
* Adds a warning notification to the stack and renders it.
*
* @return {String} message
*/
warn: function(message) {
this.notify({level: "warning", message: message});
},
/**
* Adds a l10n warning notification to the stack and renders it.
*
* @param {String} messageId L10n message id
*/
warnL10n: function(messageId) {
this.warn(l10n.get(messageId));
},
/**
* Adds an error notification to the stack and renders it.
*
* @return {String} message
*/
error: function(message) {
this.notify({level: "error", message: message});
},
/**
* Adds a l10n rror notification to the stack and renders it.
*
* @param {String} messageId L10n message id
*/
errorL10n: function(messageId) {
this.error(l10n.get(messageId));
},
/**
* Renders this view.
*
* @return {loop.shared.views.NotificationListView}
*/
render: function() { render: function() {
this.$el.html(this.collection.map(function(notification) { return (
return new NotificationView({ <div id="messages">{
model: notification, this.props.notifications.map(function(notification, key) {
collection: this.collection return <NotificationView key={key} notification={notification}/>;
}).render().$el; })
}.bind(this))); }
return this; </div>
);
} }
}); });
@ -817,7 +740,6 @@ loop.shared.views = (function(_, OT, l10n) {
FeedbackView: FeedbackView, FeedbackView: FeedbackView,
MediaControlButton: MediaControlButton, MediaControlButton: MediaControlButton,
NotificationListView: NotificationListView, NotificationListView: NotificationListView,
NotificationView: NotificationView,
UnsupportedBrowserView: UnsupportedBrowserView, UnsupportedBrowserView: UnsupportedBrowserView,
UnsupportedDeviceView: UnsupportedDeviceView UnsupportedDeviceView: UnsupportedDeviceView
}; };

View File

@ -135,8 +135,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
* Constructor. * Constructor.
* *
* Required options: * Required options:
* - {loop.shared.model.ConversationModel} model Conversation model. * - {loop.shared.models.ConversationModel} model Conversation model.
* - {loop.shared.views.NotificationListView} notifier Notifier component. * - {loop.shared.models.NotificationCollection} notifications
* *
*/ */
@ -156,7 +156,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
model: React.PropTypes.instanceOf(sharedModels.ConversationModel) model: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired, .isRequired,
// XXX Check more tightly here when we start injecting window.loop.* // XXX Check more tightly here when we start injecting window.loop.*
notifier: React.PropTypes.object.isRequired, notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired client: React.PropTypes.object.isRequired
}, },
@ -167,14 +167,11 @@ loop.webapp = (function($, _, OT, mozL10n) {
this._onSessionError); this._onSessionError);
this.props.client.requestCallUrlInfo(this.props.model.get("loopToken"), this.props.client.requestCallUrlInfo(this.props.model.get("loopToken"),
this._setConversationTimestamp); this._setConversationTimestamp);
// XXX DOM element does not exist before React view gets instantiated
// We should turn the notifier into a react component
this.props.notifier.$el = $("#messages");
}, },
_onSessionError: function(error) { _onSessionError: function(error) {
console.error(error); console.error(error);
this.props.notifier.errorL10n("unable_retrieve_call_info"); this.props.notifications.errorL10n("unable_retrieve_call_info");
}, },
/** /**
@ -195,7 +192,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
_setConversationTimestamp: function(err, callUrlInfo) { _setConversationTimestamp: function(err, callUrlInfo) {
if (err) { if (err) {
this.props.notifier.errorL10n("unable_retrieve_call_info"); this.props.notifications.errorL10n("unable_retrieve_call_info");
} else { } else {
var date = (new Date(callUrlInfo.urlCreationDate * 1000)); var date = (new Date(callUrlInfo.urlCreationDate * 1000));
var options = {year: "numeric", month: "long", day: "numeric"}; var options = {year: "numeric", month: "long", day: "numeric"};
@ -343,7 +340,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
setupOutgoingCall: function() { setupOutgoingCall: function() {
var loopToken = this._conversation.get("loopToken"); var loopToken = this._conversation.get("loopToken");
if (!loopToken) { if (!loopToken) {
this._notifier.errorL10n("missing_conversation_info"); this._notifications.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true}); this.navigate("home", {trigger: true});
} else { } else {
var callType = this._conversation.get("selectedCallType"); var callType = this._conversation.get("selectedCallType");
@ -361,7 +358,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
this._onSessionExpired(); this._onSessionExpired();
break; break;
default: default:
this._notifier.errorL10n("missing_conversation_info"); this._notifications.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true}); this.navigate("home", {trigger: true});
break; break;
} }
@ -378,7 +375,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
startCall: function() { startCall: function() {
var loopToken = this._conversation.get("loopToken"); var loopToken = this._conversation.get("loopToken");
if (!loopToken) { if (!loopToken) {
this._notifier.errorL10n("missing_conversation_info"); this._notifications.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true}); this.navigate("home", {trigger: true});
} else { } else {
this._setupWebSocketAndCallView(loopToken); this._setupWebSocketAndCallView(loopToken);
@ -404,7 +401,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
}.bind(this), function() { }.bind(this), function() {
// XXX Not the ideal response, but bug 1047410 will be replacing // XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI. // this by better "call failed" UI.
this._notifier.errorL10n("cannot_start_call_session_not_ready"); this._notifications.errorL10n("cannot_start_call_session_not_ready");
return; return;
}.bind(this)); }.bind(this));
@ -451,7 +448,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
*/ */
_handleCallRejected: function() { _handleCallRejected: function() {
this.endCall(); this.endCall();
this._notifier.errorL10n("call_timeout_notification_text"); this._notifications.errorL10n("call_timeout_notification_text");
}, },
/** /**
@ -466,7 +463,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
}, },
_onTimeout: function() { _onTimeout: function() {
this._notifier.errorL10n("call_timeout_notification_text"); this._notifications.errorL10n("call_timeout_notification_text");
}, },
/** /**
@ -504,7 +501,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
var startView = StartConversationView({ var startView = StartConversationView({
model: this._conversation, model: this._conversation,
notifier: this._notifier, notifications: this._notifications,
client: this._client client: this._client
}); });
this._conversation.once("call:outgoing:setup", this.setupOutgoingCall, this); this._conversation.once("call:outgoing:setup", this.setupOutgoingCall, this);
@ -557,7 +554,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
}); });
var router = new WebappRouter({ var router = new WebappRouter({
helper: helper, helper: helper,
notifier: new sharedViews.NotificationListView({el: "#messages"}), notifications: new sharedModels.NotificationCollection(),
client: client, client: client,
conversation: new sharedModels.ConversationModel({}, { conversation: new sharedModels.ConversationModel({}, {
sdk: OT, sdk: OT,

View File

@ -135,8 +135,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
* Constructor. * Constructor.
* *
* Required options: * Required options:
* - {loop.shared.model.ConversationModel} model Conversation model. * - {loop.shared.models.ConversationModel} model Conversation model.
* - {loop.shared.views.NotificationListView} notifier Notifier component. * - {loop.shared.models.NotificationCollection} notifications
* *
*/ */
@ -156,7 +156,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
model: React.PropTypes.instanceOf(sharedModels.ConversationModel) model: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired, .isRequired,
// XXX Check more tightly here when we start injecting window.loop.* // XXX Check more tightly here when we start injecting window.loop.*
notifier: React.PropTypes.object.isRequired, notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired client: React.PropTypes.object.isRequired
}, },
@ -167,14 +167,11 @@ loop.webapp = (function($, _, OT, mozL10n) {
this._onSessionError); this._onSessionError);
this.props.client.requestCallUrlInfo(this.props.model.get("loopToken"), this.props.client.requestCallUrlInfo(this.props.model.get("loopToken"),
this._setConversationTimestamp); this._setConversationTimestamp);
// XXX DOM element does not exist before React view gets instantiated
// We should turn the notifier into a react component
this.props.notifier.$el = $("#messages");
}, },
_onSessionError: function(error) { _onSessionError: function(error) {
console.error(error); console.error(error);
this.props.notifier.errorL10n("unable_retrieve_call_info"); this.props.notifications.errorL10n("unable_retrieve_call_info");
}, },
/** /**
@ -195,7 +192,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
_setConversationTimestamp: function(err, callUrlInfo) { _setConversationTimestamp: function(err, callUrlInfo) {
if (err) { if (err) {
this.props.notifier.errorL10n("unable_retrieve_call_info"); this.props.notifications.errorL10n("unable_retrieve_call_info");
} else { } else {
var date = (new Date(callUrlInfo.urlCreationDate * 1000)); var date = (new Date(callUrlInfo.urlCreationDate * 1000));
var options = {year: "numeric", month: "long", day: "numeric"}; var options = {year: "numeric", month: "long", day: "numeric"};
@ -343,7 +340,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
setupOutgoingCall: function() { setupOutgoingCall: function() {
var loopToken = this._conversation.get("loopToken"); var loopToken = this._conversation.get("loopToken");
if (!loopToken) { if (!loopToken) {
this._notifier.errorL10n("missing_conversation_info"); this._notifications.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true}); this.navigate("home", {trigger: true});
} else { } else {
var callType = this._conversation.get("selectedCallType"); var callType = this._conversation.get("selectedCallType");
@ -361,7 +358,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
this._onSessionExpired(); this._onSessionExpired();
break; break;
default: default:
this._notifier.errorL10n("missing_conversation_info"); this._notifications.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true}); this.navigate("home", {trigger: true});
break; break;
} }
@ -378,7 +375,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
startCall: function() { startCall: function() {
var loopToken = this._conversation.get("loopToken"); var loopToken = this._conversation.get("loopToken");
if (!loopToken) { if (!loopToken) {
this._notifier.errorL10n("missing_conversation_info"); this._notifications.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true}); this.navigate("home", {trigger: true});
} else { } else {
this._setupWebSocketAndCallView(loopToken); this._setupWebSocketAndCallView(loopToken);
@ -404,7 +401,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
}.bind(this), function() { }.bind(this), function() {
// XXX Not the ideal response, but bug 1047410 will be replacing // XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI. // this by better "call failed" UI.
this._notifier.errorL10n("cannot_start_call_session_not_ready"); this._notifications.errorL10n("cannot_start_call_session_not_ready");
return; return;
}.bind(this)); }.bind(this));
@ -451,7 +448,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
*/ */
_handleCallRejected: function() { _handleCallRejected: function() {
this.endCall(); this.endCall();
this._notifier.errorL10n("call_timeout_notification_text"); this._notifications.errorL10n("call_timeout_notification_text");
}, },
/** /**
@ -466,7 +463,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
}, },
_onTimeout: function() { _onTimeout: function() {
this._notifier.errorL10n("call_timeout_notification_text"); this._notifications.errorL10n("call_timeout_notification_text");
}, },
/** /**
@ -504,7 +501,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
var startView = StartConversationView({ var startView = StartConversationView({
model: this._conversation, model: this._conversation,
notifier: this._notifier, notifications: this._notifications,
client: this._client client: this._client
}); });
this._conversation.once("call:outgoing:setup", this.setupOutgoingCall, this); this._conversation.once("call:outgoing:setup", this.setupOutgoingCall, this);
@ -557,7 +554,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
}); });
var router = new WebappRouter({ var router = new WebappRouter({
helper: helper, helper: helper,
notifier: new sharedViews.NotificationListView({el: "#messages"}), notifications: new sharedModels.NotificationCollection(),
client: client, client: client,
conversation: new sharedModels.ConversationModel({}, { conversation: new sharedModels.ConversationModel({}, {
sdk: OT, sdk: OT,

View File

@ -11,18 +11,12 @@ describe("loop.conversation", function() {
var ConversationRouter = loop.conversation.ConversationRouter, var ConversationRouter = loop.conversation.ConversationRouter,
sandbox, sandbox,
notifier; notifications;
beforeEach(function() { beforeEach(function() {
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
sandbox.useFakeTimers(); sandbox.useFakeTimers();
notifier = { notifications = new loop.shared.models.NotificationCollection();
notify: sandbox.spy(),
warn: sandbox.spy(),
warnL10n: sandbox.spy(),
error: sandbox.spy(),
errorL10n: sandbox.spy()
};
navigator.mozLoop = { navigator.mozLoop = {
doNotDisturb: true, doNotDisturb: true,
@ -73,8 +67,6 @@ describe("loop.conversation", function() {
"initialize"); "initialize");
sandbox.stub(loop.shared.models.ConversationModel.prototype, sandbox.stub(loop.shared.models.ConversationModel.prototype,
"initialize"); "initialize");
sandbox.stub(loop.shared.views.NotificationListView.prototype,
"initialize");
sandbox.stub(Backbone.history, "start"); sandbox.stub(Backbone.history, "start");
}); });
@ -132,7 +124,7 @@ describe("loop.conversation", function() {
router = new ConversationRouter({ router = new ConversationRouter({
client: client, client: client,
conversation: conversation, conversation: conversation,
notifier: notifier notifications: notifications
}); });
sandbox.stub(router, "loadView"); sandbox.stub(router, "loadView");
sandbox.stub(conversation, "incoming"); sandbox.stub(conversation, "incoming");
@ -181,11 +173,12 @@ describe("loop.conversation", function() {
it("should display an error if requestCallsInfo returns an error", it("should display an error if requestCallsInfo returns an error",
function(){ function(){
sandbox.stub(notifications, "errorL10n");
client.requestCallsInfo.callsArgWith(1, "failed"); client.requestCallsInfo.callsArgWith(1, "failed");
router.incoming(42); router.incoming(42);
sinon.assert.calledOnce(notifier.errorL10n); sinon.assert.calledOnce(notifications.errorL10n);
}); });
describe("requestCallsInfo successful", function() { describe("requestCallsInfo successful", function() {
@ -297,12 +290,13 @@ describe("loop.conversation", function() {
}); });
it("should display an error", function(done) { it("should display an error", function(done) {
sandbox.stub(notifications, "errorL10n");
router._setupWebSocketAndCallView(); router._setupWebSocketAndCallView();
promise.then(function() { promise.then(function() {
}, function () { }, function () {
sinon.assert.calledOnce(router._notifier.errorL10n); sinon.assert.calledOnce(router._notifications.errorL10n);
sinon.assert.calledWithExactly(router._notifier.errorL10n, sinon.assert.calledWithExactly(router._notifications.errorL10n,
"cannot_start_call_session_not_ready"); "cannot_start_call_session_not_ready");
done(); done();
}); });
@ -374,10 +368,11 @@ describe("loop.conversation", function() {
it("should notify the user when session is not set", it("should notify the user when session is not set",
function() { function() {
sandbox.stub(notifications, "errorL10n");
router.conversation(); router.conversation();
sinon.assert.calledOnce(router._notifier.errorL10n); sinon.assert.calledOnce(router._notifications.errorL10n);
sinon.assert.calledWithExactly(router._notifier.errorL10n, sinon.assert.calledWithExactly(router._notifications.errorL10n,
"cannot_start_call_session_not_ready"); "cannot_start_call_session_not_ready");
}); });
}); });
@ -517,7 +512,7 @@ describe("loop.conversation", function() {
router = new loop.conversation.ConversationRouter({ router = new loop.conversation.ConversationRouter({
client: client, client: client,
conversation: conversation, conversation: conversation,
notifier: notifier notifications: notifications
}); });
}); });

View File

@ -10,11 +10,11 @@ var TestUtils = React.addons.TestUtils;
describe("loop.panel", function() { describe("loop.panel", function() {
"use strict"; "use strict";
var sandbox, notifier, fakeXHR, requests = []; var sandbox, notifications, fakeXHR, requests = [];
function createTestRouter(fakeDocument) { function createTestRouter(fakeDocument) {
return new loop.panel.PanelRouter({ return new loop.panel.PanelRouter({
notifier: notifier, notifications: notifications,
document: fakeDocument document: fakeDocument
}); });
} }
@ -27,14 +27,7 @@ describe("loop.panel", function() {
fakeXHR.xhr.onCreate = function (xhr) { fakeXHR.xhr.onCreate = function (xhr) {
requests.push(xhr); requests.push(xhr);
}; };
notifier = { notifications = new loop.shared.models.NotificationCollection();
clear: sandbox.spy(),
notify: sandbox.spy(),
warn: sandbox.spy(),
warnL10n: sandbox.spy(),
error: sandbox.spy(),
errorL10n: sandbox.spy()
};
navigator.mozLoop = { navigator.mozLoop = {
doNotDisturb: true, doNotDisturb: true,
@ -63,15 +56,15 @@ describe("loop.panel", function() {
describe("loop.panel.PanelRouter", function() { describe("loop.panel.PanelRouter", function() {
describe("#constructor", function() { describe("#constructor", function() {
it("should require a notifier", function() { it("should require a notifications collection", function() {
expect(function() { expect(function() {
new loop.panel.PanelRouter(); new loop.panel.PanelRouter();
}).to.Throw(Error, /missing required notifier/); }).to.Throw(Error, /missing required notifications/);
}); });
it("should require a document", function() { it("should require a document", function() {
expect(function() { expect(function() {
new loop.panel.PanelRouter({notifier: notifier}); new loop.panel.PanelRouter({notifications: notifications});
}).to.Throw(Error, /missing required document/); }).to.Throw(Error, /missing required document/);
}); });
}); });
@ -101,9 +94,10 @@ describe("loop.panel", function() {
describe("#reset", function() { describe("#reset", function() {
it("should clear all pending notifications", function() { it("should clear all pending notifications", function() {
sandbox.stub(notifications, "reset");
router.reset(); router.reset();
sinon.assert.calledOnce(notifier.clear); sinon.assert.calledOnce(notifications.reset);
}); });
it("should load the home view", function() { it("should load the home view", function() {
@ -213,7 +207,7 @@ describe("loop.panel", function() {
}; };
view = TestUtils.renderIntoDocument(loop.panel.PanelView({ view = TestUtils.renderIntoDocument(loop.panel.PanelView({
notifier: notifier, notifications: notifications,
client: fakeClient client: fakeClient
})); }));
}); });
@ -324,8 +318,9 @@ describe("loop.panel", function() {
} }
}; };
sandbox.stub(notifications, "reset");
view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({ view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
notifier: notifier, notifications: notifications,
client: fakeClient client: fakeClient
})); }));
}); });
@ -350,7 +345,7 @@ describe("loop.panel", function() {
it("should make a request to requestCallUrl", function() { it("should make a request to requestCallUrl", function() {
sandbox.stub(fakeClient, "requestCallUrl"); sandbox.stub(fakeClient, "requestCallUrl");
var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({ var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
notifier: notifier, notifications: notifications,
client: fakeClient client: fakeClient
})); }));
@ -363,7 +358,7 @@ describe("loop.panel", function() {
// Cancel requestCallUrl effect to keep the state pending // Cancel requestCallUrl effect to keep the state pending
fakeClient.requestCallUrl = sandbox.stub(); fakeClient.requestCallUrl = sandbox.stub();
var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({ var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
notifier: notifier, notifications: notifications,
client: fakeClient client: fakeClient
})); }));
@ -387,14 +382,14 @@ describe("loop.panel", function() {
}); });
it("should reset all pending notifications", function() { it("should reset all pending notifications", function() {
sinon.assert.calledOnce(view.props.notifier.clear); sinon.assert.calledOnce(view.props.notifications.reset);
}); });
it("should display a share button for email", function() { it("should display a share button for email", function() {
fakeClient.requestCallUrl = sandbox.stub(); fakeClient.requestCallUrl = sandbox.stub();
var mailto = 'mailto:?subject=email-subject&body=http://example.com'; var mailto = 'mailto:?subject=email-subject&body=http://example.com';
var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({ var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
notifier: notifier, notifications: notifications,
client: fakeClient client: fakeClient
})); }));
view.setState({pending: false, callUrl: "http://example.com"}); view.setState({pending: false, callUrl: "http://example.com"});
@ -407,7 +402,7 @@ describe("loop.panel", function() {
it("should feature a copy button capable of copying the call url when clicked", function() { it("should feature a copy button capable of copying the call url when clicked", function() {
fakeClient.requestCallUrl = sandbox.stub(); fakeClient.requestCallUrl = sandbox.stub();
var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({ var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
notifier: notifier, notifications: notifications,
client: fakeClient client: fakeClient
})); }));
view.setState({ view.setState({
@ -427,7 +422,7 @@ describe("loop.panel", function() {
it("should note the call url expiry when the url is copied via button", it("should note the call url expiry when the url is copied via button",
function() { function() {
var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({ var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
notifier: notifier, notifications: notifications,
client: fakeClient client: fakeClient
})); }));
view.setState({ view.setState({
@ -447,7 +442,7 @@ describe("loop.panel", function() {
it("should note the call url expiry when the url is emailed", it("should note the call url expiry when the url is emailed",
function() { function() {
var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({ var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
notifier: notifier, notifications: notifications,
client: fakeClient client: fakeClient
})); }));
view.setState({ view.setState({
@ -468,7 +463,7 @@ describe("loop.panel", function() {
it("should note the call url expiry when the url is copied manually", it("should note the call url expiry when the url is copied manually",
function() { function() {
var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({ var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
notifier: notifier, notifications: notifications,
client: fakeClient client: fakeClient
})); }));
view.setState({ view.setState({
@ -490,13 +485,14 @@ describe("loop.panel", function() {
fakeClient.requestCallUrl = function(_, cb) { fakeClient.requestCallUrl = function(_, cb) {
cb("fake error"); cb("fake error");
}; };
sandbox.stub(notifications, "errorL10n");
var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({ var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
notifier: notifier, notifications: notifications,
client: fakeClient client: fakeClient
})); }));
sinon.assert.calledOnce(notifier.errorL10n); sinon.assert.calledOnce(notifications.errorL10n);
sinon.assert.calledWithExactly(notifier.errorL10n, sinon.assert.calledWithExactly(notifications.errorL10n,
"unable_retrieve_url"); "unable_retrieve_url");
}); });
}); });

View File

@ -401,4 +401,58 @@ describe("loop.shared.models", function() {
}); });
}); });
}); });
describe("NotificationCollection", function() {
var collection, notifData, testNotif;
beforeEach(function() {
collection = new sharedModels.NotificationCollection();
sandbox.stub(l10n, "get", function(x) {
return "translated:" + x;
});
notifData = {level: "error", message: "plop"};
testNotif = new sharedModels.NotificationModel(notifData);
});
describe("#warn", function() {
it("should add a warning notification to the stack", function() {
collection.warn("watch out");
expect(collection).to.have.length.of(1);
expect(collection.at(0).get("level")).eql("warning");
expect(collection.at(0).get("message")).eql("watch out");
});
});
describe("#warnL10n", function() {
it("should warn using a l10n string id", function() {
collection.warnL10n("fakeId");
expect(collection).to.have.length.of(1);
expect(collection.at(0).get("level")).eql("warning");
expect(collection.at(0).get("message")).eql("translated:fakeId");
});
});
describe("#error", function() {
it("should add an error notification to the stack", function() {
collection.error("wrong");
expect(collection).to.have.length.of(1);
expect(collection.at(0).get("level")).eql("error");
expect(collection.at(0).get("message")).eql("wrong");
});
});
describe("#errorL10n", function() {
it("should notify an error using a l10n string id", function() {
collection.errorL10n("fakeId");
expect(collection).to.have.length.of(1);
expect(collection.at(0).get("level")).eql("error");
expect(collection.at(0).get("message")).eql("translated:fakeId");
});
});
});
}); });

View File

@ -9,17 +9,13 @@ var expect = chai.expect;
describe("loop.shared.router", function() { describe("loop.shared.router", function() {
"use strict"; "use strict";
var sandbox, notifier; var sandbox, notifications;
beforeEach(function() { beforeEach(function() {
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
notifier = { notifications = new loop.shared.models.NotificationCollection();
notify: sandbox.spy(), sandbox.stub(notifications, "errorL10n");
warn: sandbox.spy(), sandbox.stub(notifications, "warnL10n");
warnL10n: sandbox.spy(),
error: sandbox.spy(),
errorL10n: sandbox.spy()
};
}); });
afterEach(function() { afterEach(function() {
@ -36,19 +32,19 @@ describe("loop.shared.router", function() {
}); });
describe("#constructor", function() { describe("#constructor", function() {
it("should require a notifier", function() { it("should require a notifications collection", function() {
expect(function() { expect(function() {
new loop.shared.router.BaseRouter(); new loop.shared.router.BaseRouter();
}).to.Throw(Error, /missing required notifier/); }).to.Throw(Error, /missing required notifications/);
}); });
describe("inherited", function() { describe("inherited", function() {
var ExtendedRouter = loop.shared.router.BaseRouter.extend({}); var ExtendedRouter = loop.shared.router.BaseRouter.extend({});
it("should require a notifier", function() { it("should require a notifications collection", function() {
expect(function() { expect(function() {
new ExtendedRouter(); new ExtendedRouter();
}).to.Throw(Error, /missing required notifier/); }).to.Throw(Error, /missing required notifications/);
}); });
}); });
}); });
@ -62,7 +58,7 @@ describe("loop.shared.router", function() {
template: _.template("<p>plop</p>") template: _.template("<p>plop</p>")
}); });
view = new TestView(); view = new TestView();
router = new TestRouter({notifier: notifier}); router = new TestRouter({notifications: notifications});
}); });
describe("#loadView", function() { describe("#loadView", function() {
@ -131,7 +127,7 @@ describe("loop.shared.router", function() {
}; };
router = new TestRouter({ router = new TestRouter({
conversation: conversation, conversation: conversation,
notifier: notifier, notifications: notifications,
client: {} client: {}
}); });
}); });
@ -141,8 +137,8 @@ describe("loop.shared.router", function() {
it("should warn the user when .connect() call fails", function() { it("should warn the user when .connect() call fails", function() {
conversation.trigger("session:connection-error"); conversation.trigger("session:connection-error");
sinon.assert.calledOnce(notifier.errorL10n); sinon.assert.calledOnce(notifications.errorL10n);
sinon.assert.calledWithExactly(notifier.errorL10n, sinon.match.string); sinon.assert.calledWithExactly(notifications.errorL10n, sinon.match.string);
}); });
it("should invoke endCall()", function() { it("should invoke endCall()", function() {
@ -163,8 +159,8 @@ describe("loop.shared.router", function() {
it("should warn the user when peer hangs up", function() { it("should warn the user when peer hangs up", function() {
conversation.trigger("session:peer-hungup"); conversation.trigger("session:peer-hungup");
sinon.assert.calledOnce(notifier.warnL10n); sinon.assert.calledOnce(notifications.warnL10n);
sinon.assert.calledWithExactly(notifier.warnL10n, sinon.assert.calledWithExactly(notifications.warnL10n,
"peer_ended_conversation2"); "peer_ended_conversation2");
}); });
@ -178,8 +174,8 @@ describe("loop.shared.router", function() {
it("should warn the user when network disconnects", function() { it("should warn the user when network disconnects", function() {
conversation.trigger("session:network-disconnected"); conversation.trigger("session:network-disconnected");
sinon.assert.calledOnce(notifier.warnL10n); sinon.assert.calledOnce(notifications.warnL10n);
sinon.assert.calledWithExactly(notifier.warnL10n, sinon.assert.calledWithExactly(notifications.warnL10n,
"network_disconnected"); "network_disconnected");
}); });

View File

@ -597,195 +597,28 @@ describe("loop.shared.views", function() {
}); });
}); });
describe("NotificationView", function() {
var collection, model, view;
beforeEach(function() {
$("#fixtures").append('<div id="test-notif"></div>');
model = new sharedModels.NotificationModel({
level: "error",
message: "plop"
});
collection = new sharedModels.NotificationCollection([model]);
view = new sharedViews.NotificationView({
el: $("#test-notif"),
collection: collection,
model: model
});
});
describe("#dismiss", function() {
it("should automatically dismiss notification after 500ms", function() {
view.render().dismiss({preventDefault: sandbox.spy()});
expect(view.$(".message").text()).eql("plop");
sandbox.clock.tick(500);
expect(collection).to.have.length.of(0);
expect($("#test-notif").html()).eql(undefined);
});
});
describe("#render", function() {
it("should render template with model attribute values", function() {
view.render();
expect(view.$(".message").text()).eql("plop");
});
});
});
describe("NotificationListView", function() { describe("NotificationListView", function() {
var coll, notifData, testNotif; var coll, view, testNotif;
function mountTestComponent(props) {
return TestUtils.renderIntoDocument(sharedViews.NotificationListView(props));
}
beforeEach(function() { beforeEach(function() {
sandbox.stub(l10n, "get", function(x) { sandbox.stub(l10n, "get", function(x) {
return "translated:" + x; return "translated:" + x;
}); });
notifData = {level: "error", message: "plop"};
testNotif = new sharedModels.NotificationModel(notifData);
coll = new sharedModels.NotificationCollection(); coll = new sharedModels.NotificationCollection();
view = mountTestComponent({notifications: coll});
testNotif = {level: "warning", message: "foo"};
sinon.spy(view, "render");
}); });
describe("#initialize", function() { afterEach(function() {
it("should accept a collection option", function() { view.render.restore();
var view = new sharedViews.NotificationListView({collection: coll});
expect(view.collection).to.be.an.instanceOf(
sharedModels.NotificationCollection);
});
it("should set a default collection when none is passed", function() {
var view = new sharedViews.NotificationListView();
expect(view.collection).to.be.an.instanceOf(
sharedModels.NotificationCollection);
});
});
describe("#clear", function() {
it("should clear all notifications from the collection", function() {
var view = new sharedViews.NotificationListView();
view.notify(testNotif);
view.clear();
expect(coll).to.have.length.of(0);
});
});
describe("#notify", function() {
var view;
beforeEach(function() {
view = new sharedViews.NotificationListView({collection: coll});
});
describe("adds a new notification to the stack", function() {
it("using a plain object", function() {
view.notify(notifData);
expect(coll).to.have.length.of(1);
});
it("using a NotificationModel instance", function() {
view.notify(testNotif);
expect(coll).to.have.length.of(1);
});
});
});
describe("#notifyL10n", function() {
var view;
beforeEach(function() {
view = new sharedViews.NotificationListView({collection: coll});
});
it("should translate a message string identifier", function() {
view.notifyL10n("fakeId", "warning");
sinon.assert.calledOnce(l10n.get);
sinon.assert.calledWithExactly(l10n.get, "fakeId");
});
it("should notify end user with the provided message", function() {
sandbox.stub(view, "notify");
view.notifyL10n("fakeId", "warning");
sinon.assert.calledOnce(view.notify);
sinon.assert.calledWithExactly(view.notify, {
message: "translated:fakeId",
level: "warning"
});
});
});
describe("#warn", function() {
it("should add a warning notification to the stack", function() {
var view = new sharedViews.NotificationListView({collection: coll});
view.warn("watch out");
expect(coll).to.have.length.of(1);
expect(coll.at(0).get("level")).eql("warning");
expect(coll.at(0).get("message")).eql("watch out");
});
});
describe("#warnL10n", function() {
it("should warn using a l10n string id", function() {
var view = new sharedViews.NotificationListView({collection: coll});
sandbox.stub(view, "notify");
view.warnL10n("fakeId");
sinon.assert.called(view.notify);
sinon.assert.calledWithExactly(view.notify, {
message: "translated:fakeId",
level: "warning"
});
});
});
describe("#error", function() {
it("should add an error notification to the stack", function() {
var view = new sharedViews.NotificationListView({collection: coll});
view.error("wrong");
expect(coll).to.have.length.of(1);
expect(coll.at(0).get("level")).eql("error");
expect(coll.at(0).get("message")).eql("wrong");
});
});
describe("#errorL10n", function() {
it("should notify an error using a l10n string id", function() {
var view = new sharedViews.NotificationListView({collection: coll});
sandbox.stub(view, "notify");
view.errorL10n("fakeId");
sinon.assert.called(view.notify);
sinon.assert.calledWithExactly(view.notify, {
message: "translated:fakeId",
level: "error"
});
});
}); });
describe("Collection events", function() { describe("Collection events", function() {
var view;
beforeEach(function() {
sandbox.stub(sharedViews.NotificationListView.prototype, "render");
view = new sharedViews.NotificationListView({collection: coll});
});
it("should render when a notification is added to the collection", it("should render when a notification is added to the collection",
function() { function() {
coll.add(testNotif); coll.add(testNotif);
@ -798,7 +631,7 @@ describe("loop.shared.views", function() {
coll.add(testNotif); coll.add(testNotif);
coll.remove(testNotif); coll.remove(testNotif);
sinon.assert.calledTwice(view.render); sinon.assert.calledOnce(view.render);
}); });
it("should render when the collection is reset", function() { it("should render when the collection is reset", function() {

View File

@ -13,20 +13,14 @@ describe("loop.webapp", function() {
var sharedModels = loop.shared.models, var sharedModels = loop.shared.models,
sharedViews = loop.shared.views, sharedViews = loop.shared.views,
sandbox, sandbox,
notifier; notifications;
beforeEach(function() { beforeEach(function() {
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
// conversation#outgoing sets timers, so we need to use fake ones // conversation#outgoing sets timers, so we need to use fake ones
// to prevent random failures. // to prevent random failures.
sandbox.useFakeTimers(); sandbox.useFakeTimers();
notifier = { notifications = new sharedModels.NotificationCollection();
notify: sandbox.spy(),
warn: sandbox.spy(),
warnL10n: sandbox.spy(),
error: sandbox.spy(),
errorL10n: sandbox.spy(),
};
loop.config.pendingCallTimeout = 1000; loop.config.pendingCallTimeout = 1000;
}); });
@ -88,7 +82,7 @@ describe("loop.webapp", function() {
helper: {}, helper: {},
client: client, client: client,
conversation: conversation, conversation: conversation,
notifier: notifier notifications: notifications
}); });
sandbox.stub(router, "loadView"); sandbox.stub(router, "loadView");
sandbox.stub(router, "navigate"); sandbox.stub(router, "navigate");
@ -107,10 +101,11 @@ describe("loop.webapp", function() {
}); });
it("should notify the user if session token is missing", function() { it("should notify the user if session token is missing", function() {
sandbox.stub(notifications, "errorL10n");
router.startCall(); router.startCall();
sinon.assert.calledOnce(notifier.errorL10n); sinon.assert.calledOnce(notifications.errorL10n);
sinon.assert.calledWithExactly(notifier.errorL10n, sinon.assert.calledWithExactly(notifications.errorL10n,
"missing_conversation_info"); "missing_conversation_info");
}); });
@ -194,13 +189,14 @@ describe("loop.webapp", function() {
}); });
}); });
it("should display an error", function() { it("should display an error", function(done) {
sandbox.stub(notifications, "errorL10n");
router._setupWebSocketAndCallView(); router._setupWebSocketAndCallView();
promise.then(function() { promise.then(function() {
}, function () { }, function () {
sinon.assert.calledOnce(router._notifier.errorL10n); sinon.assert.calledOnce(router._notifications.errorL10n);
sinon.assert.calledWithExactly(router._notifier.errorL10n, sinon.assert.calledWithExactly(router._notifications.errorL10n,
"cannot_start_call_session_not_ready"); "cannot_start_call_session_not_ready");
done(); done();
}); });
@ -242,13 +238,15 @@ describe("loop.webapp", function() {
}); });
it("should display an error message", function() { it("should display an error message", function() {
sandbox.stub(notifications, "errorL10n");
router._websocket.trigger("progress", { router._websocket.trigger("progress", {
state: "terminated", state: "terminated",
reason: "reject" reason: "reject"
}); });
sinon.assert.calledOnce(router._notifier.errorL10n); sinon.assert.calledOnce(router._notifications.errorL10n);
sinon.assert.calledWithExactly(router._notifier.errorL10n, sinon.assert.calledWithExactly(router._notifications.errorL10n,
"call_timeout_notification_text"); "call_timeout_notification_text");
}); });
}); });
@ -472,9 +470,10 @@ describe("loop.webapp", function() {
}); });
it("should display an error", function() { it("should display an error", function() {
sandbox.stub(notifications, "errorL10n");
conversation.setupOutgoingCall(); conversation.setupOutgoingCall();
sinon.assert.calledOnce(notifier.errorL10n); sinon.assert.calledOnce(notifications.errorL10n);
}); });
}); });
@ -513,10 +512,11 @@ describe("loop.webapp", function() {
}); });
it("should notify the user on any other error", function() { it("should notify the user on any other error", function() {
sandbox.stub(notifications, "errorL10n");
client.requestCallInfo.callsArgWith(2, {errno: 104}); client.requestCallInfo.callsArgWith(2, {errno: 104});
conversation.setupOutgoingCall(); conversation.setupOutgoingCall();
sinon.assert.calledOnce(notifier.errorL10n); sinon.assert.calledOnce(notifications.errorL10n);
}); });
it("should call outgoing on the conversation model when details " + it("should call outgoing on the conversation model when details " +
@ -565,7 +565,7 @@ describe("loop.webapp", function() {
view = React.addons.TestUtils.renderIntoDocument( view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.StartConversationView({ loop.webapp.StartConversationView({
model: conversation, model: conversation,
notifier: notifier, notifications: notifications,
client: standaloneClientStub client: standaloneClientStub
}) })
); );
@ -657,7 +657,7 @@ describe("loop.webapp", function() {
view = React.addons.TestUtils.renderIntoDocument( view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.StartConversationView({ loop.webapp.StartConversationView({
model: conversation, model: conversation,
notifier: notifier, notifications: notifications,
client: {requestCallUrlInfo: requestCallUrlInfo} client: {requestCallUrlInfo: requestCallUrlInfo}
}) })
); );
@ -678,10 +678,11 @@ describe("loop.webapp", function() {
it("should trigger a notication when a session:error model event is " + it("should trigger a notication when a session:error model event is " +
" received", function() { " received", function() {
sandbox.stub(notifications, "errorL10n");
conversation.trigger("session:error", "tech error"); conversation.trigger("session:error", "tech error");
sinon.assert.calledOnce(notifier.errorL10n); sinon.assert.calledOnce(notifications.errorL10n);
sinon.assert.calledWithExactly(notifier.errorL10n, sinon.assert.calledWithExactly(notifications.errorL10n,
"unable_retrieve_call_info"); "unable_retrieve_call_info");
}); });
}); });
@ -714,7 +715,7 @@ describe("loop.webapp", function() {
view = React.addons.TestUtils.renderIntoDocument( view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.StartConversationView({ loop.webapp.StartConversationView({
model: conversation, model: conversation,
notifier: notifier, notifications: notifications,
client: {requestCallUrlInfo: requestCallUrlInfo} client: {requestCallUrlInfo: requestCallUrlInfo}
}) })
); );
@ -730,7 +731,7 @@ describe("loop.webapp", function() {
view = React.addons.TestUtils.renderIntoDocument( view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.StartConversationView({ loop.webapp.StartConversationView({
model: conversation, model: conversation,
notifier: notifier, notifications: notifications,
client: {requestCallUrlInfo: requestCallUrlInfo} client: {requestCallUrlInfo: requestCallUrlInfo}
}) })
); );

View File

@ -58,7 +58,9 @@
}); });
mockConversationModel.startSession = noop; mockConversationModel.startSession = noop;
var mockNotifier = {}; var notifications = new loop.shared.models.NotificationCollection();
var errNotifications = new loop.shared.models.NotificationCollection();
errNotifications.error("Error!");
var Example = React.createClass({displayName: 'Example', var Example = React.createClass({displayName: 'Example',
render: function() { render: function() {
@ -117,11 +119,14 @@
React.DOM.strong(null, "Note:"), " 332px wide." React.DOM.strong(null, "Note:"), " 332px wide."
), ),
Example({summary: "Call URL retrieved", dashed: "true", style: {width: "332px"}}, Example({summary: "Call URL retrieved", dashed: "true", style: {width: "332px"}},
PanelView({client: mockClient, notifier: mockNotifier, PanelView({client: mockClient, notifications: notifications,
callUrl: "http://invalid.example.url/"}) callUrl: "http://invalid.example.url/"})
), ),
Example({summary: "Pending call url retrieval", dashed: "true", style: {width: "332px"}}, Example({summary: "Pending call url retrieval", dashed: "true", style: {width: "332px"}},
PanelView({client: mockClient, notifier: mockNotifier}) PanelView({client: mockClient, notifications: notifications})
),
Example({summary: "Error Notification", dashed: "true", style: {width: "332px"}},
PanelView({client: mockClient, notifications: errNotifications})
) )
), ),
@ -192,7 +197,7 @@
React.DOM.div({className: "standalone"}, React.DOM.div({className: "standalone"},
StartConversationView({model: mockConversationModel, StartConversationView({model: mockConversationModel,
client: mockClient, client: mockClient,
notifier: mockNotifier, notifications: notifications,
showCallOptionsMenu: true}) showCallOptionsMenu: true})
) )
) )

View File

@ -58,7 +58,9 @@
}); });
mockConversationModel.startSession = noop; mockConversationModel.startSession = noop;
var mockNotifier = {}; var notifications = new loop.shared.models.NotificationCollection();
var errNotifications = new loop.shared.models.NotificationCollection();
errNotifications.error("Error!");
var Example = React.createClass({ var Example = React.createClass({
render: function() { render: function() {
@ -117,11 +119,14 @@
<strong>Note:</strong> 332px wide. <strong>Note:</strong> 332px wide.
</p> </p>
<Example summary="Call URL retrieved" dashed="true" style={{width: "332px"}}> <Example summary="Call URL retrieved" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifier={mockNotifier} <PanelView client={mockClient} notifications={notifications}
callUrl="http://invalid.example.url/" /> callUrl="http://invalid.example.url/" />
</Example> </Example>
<Example summary="Pending call url retrieval" dashed="true" style={{width: "332px"}}> <Example summary="Pending call url retrieval" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifier={mockNotifier} /> <PanelView client={mockClient} notifications={notifications} />
</Example>
<Example summary="Error Notification" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={errNotifications}/>
</Example> </Example>
</Section> </Section>
@ -192,7 +197,7 @@
<div className="standalone"> <div className="standalone">
<StartConversationView model={mockConversationModel} <StartConversationView model={mockConversationModel}
client={mockClient} client={mockClient}
notifier={mockNotifier} notifications={notifications}
showCallOptionsMenu={true} /> showCallOptionsMenu={true} />
</div> </div>
</Example> </Example>

View File

@ -77,20 +77,20 @@
<h1 class="titleText showPrivate">&aboutPrivateBrowsing.title;</h1> <h1 class="titleText showPrivate">&aboutPrivateBrowsing.title;</h1>
<h1 class="titleText showNormal">&aboutPrivateBrowsing.title.normal;</h1> <h1 class="titleText showNormal">&aboutPrivateBrowsing.title.normal;</h1>
<p class="showPrivate">&aboutPrivateBrowsing.subtitle;</p> <p class="subtitleText showPrivate">&aboutPrivateBrowsing.subtitle;</p>
<p class="showNormal">&aboutPrivateBrowsing.subtitle.normal;</p> <p class="subtitleText showNormal">&aboutPrivateBrowsing.subtitle.normal;</p>
<p>&aboutPrivateBrowsing.description;</p> <p class="descriptionText">&aboutPrivateBrowsing.description;</p>
<p class="showNormal">&aboutPrivateBrowsing.notPrivate;</p> <p class="notPrivateText showNormal">&aboutPrivateBrowsing.notPrivate;</p>
<button xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" <button xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
class="showNormal" class="openPrivate showNormal"
label="&privatebrowsingpage.openPrivateWindow.label;" label="&privatebrowsingpage.openPrivateWindow.label;"
accesskey="&privatebrowsingpage.openPrivateWindow.accesskey;" accesskey="&privatebrowsingpage.openPrivateWindow.accesskey;"
oncommand="openPrivateWindow();"/> oncommand="openPrivateWindow();"/>
<div class="showPrivate"> <div class="showPrivate">
<p>&aboutPrivateBrowsing.moreInfo;</p> <p class="moreInfoText">&aboutPrivateBrowsing.moreInfo;</p>
<p><a id="learnMore" target="_blank">&aboutPrivateBrowsing.learnMore;</a></p> <p><a id="learnMore" target="_blank">&aboutPrivateBrowsing.learnMore;</a></p>
</div> </div>
</div> </div>

View File

@ -4,19 +4,21 @@
// Tests devtools API // Tests devtools API
const Cu = Components.utils; const Cu = Components.utils;
const toolId = "test-tool"; const toolId1 = "test-tool-1";
const toolId2 = "test-tool-2";
let tempScope = {}; let tempScope = {};
Cu.import("resource://gre/modules/devtools/event-emitter.js", tempScope); Cu.import("resource://gre/modules/devtools/event-emitter.js", tempScope);
let EventEmitter = tempScope.EventEmitter; let EventEmitter = tempScope.EventEmitter;
function test() { function test() {
addTab("about:blank").then(runTests); addTab("about:blank").then(runTests1);
} }
function runTests(aTab) { // Test scenario 1: the tool definition build method returns a promise.
function runTests1(aTab) {
let toolDefinition = { let toolDefinition = {
id: toolId, id: toolId1,
isTargetSupported: function() true, isTargetSupported: function() true,
visibilityswitch: "devtools.test-tool.enabled", visibilityswitch: "devtools.test-tool.enabled",
url: "about:blank", url: "about:blank",
@ -28,40 +30,131 @@ function runTests(aTab) {
}; };
ok(gDevTools, "gDevTools exists"); ok(gDevTools, "gDevTools exists");
is(gDevTools.getToolDefinitionMap().has(toolId), false, is(gDevTools.getToolDefinitionMap().has(toolId1), false,
"The tool is not registered"); "The tool is not registered");
gDevTools.registerTool(toolDefinition); gDevTools.registerTool(toolDefinition);
is(gDevTools.getToolDefinitionMap().has(toolId), true, is(gDevTools.getToolDefinitionMap().has(toolId1), true,
"The tool is registered"); "The tool is registered");
let target = TargetFactory.forTab(gBrowser.selectedTab); let target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, toolId).then(function (toolbox) { let events = {};
// Check events on the gDevTools and toolbox objects.
gDevTools.once(toolId1 + "-init", (event, toolbox, iframe) => {
ok(iframe, "iframe argument available");
toolbox.once(toolId1 + "-init", (event, iframe) => {
ok(iframe, "iframe argument available");
events["init"] = true;
});
});
gDevTools.once(toolId1 + "-ready", (event, toolbox, panel) => {
ok(panel, "panel argument available");
toolbox.once(toolId1 + "-ready", (event, panel) => {
ok(panel, "panel argument available");
events["ready"] = true;
});
});
gDevTools.showToolbox(target, toolId1).then(function (toolbox) {
is(toolbox.target, target, "toolbox target is correct"); is(toolbox.target, target, "toolbox target is correct");
is(toolbox._host.hostTab, gBrowser.selectedTab, "toolbox host is correct"); is(toolbox._host.hostTab, gBrowser.selectedTab, "toolbox host is correct");
ok(events["init"], "init event fired");
ok(events["ready"], "ready event fired");
gDevTools.unregisterTool(toolId1);
runTests2();
});
}
// Test scenario 2: the tool definition build method returns panel instance.
function runTests2() {
let toolDefinition = {
id: toolId2,
isTargetSupported: function() true,
visibilityswitch: "devtools.test-tool.enabled",
url: "about:blank",
label: "someLabel",
build: function(iframeWindow, toolbox) {
return new DevToolPanel(iframeWindow, toolbox);
},
};
is(gDevTools.getToolDefinitionMap().has(toolId2), false,
"The tool is not registered");
gDevTools.registerTool(toolDefinition);
is(gDevTools.getToolDefinitionMap().has(toolId2), true,
"The tool is registered");
let target = TargetFactory.forTab(gBrowser.selectedTab);
let events = {};
// Check events on the gDevTools and toolbox objects.
gDevTools.once(toolId2 + "-init", (event, toolbox, iframe) => {
ok(iframe, "iframe argument available");
toolbox.once(toolId2 + "-init", (event, iframe) => {
ok(iframe, "iframe argument available");
events["init"] = true;
});
});
gDevTools.once(toolId2 + "-build", (event, toolbox, panel, iframe) => {
ok(panel, "panel argument available");
toolbox.once(toolId2 + "-build", (event, panel, iframe) => {
ok(panel, "panel argument available");
events["build"] = true;
});
});
gDevTools.once(toolId2 + "-ready", (event, toolbox, panel) => {
ok(panel, "panel argument available");
toolbox.once(toolId2 + "-ready", (event, panel) => {
ok(panel, "panel argument available");
events["ready"] = true;
});
});
gDevTools.showToolbox(target, toolId2).then(function (toolbox) {
is(toolbox.target, target, "toolbox target is correct");
is(toolbox._host.hostTab, gBrowser.selectedTab, "toolbox host is correct");
ok(events["init"], "init event fired");
ok(events["build"], "build event fired");
ok(events["ready"], "ready event fired");
continueTests(toolbox); continueTests(toolbox);
}); });
} }
function continueTests(toolbox, panel) { function continueTests(toolbox, panel) {
ok(toolbox.getCurrentPanel(), "panel value is correct"); ok(toolbox.getCurrentPanel(), "panel value is correct");
is(toolbox.currentToolId, toolId, "toolbox _currentToolId is correct"); is(toolbox.currentToolId, toolId2, "toolbox _currentToolId is correct");
ok(!toolbox.doc.getElementById("toolbox-tab-" + toolId).hasAttribute("icon-invertable"), ok(!toolbox.doc.getElementById("toolbox-tab-" + toolId2).hasAttribute("icon-invertable"),
"The tool tab does not have the invertable attribute"); "The tool tab does not have the invertable attribute");
ok(toolbox.doc.getElementById("toolbox-tab-inspector").hasAttribute("icon-invertable"), ok(toolbox.doc.getElementById("toolbox-tab-inspector").hasAttribute("icon-invertable"),
"The builtin tool tabs do have the invertable attribute"); "The builtin tool tabs do have the invertable attribute");
let toolDefinitions = gDevTools.getToolDefinitionMap(); let toolDefinitions = gDevTools.getToolDefinitionMap();
is(toolDefinitions.has(toolId), true, "The tool is in gDevTools"); is(toolDefinitions.has(toolId2), true, "The tool is in gDevTools");
let toolDefinition = toolDefinitions.get(toolId); let toolDefinition = toolDefinitions.get(toolId2);
is(toolDefinition.id, toolId, "toolDefinition id is correct"); is(toolDefinition.id, toolId2, "toolDefinition id is correct");
gDevTools.unregisterTool(toolId); gDevTools.unregisterTool(toolId2);
is(gDevTools.getToolDefinitionMap().has(toolId), false, is(gDevTools.getToolDefinitionMap().has(toolId2), false,
"The tool is no longer registered"); "The tool is no longer registered");
// Wait for unregisterTool to select the next tool before // Wait for unregisterTool to select the next tool before

View File

@ -1,46 +1,60 @@
/* Any copyright is dedicated to the Public Domain. /* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */ * http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that changing preferences in the options panel updates the prefs
// and toggles appropriate things in the toolbox.
let doc = null, toolbox = null, panelWin = null, modifiedPrefs = []; let doc = null, toolbox = null, panelWin = null, modifiedPrefs = [];
function test() { let test = asyncTest(function*() {
const URL = "data:text/html;charset=utf8,test for dynamically registering and unregistering tools"; const URL = "data:text/html;charset=utf8,test for dynamically registering and unregistering tools";
Task.spawn(function* () { registerNewTool();
let tab = yield addTab(URL); let tab = yield addTab(URL);
let target = TargetFactory.forTab(tab); let target = TargetFactory.forTab(tab);
let toolbox = yield gDevTools.showToolbox(target); toolbox = yield gDevTools.showToolbox(target);
yield testSelectTool(toolbox);
yield testOptionsShortcut();
yield testOptions();
yield testToggleTools();
}).then(cleanup, errorHandler);
}
function testSelectTool(aToolbox) {
let deferred = promise.defer();
toolbox = aToolbox;
doc = toolbox.doc; doc = toolbox.doc;
toolbox.once("options-selected", () => { yield testSelectTool();
ok(true, "Toolbox selected via selectTool method"); yield testOptionsShortcut();
deferred.resolve(); yield testOptions();
}); yield testToggleTools();
toolbox.selectTool("options"); yield cleanup();
});
return deferred.promise; function registerNewTool() {
let toolDefinition = {
id: "test-tool",
isTargetSupported: function() true,
visibilityswitch: "devtools.test-tool.enabled",
url: "about:blank",
label: "someLabel"
};
ok(gDevTools, "gDevTools exists");
ok(!gDevTools.getToolDefinitionMap().has("test-tool"),
"The tool is not registered");
gDevTools.registerTool(toolDefinition);
ok(gDevTools.getToolDefinitionMap().has("test-tool"),
"The tool is registered");
} }
function testOptionsShortcut() { function* testSelectTool() {
let deferred = promise.defer(); info ("Checking to make sure that the options panel can be selected.");
toolbox.selectTool("webconsole") let onceSelected = toolbox.once("options-selected");
toolbox.selectTool("options");
yield onceSelected;
ok(true, "Toolbox selected via selectTool method");
}
function* testOptionsShortcut() {
info ("Selecting another tool, then reselecting options panel with keyboard.");
yield toolbox.selectTool("webconsole")
.then(() => synthesizeKeyFromKeyTag("toolbox-options-key", doc)) .then(() => synthesizeKeyFromKeyTag("toolbox-options-key", doc))
.then(() => { .then(() => {
ok(true, "Toolbox selected via shortcut key"); ok(true, "Toolbox selected via shortcut key");
deferred.resolve();
}); });
return deferred.promise;
} }
function* testOptions() { function* testOptions() {
@ -103,7 +117,7 @@ function* testMenuList(menulist) {
} }
} }
function testMouseClick(node, prefValue) { function* testMouseClick(node, prefValue) {
let deferred = promise.defer(); let deferred = promise.defer();
let pref = node.getAttribute("data-pref"); let pref = node.getAttribute("data-pref");
@ -127,14 +141,17 @@ function testMouseClick(node, prefValue) {
EventUtils.synthesizeMouseAtCenter(node, {}, panelWin); EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
}); });
return deferred.promise; yield deferred.promise;
} }
function testToggleTools() { function* testToggleTools() {
let toolNodes = panelWin.document.querySelectorAll("#default-tools-box > checkbox:not([unsupported])"); let toolNodes = panelWin.document.querySelectorAll("#default-tools-box > checkbox:not([unsupported]), #additional-tools-box > checkbox:not([unsupported])");
let enabledTools = Array.prototype.filter.call(toolNodes, node => node.checked); let enabledTools = [...toolNodes].filter(node => node.checked);
let toggleableTools = gDevTools.getDefaultTools().filter(tool => {
return tool.visibilityswitch;
}).concat(gDevTools.getAdditionalTools());
let toggleableTools = gDevTools.getDefaultTools().filter(tool => tool.visibilityswitch);
for (let node of toolNodes) { for (let node of toolNodes) {
let id = node.getAttribute("id"); let id = node.getAttribute("id");
ok (toggleableTools.some(tool => tool.id === id), ok (toggleableTools.some(tool => tool.id === id),
@ -148,23 +165,22 @@ function testToggleTools() {
} }
// Toggle each tool // Toggle each tool
let p = promise.resolve();
for (let node of toolNodes) { for (let node of toolNodes) {
p = p.then(toggleTool.bind(null, node)); yield toggleTool(node);
} }
// Toggle again to reset tool enablement state // Toggle again to reset tool enablement state
for (let node of toolNodes) { for (let node of toolNodes) {
p = p.then(toggleTool.bind(null, node)); yield toggleTool(node);
} }
// Test that a tool can still be added when no tabs are present: // Test that a tool can still be added when no tabs are present:
// Disable all tools // Disable all tools
for (let node of enabledTools) { for (let node of enabledTools) {
p = p.then(toggleTool.bind(null, node)); yield toggleTool(node);
} }
// Re-enable the tools which are enabled by default // Re-enable the tools which are enabled by default
for (let node of enabledTools) { for (let node of enabledTools) {
p = p.then(toggleTool.bind(null, node)); yield toggleTool(node);
} }
// Toggle first, middle, and last tools to ensure that toolbox tabs are // Toggle first, middle, and last tools to ensure that toolbox tabs are
@ -173,20 +189,19 @@ function testToggleTools() {
middleTool = toolNodes[(toolNodes.length / 2) | 0], middleTool = toolNodes[(toolNodes.length / 2) | 0],
lastTool = toolNodes[toolNodes.length - 1]; lastTool = toolNodes[toolNodes.length - 1];
p = p.then(toggleTool.bind(null, firstTool)) yield toggleTool(firstTool);
.then(toggleTool.bind(null, firstTool)) yield toggleTool(firstTool);
.then(toggleTool.bind(null, middleTool)) yield toggleTool(middleTool);
.then(toggleTool.bind(null, middleTool)) yield toggleTool(middleTool);
.then(toggleTool.bind(null, lastTool)) yield toggleTool(lastTool);
.then(toggleTool.bind(null, lastTool)); yield toggleTool(lastTool);
return p;
} }
function toggleTool(node) { function* toggleTool(node) {
let deferred = promise.defer(); let deferred = promise.defer();
let toolId = node.getAttribute("id"); let toolId = node.getAttribute("id");
let onRegistrationChange;
if (node.checked) { if (node.checked) {
gDevTools.once("tool-unregistered", checkUnregistered.bind(null, toolId, deferred)); gDevTools.once("tool-unregistered", checkUnregistered.bind(null, toolId, deferred));
} else { } else {
@ -195,7 +210,7 @@ function toggleTool(node) {
node.scrollIntoView(); node.scrollIntoView();
EventUtils.synthesizeMouseAtCenter(node, {}, panelWin); EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
return deferred.promise; yield deferred.promise;
} }
function checkUnregistered(toolId, deferred, event, data) { function checkUnregistered(toolId, deferred, event, data) {
@ -249,32 +264,12 @@ function GetPref(name) {
} }
} }
function SetPref(name, value) { function* cleanup() {
let type = Services.prefs.getPrefType(name); gDevTools.unregisterTool("test-tool");
switch (type) { yield toolbox.destroy();
case Services.prefs.PREF_STRING: gBrowser.removeCurrentTab();
return Services.prefs.setCharPref(name, value); for (let pref of modifiedPrefs) {
case Services.prefs.PREF_INT: Services.prefs.clearUserPref(pref);
return Services.prefs.setIntPref(name, value);
case Services.prefs.PREF_BOOL:
return Services.prefs.setBoolPref(name, value);
default:
throw new Error("Unknown type");
} }
} toolbox = doc = panelWin = modifiedPrefs = null;
function cleanup() {
toolbox.destroy().then(function() {
gBrowser.removeCurrentTab();
for (let pref of modifiedPrefs) {
Services.prefs.clearUserPref(pref);
}
toolbox = doc = panelWin = modifiedPrefs = null;
finish();
});
}
function errorHandler(error) {
ok(false, "Unexpected error: " + error);
cleanup();
} }

View File

@ -168,7 +168,7 @@ OptionsPanel.prototype = {
let onCheckboxClick = (checkbox) => { let onCheckboxClick = (checkbox) => {
let toolDefinition = toggleableButtons.filter(tool => tool.id === checkbox.id)[0]; let toolDefinition = toggleableButtons.filter(tool => tool.id === checkbox.id)[0];
SetPref(toolDefinition.visibilityswitch, checkbox.checked); Services.prefs.setBoolPref(toolDefinition.visibilityswitch, checkbox.checked);
setToolboxButtonsVisibility(); setToolboxButtonsVisibility();
}; };
@ -198,7 +198,7 @@ OptionsPanel.prototype = {
let onCheckboxClick = function(id) { let onCheckboxClick = function(id) {
let toolDefinition = gDevTools._tools.get(id); let toolDefinition = gDevTools._tools.get(id);
// Set the kill switch pref boolean to true // Set the kill switch pref boolean to true
SetPref(toolDefinition.visibilityswitch, this.checked); Services.prefs.setBoolPref(toolDefinition.visibilityswitch, this.checked);
if (this.checked) { if (this.checked) {
gDevTools.emit("tool-registered", id); gDevTools.emit("tool-registered", id);
} }

View File

@ -58,7 +58,8 @@ const ToolboxButtons = [
!target.isAddon && target.activeTab && target.activeTab.traits.frames !target.isAddon && target.activeTab && target.activeTab.traits.frames
) )
}, },
{ id: "command-button-splitconsole" }, { id: "command-button-splitconsole",
isTargetSupported: target => !target.isAddon },
{ id: "command-button-responsive" }, { id: "command-button-responsive" },
{ id: "command-button-paintflashing" }, { id: "command-button-paintflashing" },
{ id: "command-button-tilt" }, { id: "command-button-tilt" },
@ -619,11 +620,6 @@ Toolbox.prototype = {
this._buildPickerButton(); this._buildPickerButton();
} }
if (!this.target.isLocalTab) {
this.setToolboxButtonsVisibility();
return Promise.resolve();
}
let spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec"); let spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec");
let environment = CommandUtils.createEnvironment(this, '_target'); let environment = CommandUtils.createEnvironment(this, '_target');
return CommandUtils.createRequisition(environment).then(requisition => { return CommandUtils.createRequisition(environment).then(requisition => {
@ -631,7 +627,11 @@ Toolbox.prototype = {
return CommandUtils.createButtons(spec, this.target, this.doc, return CommandUtils.createButtons(spec, this.target, this.doc,
requisition).then(buttons => { requisition).then(buttons => {
let container = this.doc.getElementById("toolbox-buttons"); let container = this.doc.getElementById("toolbox-buttons");
buttons.forEach(container.appendChild.bind(container)); buttons.forEach(button=> {
if (button) {
container.appendChild(button);
}
});
this.setToolboxButtonsVisibility(); this.setToolboxButtonsVisibility();
}); });
}); });
@ -875,18 +875,49 @@ Toolbox.prototype = {
iframe.tooltip = "aHTMLTooltip"; iframe.tooltip = "aHTMLTooltip";
iframe.style.visibility = "hidden"; iframe.style.visibility = "hidden";
let vbox = this.doc.getElementById("toolbox-panel-" + id); gDevTools.emit(id + "-init", this, iframe);
vbox.appendChild(iframe); this.emit(id + "-init", iframe);
// If no parent yet, append the frame into default location.
if (!iframe.parentNode) {
let vbox = this.doc.getElementById("toolbox-panel-" + id);
vbox.appendChild(iframe);
}
let onLoad = () => { let onLoad = () => {
// Prevent flicker while loading by waiting to make visible until now. // Prevent flicker while loading by waiting to make visible until now.
iframe.style.visibility = "visible"; iframe.style.visibility = "visible";
// The build method should return a panel instance, so events can
// be fired with the panel as an argument. However, in order to keep
// backward compatibility with existing extensions do a check
// for a promise return value.
let built = definition.build(iframe.contentWindow, this); let built = definition.build(iframe.contentWindow, this);
if (!(built instanceof Promise)) {
let panel = built;
iframe.panel = panel;
gDevTools.emit(id + "-build", this, panel);
this.emit(id + "-build", panel);
// The panel can implement an 'open' method for asynchronous
// initialization sequence.
if (typeof panel.open == "function") {
built = panel.open();
} else {
let deferred = promise.defer();
deferred.resolve(panel);
built = deferred.promise;
}
}
// Wait till the panel is fully ready and fire 'ready' events.
promise.resolve(built).then((panel) => { promise.resolve(built).then((panel) => {
this._toolPanels.set(id, panel); this._toolPanels.set(id, panel);
this.emit(id + "-ready", panel);
gDevTools.emit(id + "-ready", this, panel); gDevTools.emit(id + "-ready", this, panel);
this.emit(id + "-ready", panel);
deferred.resolve(panel); deferred.resolve(panel);
}, console.error); }, console.error);
}; };
@ -1514,7 +1545,7 @@ Toolbox.prototype = {
// Remove the host UI // Remove the host UI
outstanding.push(this.destroyHost()); outstanding.push(this.destroyHost());
if (this.target.isLocalTab) { if (this._requisition) {
this._requisition.destroy(); this._requisition.destroy();
} }
this._telemetry.toolClosed("toolbox"); this._telemetry.toolClosed("toolbox");

View File

@ -82,8 +82,7 @@ Tools.options = {
return true; return true;
}, },
build: function(iframeWindow, toolbox) { build: function(iframeWindow, toolbox) {
let panel = new OptionsPanel(iframeWindow, toolbox); return new OptionsPanel(iframeWindow, toolbox);
return panel.open();
} }
} }
@ -115,8 +114,7 @@ Tools.webConsole = {
return true; return true;
}, },
build: function(iframeWindow, toolbox) { build: function(iframeWindow, toolbox) {
let panel = new WebConsolePanel(iframeWindow, toolbox); return new WebConsolePanel(iframeWindow, toolbox);
return panel.open();
} }
}; };
@ -149,8 +147,7 @@ Tools.inspector = {
}, },
build: function(iframeWindow, toolbox) { build: function(iframeWindow, toolbox) {
let panel = new InspectorPanel(iframeWindow, toolbox); return new InspectorPanel(iframeWindow, toolbox);
return panel.open();
} }
}; };
@ -175,8 +172,7 @@ Tools.jsdebugger = {
}, },
build: function(iframeWindow, toolbox) { build: function(iframeWindow, toolbox) {
let panel = new DebuggerPanel(iframeWindow, toolbox); return new DebuggerPanel(iframeWindow, toolbox);
return panel.open();
} }
}; };
@ -200,8 +196,7 @@ Tools.styleEditor = {
}, },
build: function(iframeWindow, toolbox) { build: function(iframeWindow, toolbox) {
let panel = new StyleEditorPanel(iframeWindow, toolbox); return new StyleEditorPanel(iframeWindow, toolbox);
return panel.open();
} }
}; };
@ -221,8 +216,7 @@ Tools.shaderEditor = {
}, },
build: function(iframeWindow, toolbox) { build: function(iframeWindow, toolbox) {
let panel = new ShaderEditorPanel(iframeWindow, toolbox); return new ShaderEditorPanel(iframeWindow, toolbox);
return panel.open();
} }
}; };
@ -242,8 +236,7 @@ Tools.canvasDebugger = {
return !target.isAddon && !target.chrome; return !target.isAddon && !target.chrome;
}, },
build: function (iframeWindow, toolbox) { build: function (iframeWindow, toolbox) {
let panel = new CanvasDebuggerPanel(iframeWindow, toolbox); return new CanvasDebuggerPanel(iframeWindow, toolbox);
return panel.open();
} }
}; };
@ -261,8 +254,7 @@ Tools.webAudioEditor = {
return !target.isAddon; return !target.isAddon;
}, },
build: function(iframeWindow, toolbox) { build: function(iframeWindow, toolbox) {
let panel = new WebAudioEditorPanel(iframeWindow, toolbox); return new WebAudioEditorPanel(iframeWindow, toolbox);
return panel.open();
} }
}; };
@ -288,8 +280,7 @@ Tools.jsprofiler = {
}, },
build: function (frame, target) { build: function (frame, target) {
let panel = new ProfilerPanel(frame, target); return new ProfilerPanel(frame, target);
return panel.open();
} }
}; };
@ -314,8 +305,7 @@ Tools.netMonitor = {
}, },
build: function(iframeWindow, toolbox) { build: function(iframeWindow, toolbox) {
let panel = new NetMonitorPanel(iframeWindow, toolbox); return new NetMonitorPanel(iframeWindow, toolbox);
return panel.open();
} }
}; };
@ -341,8 +331,7 @@ Tools.storage = {
}, },
build: function(iframeWindow, toolbox) { build: function(iframeWindow, toolbox) {
let panel = new StoragePanel(iframeWindow, toolbox); return new StoragePanel(iframeWindow, toolbox);
return panel.open();
} }
}; };
@ -364,8 +353,7 @@ Tools.scratchpad = {
}, },
build: function(iframeWindow, toolbox) { build: function(iframeWindow, toolbox) {
let panel = new ScratchpadPanel(iframeWindow, toolbox); return new ScratchpadPanel(iframeWindow, toolbox);
return panel.open();
} }
}; };

View File

@ -2187,6 +2187,7 @@ function truncateString(str, maxLength) {
"…" + "…" +
str.substring(str.length - Math.floor(maxLength / 2)); str.substring(str.length - Math.floor(maxLength / 2));
} }
/** /**
* Parse attribute names and values from a string. * Parse attribute names and values from a string.
* *
@ -2200,49 +2201,16 @@ function truncateString(str, maxLength) {
function parseAttributeValues(attr, doc) { function parseAttributeValues(attr, doc) {
attr = attr.trim(); attr = attr.trim();
// Prepare other versions of the string to be parsed by appending a " or ' // Handle bad user inputs by appending a " or ' if it fails to parse without
// and using those if the first one fails to parse without these characters // them. Also note that a SVG tag is used to make sure the HTML parser
let stringsToParse = [ // preserves mixed-case attributes
"<div " + attr + "></div>", let el = DOMParser.parseFromString("<svg " + attr + "></svg>", "text/html").body.childNodes[0] ||
"<div " + attr + "\"></div>", DOMParser.parseFromString("<svg " + attr + "\"></svg>", "text/html").body.childNodes[0] ||
"<div " + attr + "'></div>" DOMParser.parseFromString("<svg " + attr + "'></svg>", "text/html").body.childNodes[0];
];
// Try to parse as XML, this way, if the string is wellformed, this will
// preserve the case.
let parsedAttributes = [];
for (let str of stringsToParse) {
let parsed = DOMParser.parseFromString(str, "text/xml");
// The XML parser generates a valid XML document even when parsing errors
// occur, in which case the document contains a <parsererror> node, so check
// that the document contains our expected DIV
if (parsed.childNodes[0].localName === "div") {
for (let {name, value} of parsed.childNodes[0].attributes) {
parsedAttributes.push({ name, value });
}
break;
}
}
// If the XML parsing failed, parse as HTML to get malformed attributes
if (parsedAttributes.length === 0) {
for (let str of stringsToParse) {
let parsed = DOMParser.parseFromString(str, "text/html");
// Check that the parsed document does contain the expected DIV as a child
// of <body>
if (parsed.body.childNodes[0]) {
for (let {name, value} of parsed.body.childNodes[0].attributes) {
parsedAttributes.push({ name, value });
}
break;
}
}
}
let div = doc.createElement("div"); let div = doc.createElement("div");
let attributes = []; let attributes = [];
for (let {name, value} of parsedAttributes) { for (let {name, value} of el.attributes) {
// Try to set on an element in the document, throws exception on bad input. // Try to set on an element in the document, throws exception on bad input.
// Prevents InvalidCharacterError - "String contains an invalid character". // Prevents InvalidCharacterError - "String contains an invalid character".
try { try {

View File

@ -45,11 +45,8 @@ function* testWellformedMixedCase(inspector) {
} }
function* testMalformedMixedCase(inspector) { function* testMalformedMixedCase(inspector) {
info("Modifying a mixed-case attribute, making sure to generate a parsing" + info("Modifying a malformed, mixed-case attribute, " +
"error, and expecting the attribute's case to NOT be preserved"); "expecting the attribute's case to be preserved");
// See /browser/devtools/markupview/markup-view.js:parseAttributeValues
// When attributes are malformed, they cannot be parsed with the XML parser
// and so we fall back to the HTML parser which lowercases attributes.
info("Listening to markup mutations"); info("Listening to markup mutations");
let onMutated = inspector.once("markupmutation"); let onMutated = inspector.once("markupmutation");
@ -67,7 +64,7 @@ function* testMalformedMixedCase(inspector) {
yield onMutated; yield onMutated;
assertAttributes("svg", { assertAttributes("svg", {
"viewbox": "<>", "viewBox": "<>",
"width": "200", "width": "200",
"height": "200" "height": "200"
}); });

View File

@ -1212,9 +1212,6 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
this.updateMenuView(template, 'method', aMethod); this.updateMenuView(template, 'method', aMethod);
this.updateMenuView(template, 'url', aUrl); this.updateMenuView(template, 'url', aUrl);
let waterfall = $(".requests-menu-waterfall", template);
waterfall.style.backgroundImage = this._cachedWaterfallBackground;
// Flatten the DOM by removing one redundant box (the template container). // Flatten the DOM by removing one redundant box (the template container).
for (let node of template.childNodes) { for (let node of template.childNodes) {
fragment.appendChild(node.cloneNode(true)); fragment.appendChild(node.cloneNode(true));
@ -1378,7 +1375,6 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
// Redraw and set the canvas background for each waterfall view. // Redraw and set the canvas background for each waterfall view.
this._showWaterfallDivisionLabels(scale); this._showWaterfallDivisionLabels(scale);
this._drawWaterfallBackground(scale); this._drawWaterfallBackground(scale);
this._flushWaterfallBackgrounds();
// Apply CSS transforms to each waterfall in this container totalTime // Apply CSS transforms to each waterfall in this container totalTime
// accurately translate and resize as needed. // accurately translate and resize as needed.
@ -1497,8 +1493,8 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
let pixelArray = imageData.data; let pixelArray = imageData.data;
let buf = new ArrayBuffer(pixelArray.length); let buf = new ArrayBuffer(pixelArray.length);
let buf8 = new Uint8ClampedArray(buf); let view8bit = new Uint8ClampedArray(buf);
let data32 = new Uint32Array(buf); let view32bit = new Uint32Array(buf);
// Build new millisecond tick lines... // Build new millisecond tick lines...
let timingStep = REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE; let timingStep = REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE;
@ -1520,26 +1516,16 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
let increment = scaledStep * Math.pow(2, i); let increment = scaledStep * Math.pow(2, i);
for (let x = 0; x < canvasWidth; x += increment) { for (let x = 0; x < canvasWidth; x += increment) {
let position = (window.isRTL ? canvasWidth - x : x) | 0; let position = (window.isRTL ? canvasWidth - x : x) | 0;
data32[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r; view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
} }
alphaComponent += REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD; alphaComponent += REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
} }
} }
// Flush the image data and cache the waterfall background. // Flush the image data and cache the waterfall background.
pixelArray.set(buf8); pixelArray.set(view8bit);
ctx.putImageData(imageData, 0, 0); ctx.putImageData(imageData, 0, 0);
this._cachedWaterfallBackground = "url(" + canvas.toDataURL() + ")"; document.mozSetImageElement("waterfall-background", canvas);
},
/**
* Reapplies the current waterfall background on all request items.
*/
_flushWaterfallBackgrounds: function() {
for (let { target } of this) {
let waterfallNode = $(".requests-menu-waterfall", target);
waterfallNode.style.backgroundImage = this._cachedWaterfallBackground;
}
}, },
/** /**
@ -1762,7 +1748,6 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
_canvas: null, _canvas: null,
_ctx: null, _ctx: null,
_cachedWaterfallWidth: 0, _cachedWaterfallWidth: 0,
_cachedWaterfallBackground: "",
_firstRequestStartedMillis: -1, _firstRequestStartedMillis: -1,
_lastRequestEndedMillis: -1, _lastRequestEndedMillis: -1,
_updateQueue: [], _updateQueue: [],

View File

@ -1,4 +1,5 @@
[DEFAULT] [DEFAULT]
skip-if = e10s # Bug 1058898
subsuite = devtools subsuite = devtools
support-files = support-files =
doc_simple-test.html doc_simple-test.html

View File

@ -104,6 +104,13 @@ let CommandUtils = {
if (command == null) { if (command == null) {
throw new Error("No command '" + typed + "'"); throw new Error("No command '" + typed + "'");
} }
// Do not build a button for a non-remote safe command in a non-local target.
if (!target.isLocalTab && !command.isRemoteSafe) {
requisition.clear();
return;
}
if (command.buttonId != null) { if (command.buttonId != null) {
button.id = command.buttonId; button.id = command.buttonId;
if (command.buttonClass != null) { if (command.buttonClass != null) {

View File

@ -14,6 +14,7 @@ exports.items = [
buttonId: "command-button-splitconsole", buttonId: "command-button-splitconsole",
buttonClass: "command-button command-button-invertable", buttonClass: "command-button command-button-invertable",
tooltipText: gcli.lookup("splitconsoleTooltip"), tooltipText: gcli.lookup("splitconsoleTooltip"),
isRemoteSafe: true,
state: { state: {
isChecked: function(target) { isChecked: function(target) {
let toolbox = gDevTools.getToolbox(target); let toolbox = gDevTools.getToolbox(target);

View File

@ -2,8 +2,8 @@
subsuite = devtools subsuite = devtools
support-files = support-files =
addons/simulators.json addons/simulators.json
doc_tabs.html
head.js head.js
templates.json templates.json
[browser_tabs.js] [browser_tabs.js]
skip-if = true # Fails on TBPL, to be fixed in bug 1062611

View File

@ -2,6 +2,8 @@
http://creativecommons.org/publicdomain/zero/1.0/ */ http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict"; "use strict";
const TEST_URI = "http://example.com/browser/browser/devtools/webide/test/doc_tabs.html";
function test() { function test() {
waitForExplicitFinish(); waitForExplicitFinish();
SimpleTest.requestCompleteLog(); SimpleTest.requestCompleteLog();
@ -9,10 +11,14 @@ function test() {
Task.spawn(function() { Task.spawn(function() {
const { DebuggerServer } = const { DebuggerServer } =
Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {}); Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
// Since we test the connections set below, destroy the server in case it
// was left open.
DebuggerServer.destroy();
DebuggerServer.init(function () { return true; }); DebuggerServer.init(function () { return true; });
DebuggerServer.addBrowserActors(); DebuggerServer.addBrowserActors();
let tab = yield addTab("about:newtab"); let tab = yield addTab(TEST_URI);
let win = yield openWebIDE(); let win = yield openWebIDE();
@ -23,15 +29,13 @@ function test() {
yield selectTabProject(win); yield selectTabProject(win);
let project = win.AppManager.selectedProject; let project = win.AppManager.selectedProject;
is(project.location, "about:newtab", "Location is correct"); is(project.location, TEST_URI, "Location is correct");
is(project.name, "New Tab", "Name is correct"); is(project.name, "example.com: Test Tab", "Name is correct");
yield closeWebIDE(win); yield closeWebIDE(win);
DebuggerServer.destroy(); DebuggerServer.destroy();
yield removeTab(tab); yield removeTab(tab);
}).then(finish, handleError);
finish();
});
} }
function connectToLocal(win) { function connectToLocal(win) {

View File

@ -0,0 +1,15 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Test Tab</title>
</head>
<body>
Test Tab
</body>
</html>

View File

@ -153,3 +153,8 @@ function removeTab(aTab, aWindow) {
targetBrowser.removeTab(aTab); targetBrowser.removeTab(aTab);
return deferred.promise; return deferred.promise;
} }
function handleError(aError) {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
finish();
}

View File

@ -67,8 +67,7 @@ window.busy-determined #action-busy-undetermined {
#project-panel-button { #project-panel-button {
-moz-box-pack: start; -moz-box-pack: start;
width: 150px; max-width: calc(50vw - 100px);
max-width: 150px;
} }
#project-panel-button > .panel-button-image { #project-panel-button > .panel-button-image {
@ -88,8 +87,7 @@ window.busy-determined #action-busy-undetermined {
} }
#project-panel-button > .panel-button-label { #project-panel-button > .panel-button-label {
width: 150px; -moz-box-flex: 1;
max-width: 150px;
} }
/* Panel buttons - runtime */ /* Panel buttons - runtime */

View File

@ -71,6 +71,17 @@ object with the following properties:
- ``move`` counts the number of times an item is moved somewhere else (but not to the palette); - ``move`` counts the number of times an item is moved somewhere else (but not to the palette);
- ``remove`` counts the number of times an item is removed to the palette; - ``remove`` counts the number of times an item is removed to the palette;
- ``reset`` counts the number of times the 'restore defaults' button is used; - ``reset`` counts the number of times the 'restore defaults' button is used;
- ``search`` is an object tracking searches of various types, keyed off the search
location, storing a number indicating how often the respective type of search
has happened.
- There are also two special keys that mean slightly different things.
- ``urlbar-keyword`` records searches that would have been an invalid-protocol
error, but are now keyword searches. They are also counted in the ``urlbar``
keyword (along with all the other urlbar searches).
- ``selection`` searches records selections of search suggestions. They include
the source, the index of the selection, and the kind of selection (mouse or
enter key). Selection searches are also counted in their sources.
``UITour`` ``UITour``

View File

@ -66,6 +66,33 @@
<!ENTITY cmd.clearList.accesskey "a"> <!ENTITY cmd.clearList.accesskey "a">
<!ENTITY cmd.clearDownloads.label "Clear Downloads"> <!ENTITY cmd.clearDownloads.label "Clear Downloads">
<!ENTITY cmd.clearDownloads.accesskey "D"> <!ENTITY cmd.clearDownloads.accesskey "D">
<!-- LOCALIZATION NOTE (cmd.unblock.label):
This command may be shown in the context menu, as a menu button item, or as
a text link when malware or potentially unwanted downloads are blocked.
Note: This string doesn't exist in the UI yet. See bug 1053890.
-->
<!ENTITY cmd.unblock.label "Unblock">
<!ENTITY cmd.unblock.accesskey "U">
<!-- LOCALIZATION NOTE (cmd.removeFile.label):
This command may be shown in the context menu or as a menu button label
when malware or potentially unwanted downloads are blocked.
Note: This string doesn't exist in the UI yet. See bug 1053890.
-->
<!ENTITY cmd.removeFile.label "Remove File">
<!ENTITY cmd.removeFile.accesskey "m">
<!-- LOCALIZATION NOTE (blocked.label):
Shown as a tag before the file name for some types of blocked downloads.
Note: This string doesn't exist in the UI yet. See bug 1053890.
-->
<!ENTITY blocked.label "BLOCKED">
<!-- LOCALIZATION NOTE (learnMore.label):
Shown as a text link for some types of blocked downloads, for example
malware, when there is an associated explanation page on the Mozilla site.
Note: This string doesn't exist in the UI yet. See bug 1053890.
-->
<!ENTITY learnMore.label "Learn More">
<!-- LOCALIZATION NOTE (downloadsHistory.label, downloadsHistory.accesskey): <!-- LOCALIZATION NOTE (downloadsHistory.label, downloadsHistory.accesskey):
This string is shown at the bottom of the Downloads Panel when all the This string is shown at the bottom of the Downloads Panel when all the

View File

@ -38,13 +38,40 @@ stateBlockedPolicy=Blocked by your security zone policy
# Indicates that the download was blocked after scanning. # Indicates that the download was blocked after scanning.
stateDirty=Blocked: May contain a virus or spyware stateDirty=Blocked: May contain a virus or spyware
# LOCALIZATION NOTE (blockedMalware, blockedPotentiallyUnwanted,
# blockedUncommon):
# These strings are shown in the panel for some types of blocked downloads, and
# are immediately followed by the "Learn More" link, thus they must end with a
# period. You may need to adjust "downloadDetails.width" in "downloads.dtd" if
# this turns out to be longer than the other existing status strings.
# Note: These strings don't exist in the UI yet. See bug 1053890.
blockedMalware=This file contains a virus or malware.
blockedPotentiallyUnwanted=This file may harm your computer.
blockedUncommon=This file may not be safe to open.
# LOCALIZATION NOTE (unblockHeader, unblockTypeMalware,
# unblockTypePotentiallyUnwanted, unblockTypeUncommon,
# unblockTip, unblockButtonContinue, unblockButtonCancel):
# These strings are displayed in the dialog shown when the user asks a blocked
# download to be unblocked. The severity of the threat is expressed in
# descending order by the unblockType strings, it is higher for files detected
# as malware and lower for uncommon downloads.
# Note: These strings don't exist in the UI yet. See bug 1053890.
unblockHeader=Are you sure you want to unblock this file?
unblockTypeMalware=This file contains a virus or other malware that will harm your computer.
unblockTypePotentiallyUnwanted=This file, disguised as a helpful download, will make unexpected changes to your programs and settings.
unblockTypeUncommon=This file has been downloaded from an unfamiliar and potentially dangerous website and may not be safe to open.
unblockTip=You can search for an alternate download source or try to download the file again later.
unblockButtonContinue=Unblock anyway
unblockButtonCancel=Keep me safe
# LOCALIZATION NOTE (sizeWithUnits): # LOCALIZATION NOTE (sizeWithUnits):
# %1$S is replaced with the size number, and %2$S with the measurement unit. # %1$S is replaced with the size number, and %2$S with the measurement unit.
sizeWithUnits=%1$S %2$S sizeWithUnits=%1$S %2$S
sizeUnknown=Unknown size sizeUnknown=Unknown size
# LOCALIZATION NOTE (shortTimeLeftSeconds, shortTimeLeftMinutes, # LOCALIZATION NOTE (shortTimeLeftSeconds, shortTimeLeftMinutes,
# shortTimeLeftHours, shortTimeLeftDays): # shortTimeLeftHours, shortTimeLeftDays):
# These values are displayed in the downloads indicator in the main browser # These values are displayed in the downloads indicator in the main browser
# window, where space is available for three characters maximum. %1$S is # window, where space is available for three characters maximum. %1$S is
# replaced with the time left for the given measurement unit. Even for days, # replaced with the time left for the given measurement unit. Even for days,

View File

@ -191,7 +191,7 @@ let AboutHome = {
let engine = Services.search.currentEngine; let engine = Services.search.currentEngine;
#ifdef MOZ_SERVICES_HEALTHREPORT #ifdef MOZ_SERVICES_HEALTHREPORT
window.BrowserSearch.recordSearchInHealthReport(engine, "abouthome"); window.BrowserSearch.recordSearchInHealthReport(engine, "abouthome", data.selection);
#endif #endif
// Trigger a search through nsISearchEngine.getSubmission() // Trigger a search through nsISearchEngine.getSubmission()
let submission = engine.getSubmission(data.searchTerms, null, "homepage"); let submission = engine.getSubmission(data.searchTerms, null, "homepage");

View File

@ -179,6 +179,7 @@ this.BrowserUITelemetry = {
Services.obs.addObserver(this, "sessionstore-windows-restored", false); Services.obs.addObserver(this, "sessionstore-windows-restored", false);
Services.obs.addObserver(this, "browser-delayed-startup-finished", false); Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
Services.obs.addObserver(this, "autocomplete-did-enter-text", false);
CustomizableUI.addListener(this); CustomizableUI.addListener(this);
}, },
@ -190,6 +191,13 @@ this.BrowserUITelemetry = {
case "browser-delayed-startup-finished": case "browser-delayed-startup-finished":
this._registerWindow(aSubject); this._registerWindow(aSubject);
break; break;
case "autocomplete-did-enter-text":
let input = aSubject.QueryInterface(Ci.nsIAutoCompleteInput);
if (input && input.id == "urlbar" && !input.inPrivateContext &&
input.popup.selectedIndex != -1) {
this._logAwesomeBarSearchResult(input.textValue);
}
break;
} }
}, },
@ -554,11 +562,25 @@ this.BrowserUITelemetry = {
this._countEvent(["customize", aEventType]); this._countEvent(["customize", aEventType]);
}, },
countSearchEvent: function(source, query) { countSearchEvent: function(source, query, selection) {
this._countEvent(["search", source]); this._countEvent(["search", source]);
if ((/^[a-zA-Z]+:[^\/\\]/).test(query)) { if ((/^[a-zA-Z]+:[^\/\\]/).test(query)) {
this._countEvent(["search", "urlbar-keyword"]); this._countEvent(["search", "urlbar-keyword"]);
} }
if (selection) {
this._countEvent(["search", "selection", source, selection.index, selection.kind]);
}
},
_logAwesomeBarSearchResult: function (url) {
let spec = Services.search.parseSubmissionURL(url);
if (spec.engine) {
let matchedEngine = "default";
if (spec.engine.name !== Services.search.currentEngine.name) {
matchedEngine = "other";
}
this.countSearchEvent("autocomplete-" + matchedEngine);
}
}, },
_durations: { _durations: {

View File

@ -206,7 +206,7 @@ this.ContentSearch = {
]); ]);
let browserWin = msg.target.ownerDocument.defaultView; let browserWin = msg.target.ownerDocument.defaultView;
let engine = Services.search.getEngineByName(data.engineName); let engine = Services.search.getEngineByName(data.engineName);
browserWin.BrowserSearch.recordSearchInHealthReport(engine, data.whence); browserWin.BrowserSearch.recordSearchInHealthReport(engine, data.whence, data.selection);
let submission = engine.getSubmission(data.searchString, "", data.whence); let submission = engine.getSubmission(data.searchString, "", data.whence);
browserWin.loadURI(submission.uri.spec, null, submission.postData); browserWin.loadURI(submission.uri.spec, null, submission.postData);
return Promise.resolve(); return Promise.resolve();

View File

@ -320,7 +320,10 @@ label.requests-menu-status-code {
.requests-menu-subitem.requests-menu-waterfall { .requests-menu-subitem.requests-menu-waterfall {
-moz-padding-start: 0px; -moz-padding-start: 0px;
-moz-padding-end: 4px; -moz-padding-end: 4px;
background-repeat: repeat-y; /* Background created on a <canvas> in js. */ /* Background created on a <canvas> in js. */
/* @see browser/devtools/netmonitor/netmonitor-view.js */
background-image: -moz-element(#waterfall-background);
background-repeat: repeat-y;
background-position: -1px center; background-position: -1px center;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 615 B

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 641 B

After

Width:  |  Height:  |  Size: 590 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 B

After

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 609 B

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 B

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 798 B

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 800 B

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 791 B

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 776 B

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 767 B

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 B

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 B

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 B

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 B

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 B

After

Width:  |  Height:  |  Size: 115 B

Some files were not shown because too many files have changed in this diff Show More