Merge m-c to inbound a=merge on a CLOSED TREE

This commit is contained in:
Wes Kocher 2014-06-24 18:39:27 -07:00
commit 934a0b53a3
93 changed files with 1963 additions and 1068 deletions

View File

@ -233,7 +233,6 @@ function startup(data, reasonCode) {
// Arguments related to test runner.
modules: {
'@test/options': {
allTestModules: options.allTestModules,
iterations: options.iterations,
filter: options.filter,
profileMemory: options.profileMemory,

View File

@ -54,7 +54,6 @@ exports.install = function install(xpiPath) {
setTimeout(resolve, 0, aAddon.id);
},
onInstallFailed: function (aInstall) {
console.log("failed");
aInstall.removeListener(listener);
reject(aInstall.error);
},
@ -114,8 +113,9 @@ exports.isActive = function isActive(addonId) {
return getAddon(addonId).then(addon => addon.isActive && !addon.appDisabled);
};
function getAddon (id) {
const getAddon = function getAddon (id) {
let { promise, resolve, reject } = defer();
AddonManager.getAddonByID(id, addon => addon ? resolve(addon) : reject());
return promise;
}
exports.getAddon = getAddon;

View File

@ -56,8 +56,6 @@ eventTarget.addEventListener("DOMContentLoaded", function handler(event) {
resolve();
}, false);
exports.ready = promise;
exports.window = window;

View File

@ -1,7 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
@ -10,9 +9,109 @@ module.metadata = {
const file = require("../io/file");
const memory = require('./memory');
const suites = require('@test/options').allTestModules;
const { Loader } = require("sdk/test/loader");
const cuddlefish = require("sdk/loader/cuddlefish");
const { Loader } = require("../test/loader");
const cuddlefish = require("../loader/cuddlefish");
const { defer, resolve } = require("../core/promise");
const { getAddon } = require("../addon/installer");
const { id } = require("sdk/self");
const { newURI } = require('sdk/url/utils');
const { getZipReader } = require("../zip/utils");
const { Cc, Ci, Cu } = require("chrome");
const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
var ios = Cc['@mozilla.org/network/io-service;1']
.getService(Ci.nsIIOService);
const TEST_REGEX = /(([^\/]+\/)(?:lib\/)?)(tests?\/test-[^\.\/]+)\.js$/;
const { mapcat, map, filter, fromEnumerator } = require("sdk/util/sequence");
const toFile = x => x.QueryInterface(Ci.nsIFile);
const isTestFile = ({leafName}) => leafName.substr(0, 5) == "test-" && leafName.substr(-3, 3) == ".js";
const getFileURI = x => ios.newFileURI(x).spec;
const getDirectoryEntries = file => map(toFile, fromEnumerator(_ => file.directoryEntries));
const getTestFiles = directory => filter(isTestFile, getDirectoryEntries(directory));
const getTestURIs = directory => map(getFileURI, getTestFiles(directory));
const isDirectory = x => x.isDirectory();
const getTestEntries = directory => mapcat(entry =>
/^tests?$/.test(entry.leafName) ? getTestURIs(entry) : getTestEntries(entry),
filter(isDirectory, getDirectoryEntries(directory)));
const removeDups = (array) => array.reduce((result, value) => {
if (value != result[result.length - 1]) {
result.push(value);
}
return result;
}, []);
const getSuites = function getSuites({ id }) {
return getAddon(id).then(addon => {
let fileURI = addon.getResourceURI("tests/");
let isPacked = fileURI.scheme == "jar";
let xpiURI = addon.getResourceURI();
let file = xpiURI.QueryInterface(Ci.nsIFileURL).file;
let suites = [];
let addEntry = (entry) => {
if (TEST_REGEX.test(entry)) {
let suite = RegExp.$2 + RegExp.$3;
suites.push(suite);
}
}
if (isPacked) {
return getZipReader(file).then(zip => {
let entries = zip.findEntries(null);
while (entries.hasMore()) {
let entry = entries.getNext();
addEntry(entry);
}
zip.close();
// sort and remove dups
suites = removeDups(suites.sort());
return suites;
})
} else {
let tests = getTestEntries(file);
[...tests].forEach(addEntry);
}
// sort and remove dups
suites = removeDups(suites.sort());
return suites;
});
}
exports.getSuites = getSuites;
const makeFilter = function makeFilter(options) {
// A filter string is {fileNameRegex}[:{testNameRegex}] - ie, a colon
// optionally separates a regex for the test fileName from a regex for the
// testName.
if (options.filter) {
let colonPos = options.filter.indexOf(':');
let filterFileRegex, filterNameRegex;
if (colonPos === -1) {
filterFileRegex = new RegExp(options.filter);
} else {
filterFileRegex = new RegExp(options.filter.substr(0, colonPos));
filterNameRegex = new RegExp(options.filter.substr(colonPos + 1));
}
// This function will first be called with just the filename; if
// it returns true the module will be loaded then the function
// called again with both the filename and the testname.
return (filename, testname) => {
return filterFileRegex.test(filename) &&
((testname && filterNameRegex) ? filterNameRegex.test(testname)
: true);
};
}
return () => true;
}
exports.makeFilter = makeFilter;
let loader = Loader(module);
const NOT_TESTS = ['setup', 'teardown'];
@ -25,45 +124,20 @@ var TestFinder = exports.TestFinder = function TestFinder(options) {
};
TestFinder.prototype = {
findTests: function findTests(cb) {
var self = this;
var tests = [];
var filter;
// A filter string is {fileNameRegex}[:{testNameRegex}] - ie, a colon
// optionally separates a regex for the test fileName from a regex for the
// testName.
if (this.filter) {
var colonPos = this.filter.indexOf(':');
var filterFileRegex, filterNameRegex;
if (colonPos === -1) {
filterFileRegex = new RegExp(self.filter);
} else {
filterFileRegex = new RegExp(self.filter.substr(0, colonPos));
filterNameRegex = new RegExp(self.filter.substr(colonPos + 1));
}
// This function will first be called with just the filename; if
// it returns true the module will be loaded then the function
// called again with both the filename and the testname.
filter = function(filename, testname) {
return filterFileRegex.test(filename) &&
((testname && filterNameRegex) ? filterNameRegex.test(testname)
: true);
};
} else
filter = function() {return true};
findTests: function findTests() {
return getSuites({ id: id }).then(suites => {
let filter = makeFilter({ filter: this.filter });
let tests = [];
suites.forEach(function(suite) {
suites.forEach(suite => {
// Load each test file as a main module in its own loader instance
// `suite` is defined by cuddlefish/manifest.py:ManifestBuilder.build
let suiteModule;
try {
suiteModule = cuddlefish.main(loader, suite);
}
catch (e) {
if (!/^Unsupported Application/.test(e.message))
throw e;
// If `Unsupported Application` error thrown during test,
// skip the test suite
suiteModule = {
@ -71,19 +145,21 @@ TestFinder.prototype = {
};
}
if (self.testInProcess)
if (this.testInProcess) {
for each (let name in Object.keys(suiteModule).sort()) {
if(NOT_TESTS.indexOf(name) === -1 && filter(suite, name)) {
if (NOT_TESTS.indexOf(name) === -1 && filter(suite, name)) {
tests.push({
setup: suiteModule.setup,
teardown: suiteModule.teardown,
testFunction: suiteModule[name],
name: suite + "." + name
});
setup: suiteModule.setup,
teardown: suiteModule.teardown,
testFunction: suiteModule[name],
name: suite + "." + name
});
}
}
});
}
})
cb(tests);
return tests;
});
}
};

View File

@ -1,7 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
module.metadata = {
@ -9,12 +8,13 @@ module.metadata = {
};
const memory = require('./memory');
var timer = require("../timers");
const timer = require("../timers");
var cfxArgs = require("@test/options");
const { getTabs, getURI } = require("../tabs/utils");
const { windows, isBrowser } = require("../window/utils");
const { defer } = require("../core/promise");
exports.findAndRunTests = function findAndRunTests(options) {
const findAndRunTests = function findAndRunTests(options) {
var TestFinder = require("./unit-test-finder").TestFinder;
var finder = new TestFinder({
filter: options.filter,
@ -22,15 +22,17 @@ exports.findAndRunTests = function findAndRunTests(options) {
testOutOfProcess: options.testOutOfProcess
});
var runner = new TestRunner({fs: options.fs});
finder.findTests(
function (tests) {
runner.startMany({tests: tests,
stopOnError: options.stopOnError,
onDone: options.onDone});
finder.findTests().then(tests => {
runner.startMany({
tests: tests,
stopOnError: options.stopOnError,
onDone: options.onDone
});
});
};
exports.findAndRunTests = findAndRunTests;
var TestRunner = exports.TestRunner = function TestRunner(options) {
const TestRunner = function TestRunner(options) {
if (options) {
this.fs = options.fs;
}
@ -40,6 +42,7 @@ var TestRunner = exports.TestRunner = function TestRunner(options) {
this.failed = 0;
this.testRunSummary = [];
this.expectFailNesting = 0;
this.done = TestRunner.prototype.done.bind(this);
};
TestRunner.prototype = {
@ -252,9 +255,9 @@ TestRunner.prototype = {
assertArray: function(a, message) {
this.assertStrictEqual('[object Array]', Object.prototype.toString.apply(a), message);
},
assertNumber: function(a, message) {
this.assertStrictEqual('[object Number]', Object.prototype.toString.apply(a), message);
this.assertStrictEqual('[object Number]', Object.prototype.toString.apply(a), message);
},
done: function done() {
@ -321,36 +324,37 @@ TestRunner.prototype = {
}
}
},
// Set of assertion functions to wait for an assertion to become true
// These functions take the same arguments as the TestRunner.assert* methods.
waitUntil: function waitUntil() {
return this._waitUntil(this.assert, arguments);
},
waitUntilNotEqual: function waitUntilNotEqual() {
return this._waitUntil(this.assertNotEqual, arguments);
},
waitUntilEqual: function waitUntilEqual() {
return this._waitUntil(this.assertEqual, arguments);
},
waitUntilMatches: function waitUntilMatches() {
return this._waitUntil(this.assertMatches, arguments);
},
/**
* Internal function that waits for an assertion to become true.
* @param {Function} assertionMethod
* Reference to a TestRunner assertion method like test.assert,
* Reference to a TestRunner assertion method like test.assert,
* test.assertEqual, ...
* @param {Array} args
* List of arguments to give to the previous assertion method.
* List of arguments to give to the previous assertion method.
* All functions in this list are going to be called to retrieve current
* assertion values.
*/
_waitUntil: function waitUntil(assertionMethod, args) {
let { promise, resolve } = defer();
let count = 0;
let maxCount = this.DEFAULT_PAUSE_TIMEOUT / this.PAUSE_DELAY;
@ -358,9 +362,7 @@ TestRunner.prototype = {
if (!this.waitTimeout)
this.waitUntilDone(this.DEFAULT_PAUSE_TIMEOUT);
let callback = null;
let finished = false;
let test = this;
// capture a traceback before we go async.
@ -380,9 +382,8 @@ TestRunner.prototype = {
pass: function (msg) {
test.pass(msg);
test.waitUntilCallback = null;
if (callback && !stopIt)
callback();
finished = true;
if (!stopIt)
resolve();
},
fail: function (msg) {
// If we are called on test timeout, we stop the loop
@ -398,8 +399,8 @@ TestRunner.prototype = {
timeout = timer.setTimeout(loop, test.PAUSE_DELAY);
}
};
// Automatically call args closures in order to build arguments for
// Automatically call args closures in order to build arguments for
// assertion function
let appliedArgs = [];
for (let i = 0, l = args.length; i < l; i++) {
@ -411,33 +412,21 @@ TestRunner.prototype = {
catch(e) {
test.fail("Exception when calling asynchronous assertion: " + e +
"\n" + e.stack);
finished = true;
return;
return resolve();
}
}
appliedArgs.push(a);
}
// Finally call assertion function with current assertion values
assertionMethod.apply(mock, appliedArgs);
}
loop();
this.waitUntilCallback = loop;
// Return an object with `then` method, to offer a way to execute
// some code when the assertion passed or failed
return {
then: function (c) {
callback = c;
// In case of immediate positive result, we need to execute callback
// immediately here:
if (finished)
callback();
}
};
return promise;
},
waitUntilDone: function waitUntilDone(ms) {
if (ms === undefined)
ms = this.DEFAULT_PAUSE_TIMEOUT;
@ -514,3 +503,4 @@ TestRunner.prototype = {
this.done();
}
};
exports.TestRunner = TestRunner;

View File

@ -621,7 +621,4 @@ var runTests = exports.runTests = function runTests(options) {
}
};
unload(function() {
cService.unregisterListener(consoleListener);
});
unload(_ => cService.unregisterListener(consoleListener));

View File

@ -33,24 +33,22 @@ function runTests(findAndRunTests) {
// are not correctly updated.
// For ex: nsIFocusManager.getFocusedElementForWindow may throw
// NS_ERROR_ILLEGAL_VALUE exception.
require("../timers").setTimeout(function () {
harness.runTests({
findAndRunTests: findAndRunTests,
iterations: cfxArgs.iterations || 1,
filter: cfxArgs.filter,
profileMemory: cfxArgs.profileMemory,
stopOnError: cfxArgs.stopOnError,
verbose: cfxArgs.verbose,
parseable: cfxArgs.parseable,
print: stdout.write,
onDone: onDone
});
}, 0);
require("../timers").setTimeout(_ => harness.runTests({
findAndRunTests: findAndRunTests,
iterations: cfxArgs.iterations || 1,
filter: cfxArgs.filter,
profileMemory: cfxArgs.profileMemory,
stopOnError: cfxArgs.stopOnError,
verbose: cfxArgs.verbose,
parseable: cfxArgs.parseable,
print: stdout.write,
onDone: onDone
}));
}
function printFailedTests(tests, print) {
let iterationNumber = 0;
let singleIteration = tests.testRuns.length == 1;
let singleIteration = (tests.testRuns || []).length == 1;
let padding = singleIteration ? "" : " ";
print("\nThe following tests failed:\n");
@ -94,7 +92,7 @@ exports.runTestsFromModule = function runTestsFromModule(module) {
let id = module.id;
// Make a copy of exports as it may already be frozen by module loader
let exports = {};
Object.keys(module.exports).forEach(function(key) {
Object.keys(module.exports).forEach(key => {
exports[key] = module.exports[key];
});
@ -102,7 +100,7 @@ exports.runTestsFromModule = function runTestsFromModule(module) {
// Consider that all these tests are CommonJS ones
loader.require('../../test').run(exports);
// Reproduce what is done in unit-test-finder.findTests()
// Reproduce what is done in sdk/deprecated/unit-test-finder.findTests()
let tests = [];
for each (let name in Object.keys(exports).sort()) {
tests.push({

View File

@ -0,0 +1,22 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
const { Cc, Ci, Cu } = require("chrome");
const { defer } = require("../core/promise");
const getZipReader = function getZipReader(aFile) {
let { promise, resolve, reject } = defer();
let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
createInstance(Ci.nsIZipReader);
try {
zipReader.open(aFile);
}
catch(e){
reject(e);
}
resolve(zipReader);
return promise;
};
exports.getZipReader = getZipReader;

View File

@ -6,5 +6,6 @@
"xulrunner", "firefox", "browser"
],
"loader": "lib/sdk/loader/cuddlefish.js",
"license": "MPL 2.0"
"license": "MPL 2.0",
"unpack": true
}

View File

@ -836,12 +836,6 @@ def run(arguments=sys.argv[1:], target_cfg=None, pkg_cfg=None,
# Pass a flag in order to force using sdk modules shipped in the xpi
harness_options['force-use-bundled-sdk'] = True
# Pass the list of absolute path for all test modules
if command == "test":
harness_options['allTestModules'] = manifest.get_all_test_modules()
if len(harness_options['allTestModules']) == 0:
sys.exit(0)
from cuddlefish.rdf import gen_manifest, RDFUpdate
manifest_rdf = gen_manifest(template_root_dir=app_extension_dir,

View File

@ -278,9 +278,6 @@ class ManifestBuilder:
if me.packageName != "addon-sdk" or bundle_sdk_modules:
yield me.js_filename
def get_all_test_modules(self):
return self.test_modules
def get_harness_options_manifest(self, bundle_sdk_modules):
manifest = {}
for me in self.get_module_entries():

View File

@ -539,7 +539,10 @@ def run_app(harness_root_dir, manifest_rdf, harness_options,
outfile_tail = follow_file(outfile)
def maybe_remove_outfile():
if os.path.exists(outfile):
os.remove(outfile)
try:
os.remove(outfile)
except Exception, e:
print "Error Cleaning up: " + str(e)
atexit.register(maybe_remove_outfile)
outf = open(outfile, "w")
popen_kwargs = { 'stdout': outf, 'stderr': outf}

View File

@ -190,8 +190,7 @@ class TestCfxQuits(unittest.TestCase):
self.assertIn("Program terminated successfully.", err)
def test_cfx_test(self):
addon_path = os.path.join(tests_path,
"addons", "simplest-test")
addon_path = os.path.join(tests_path, "addons", "simplest-test")
rc, out, err = self.run_cfx(addon_path, ["test"])
self.assertEqual(rc, 0)
self.assertIn("1 of 1 tests passed.", err)

View File

@ -244,91 +244,6 @@ class SmallXPI(unittest.TestCase):
"uft8_value": "\u00e9"
}'''))
def test_scantests(self):
target_cfg = self.get_pkg("three")
package_path = [self.get_linker_files_dir("three-deps")]
pkg_cfg = packaging.build_config(self.root, target_cfg,
packagepath=package_path)
deps = packaging.get_deps_for_targets(pkg_cfg,
[target_cfg.name, "addon-sdk"])
m = manifest.build_manifest(target_cfg, pkg_cfg, deps, scan_tests=True)
self.failUnlessEqual(sorted(m.get_all_test_modules()),
sorted(["three/tests/test-one", "three/tests/test-two"]))
# the current __init__.py code omits limit_to=used_files for 'cfx
# test', so all test files are included in the XPI. But the test
# runner will only execute the tests that m.get_all_test_modules()
# tells us about (which are put into the .allTestModules property of
# harness-options.json).
used_deps = m.get_used_packages()
build = packaging.generate_build_for_target(pkg_cfg, target_cfg.name,
used_deps,
include_tests=True)
options = {'main': target_cfg.main}
options.update(build)
basedir = self.make_basedir()
xpi_name = os.path.join(basedir, "contents.xpi")
xpi.build_xpi(template_root_dir=xpi_template_path,
manifest=fake_manifest,
xpi_path=xpi_name,
harness_options=options,
limit_to=None)
x = zipfile.ZipFile(xpi_name, "r")
names = x.namelist()
self.failUnless("resources/addon-sdk/lib/sdk/deprecated/unit-test.js" in names, names)
self.failUnless("resources/addon-sdk/lib/sdk/deprecated/unit-test-finder.js" in names, names)
self.failUnless("resources/addon-sdk/lib/sdk/test/harness.js" in names, names)
self.failUnless("resources/addon-sdk/lib/sdk/test/runner.js" in names, names)
# all files are copied into the XPI, even the things that don't look
# like tests.
self.failUnless("resources/three/tests/test-one.js" in names, names)
self.failUnless("resources/three/tests/test-two.js" in names, names)
self.failUnless("resources/three/tests/nontest.js" in names, names)
def test_scantests_filter(self):
target_cfg = self.get_pkg("three")
package_path = [self.get_linker_files_dir("three-deps")]
pkg_cfg = packaging.build_config(self.root, target_cfg,
packagepath=package_path)
deps = packaging.get_deps_for_targets(pkg_cfg,
[target_cfg.name, "addon-sdk"])
FILTER = ".*one.*"
m = manifest.build_manifest(target_cfg, pkg_cfg, deps, scan_tests=True,
test_filter_re=FILTER)
self.failUnlessEqual(sorted(m.get_all_test_modules()),
sorted(["three/tests/test-one"]))
# the current __init__.py code omits limit_to=used_files for 'cfx
# test', so all test files are included in the XPI. But the test
# runner will only execute the tests that m.get_all_test_modules()
# tells us about (which are put into the .allTestModules property of
# harness-options.json).
used_deps = m.get_used_packages()
build = packaging.generate_build_for_target(pkg_cfg, target_cfg.name,
used_deps,
include_tests=True)
options = {'main': target_cfg.main}
options.update(build)
basedir = self.make_basedir()
xpi_name = os.path.join(basedir, "contents.xpi")
xpi.build_xpi(template_root_dir=xpi_template_path,
manifest=fake_manifest,
xpi_path=xpi_name,
harness_options=options,
limit_to=None)
x = zipfile.ZipFile(xpi_name, "r")
names = x.namelist()
self.failUnless("resources/addon-sdk/lib/sdk/deprecated/unit-test.js" in names, names)
self.failUnless("resources/addon-sdk/lib/sdk/deprecated/unit-test-finder.js" in names, names)
self.failUnless("resources/addon-sdk/lib/sdk/test/harness.js" in names, names)
self.failUnless("resources/addon-sdk/lib/sdk/test/runner.js" in names, names)
# get_all_test_modules() respects the filter. But all files are still
# copied into the XPI.
self.failUnless("resources/three/tests/test-one.js" in names, names)
self.failUnless("resources/three/tests/test-two.js" in names, names)
self.failUnless("resources/three/tests/nontest.js" in names, names)
def document_dir(name):
if name in ['packages', 'xpi-template']:

View File

@ -24,8 +24,9 @@ def mkzipdir(zf, path):
def build_xpi(template_root_dir, manifest, xpi_path,
harness_options, limit_to=None, extra_harness_options={},
bundle_sdk=True, pkgdir=""):
IGNORED_FILES = [".hgignore", ".DS_Store", "install.rdf",
IGNORED_FILES = [".hgignore", ".DS_Store",
"application.ini", xpi_path]
IGNORED_TOP_LVL_FILES = ["install.rdf"]
files_to_copy = {} # maps zipfile path to local-disk abspath
dirs_to_create = set() # zipfile paths, no trailing slash
@ -71,6 +72,8 @@ def build_xpi(template_root_dir, manifest, xpi_path,
files_to_copy[str(arcpath)] = str(abspath)
for dirpath, dirnames, filenames in os.walk(template_root_dir):
if template_root_dir == dirpath:
filenames = list(filter_filenames(filenames, IGNORED_TOP_LVL_FILES))
filenames = list(filter_filenames(filenames, IGNORED_FILES))
dirnames[:] = filter_dirnames(dirnames)
for dirname in dirnames:

View File

@ -21,7 +21,6 @@ exports.testNoTabCloseOnStartup = function(assert, done) {
});
}
exports.main = function() {
tabs.on('close', closeEventDetector);

View File

@ -8,7 +8,7 @@ const { Loader } = require("sdk/test/loader");
var setupCalled = false, teardownCalled = false;
exports.setup = function() {
setupCalled = true;
setupCalled = true;
};
exports.teardown = function() {
@ -16,10 +16,10 @@ exports.teardown = function() {
setupCalled = false;
};
// Important note - unit tests are run in alphabetical order. The following
// unit tests for setup/teardown are order dependent, sometimes the result of
// one test is checked in the next test (testing for teardown does this). When
// tests are cohesively a single unit, they are named <test_name> - partN where
// Important note - unit tests are run in alphabetical order. The following
// unit tests for setup/teardown are order dependent, sometimes the result of
// one test is checked in the next test (testing for teardown does this). When
// tests are cohesively a single unit, they are named <test_name> - partN where
// N is their order in the sequence. Secondly, because these tests should be
// run before all others, they start with an A.
exports.testASetupTeardownSyncTestPart1 = function(test) {
@ -34,11 +34,10 @@ exports.testASetupTeardownSyncTestPart2 = function(test) {
exports.testATeardownAsyncTestPart1 = function(test) {
teardownCalled = false;
timer.setTimeout(function() {
timer.setTimeout(_ => {
test.assertEqual(false, teardownCalled, "teardown not called until done");
test.done();
}, 200);
}, 20);
test.waitUntilDone();
};
@ -48,7 +47,7 @@ exports.testATeardownAsyncTestPart2 = function(test) {
exports.testWaitUntilInstant = function(test) {
test.waitUntilDone();
test.waitUntil(function () true, "waitUntil with instant true pass")
.then(function () test.done());
}
@ -56,60 +55,63 @@ exports.testWaitUntilInstant = function(test) {
exports.testWaitUntil = function(test) {
test.waitUntilDone();
let succeed = false;
test.waitUntil(function () succeed, "waitUntil pass")
.then(function () test.done());
timer.setTimeout(function () {
test.waitUntil(_ => succeed, "waitUntil pass")
.then(test.done);
timer.setTimeout(_ => {
test.pass("succeed");
succeed = true;
}, 200);
}, 20);
}
exports.testWaitUntilEqual = function(test) {
test.waitUntilDone();
let succeed = false;
test.waitUntilEqual("foo", function () succeed ? "foo" : "bar",
test.waitUntilEqual("foo", _ => succeed ? "foo" : "bar",
"waitUntilEqual pass")
.then(function () test.done());
timer.setTimeout(function () {
.then(test.done);
timer.setTimeout(_ => {
test.pass("succeed");
succeed = true;
}, 200);
}, 20);
}
exports.testWaitUntilNotEqual = function(test) {
test.waitUntilDone();
let succeed = false;
test.waitUntilNotEqual("foo", function () succeed ? "bar" : "foo",
test.waitUntilNotEqual("foo", _ => succeed ? "bar" : "foo",
"waitUntilNotEqual pass")
.then(function () test.done());
timer.setTimeout(function () {
.then(test.done);
timer.setTimeout(_ => {
test.pass("succeed");
succeed = true;
}, 200);
}, 20);
}
exports.testWaitUntilMatches = function(test) {
test.waitUntilDone();
let succeed = false;
test.waitUntilMatches(function () succeed ? "foo" : "bar",
test.waitUntilMatches(_ => succeed ? "foo" : "bar",
/foo/, "waitUntilEqual pass")
.then(function () test.done());
timer.setTimeout(function () {
.then(test.done);
timer.setTimeout(_ => {
test.pass("succeed");
succeed = true;
}, 200);
}, 20);
}
exports.testWaitUntilErrorInCallback = function(test) {
test.waitUntilDone();
test.expectFail(function() {
test.waitUntil(function () {throw "oops"}, "waitUntil pass")
.then(function () test.done());
test.expectFail(_ => {
test.waitUntil(_ => { throw "oops"; }, "waitUntil pass")
.then(test.done);
});
}
@ -195,26 +197,26 @@ exports.testAssertFunction = function(test) {
test.assertFunction(function() {}, 'assertFunction with function');
test.expectFail(function() {
test.assertFunction(null, 'assertFunction with non-function');
});
});
};
exports.testAssertUndefined = function(test) {
test.assertUndefined(undefined, 'assertUndefined with undefined');
test.expectFail(function() {
test.assertUndefined(null, 'assertUndefined with null');
});
});
test.expectFail(function() {
test.assertUndefined(false, 'assertUndefined with false');
});
});
test.expectFail(function() {
test.assertUndefined(0, 'assertUndefined with 0');
});
});
};
exports.testAssertNotUndefined = function(test) {
test.expectFail(function() {
test.assertNotUndefined(undefined, 'assertNotUndefined with undefined');
});
});
test.assertNotUndefined(null, 'assertNotUndefined with null');
test.assertNotUndefined(false, 'assertNotUndefined with false');
test.assertNotUndefined(0, 'assertNotUndefined with 0');
@ -224,7 +226,7 @@ exports.testAssertNull = function(test) {
test.assertNull(null, 'assertNull with null');
test.expectFail(function() {
test.assertNull(undefined, 'assertNull with undefined');
});
});
test.expectFail(function() {
test.assertNull(false, 'assertNull with false');
});
@ -240,7 +242,7 @@ exports.testAssertNotNull = function(test) {
test.expectFail(function() {
test.assertNotNull(null, 'testAssertNotNull with null');
});
});
};
exports.testAssertObject = function(test) {

View File

@ -19,13 +19,13 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="2944695a89c3281d49dbe5ff9c0cd26c8318e2ba"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="57da30f405ba37a5d4844f32bb292271b81faee2"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="3326b51017252e09ccdd715dec6c5e12a7d1ecfe"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="c510babaf88dfa2cfe2c202afb2649ee124569af"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="aa5ba9351d37d4f9055c696edfaaee8db559f851"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="b5e21ed930860a0d43728b556b09e8dedd27fdeb"/>
<!-- Stock Android things -->
<project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
<project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>

View File

@ -17,10 +17,10 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2944695a89c3281d49dbe5ff9c0cd26c8318e2ba"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="57da30f405ba37a5d4844f32bb292271b81faee2"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="c510babaf88dfa2cfe2c202afb2649ee124569af"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="aa5ba9351d37d4f9055c696edfaaee8db559f851"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="b5e21ed930860a0d43728b556b09e8dedd27fdeb"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<!-- Stock Android things -->

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="276ce45e78b09c4a4ee643646f691d22804754c1">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2944695a89c3281d49dbe5ff9c0cd26c8318e2ba"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="57da30f405ba37a5d4844f32bb292271b81faee2"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
@ -23,7 +23,7 @@
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="aa5ba9351d37d4f9055c696edfaaee8db559f851"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="b5e21ed930860a0d43728b556b09e8dedd27fdeb"/>
<!-- Stock Android things -->
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="f92a936f2aa97526d4593386754bdbf02db07a12"/>
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="6e47ff2790f5656b5b074407829ceecf3e6188c4"/>

View File

@ -19,13 +19,13 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="2944695a89c3281d49dbe5ff9c0cd26c8318e2ba"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="57da30f405ba37a5d4844f32bb292271b81faee2"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="3326b51017252e09ccdd715dec6c5e12a7d1ecfe"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="c510babaf88dfa2cfe2c202afb2649ee124569af"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="aa5ba9351d37d4f9055c696edfaaee8db559f851"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="b5e21ed930860a0d43728b556b09e8dedd27fdeb"/>
<!-- Stock Android things -->
<project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
<project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>

View File

@ -17,10 +17,10 @@
</project>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2944695a89c3281d49dbe5ff9c0cd26c8318e2ba"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="57da30f405ba37a5d4844f32bb292271b81faee2"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="c510babaf88dfa2cfe2c202afb2649ee124569af"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="aa5ba9351d37d4f9055c696edfaaee8db559f851"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="b5e21ed930860a0d43728b556b09e8dedd27fdeb"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<!-- Stock Android things -->

View File

@ -4,6 +4,6 @@
"remote": "",
"branch": ""
},
"revision": "2ae85acdbb87a0bd64144196d44ca0acd134b38c",
"revision": "4073b397d7b5d37ae81ced569e4af7c734defe68",
"repo_path": "/integration/gaia-central"
}

View File

@ -17,12 +17,12 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="2944695a89c3281d49dbe5ff9c0cd26c8318e2ba"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="57da30f405ba37a5d4844f32bb292271b81faee2"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="c510babaf88dfa2cfe2c202afb2649ee124569af"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="aa5ba9351d37d4f9055c696edfaaee8db559f851"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="b5e21ed930860a0d43728b556b09e8dedd27fdeb"/>
<!-- Stock Android things -->
<project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
<project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>

View File

@ -15,7 +15,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="2944695a89c3281d49dbe5ff9c0cd26c8318e2ba"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="57da30f405ba37a5d4844f32bb292271b81faee2"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -17,10 +17,10 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2944695a89c3281d49dbe5ff9c0cd26c8318e2ba"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="57da30f405ba37a5d4844f32bb292271b81faee2"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="c510babaf88dfa2cfe2c202afb2649ee124569af"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="aa5ba9351d37d4f9055c696edfaaee8db559f851"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="b5e21ed930860a0d43728b556b09e8dedd27fdeb"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<!-- Stock Android things -->

View File

@ -17,12 +17,12 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="2944695a89c3281d49dbe5ff9c0cd26c8318e2ba"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="57da30f405ba37a5d4844f32bb292271b81faee2"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="c510babaf88dfa2cfe2c202afb2649ee124569af"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="aa5ba9351d37d4f9055c696edfaaee8db559f851"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="b5e21ed930860a0d43728b556b09e8dedd27fdeb"/>
<project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
<!-- Stock Android things -->
<project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>

View File

@ -486,7 +486,7 @@
@BINPATH@/components/FormHistoryStartup.js
@BINPATH@/components/nsInputListAutoComplete.js
@BINPATH@/components/formautofill.manifest
@BINPATH@/components/AutofillController.js
@BINPATH@/components/FormAutofillContentService.js
@BINPATH@/components/contentSecurityPolicy.manifest
@BINPATH@/components/contentSecurityPolicy.js
@BINPATH@/components/contentAreaDropListener.manifest

View File

@ -1607,8 +1607,6 @@
// allows the TabLabelModified event to be properly dispatched.
if (!aURI || isBlankPageURL(aURI)) {
t.label = this.mStringBundle.getString("tabs.emptyTabTitle");
} else {
t.label = aURI;
}
this.tabContainer.updateVisibility();

View File

@ -372,16 +372,10 @@ function test18a() {
var updateLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "checkForUpdatesLink");
ok(updateLink.style.visibility != "hidden", "Test 18a, Plugin should have an update link");
var tabOpenListener = new TabOpenListener(Services.urlFormatter.formatURLPref("plugins.update.url"), false, false);
tabOpenListener.handleEvent = function(event) {
if (event.type == "TabOpen") {
gBrowser.tabContainer.removeEventListener("TabOpen", this, false);
this.tab = event.originalTarget;
is(event.target.label, this.url, "Test 18a, Update link should open up the plugin check page");
gBrowser.removeTab(this.tab);
test18b();
}
};
var pluginUpdateURL = Services.urlFormatter.formatURLPref("plugins.update.url");
var tabOpenListener = new TabOpenListener(pluginUpdateURL, function(tab) {
gBrowser.removeTab(tab);
}, test18b);
EventUtils.synthesizeMouseAtCenter(updateLink, {}, gTestBrowser.contentWindow);
}

View File

@ -56,7 +56,7 @@
#ifdef USE_WIN_TITLE_STYLE
title="&prefWindow.titleWin;">
#else
title="&prefWindow.titleGNOME;">
title="&prefWindow.title;">
#endif
<html:link rel="shortcut icon"

View File

@ -9,6 +9,10 @@ this.EXPORTED_SYMBOLS = ["PrivacyLevel"];
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup",
"@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
const PREF_NORMAL = "browser.sessionstore.privacy_level";
const PREF_DEFERRED = "browser.sessionstore.privacy_level_deferred";
@ -24,14 +28,6 @@ const PRIVACY_ENCRYPTED = 1;
// Collect no data.
const PRIVACY_FULL = 2;
/**
* Returns whether we will resume the session automatically on next startup.
*/
function willResumeAutomatically() {
return Services.prefs.getIntPref("browser.startup.page") == 3 ||
Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
}
/**
* Determines the current privacy level as set by the user.
*
@ -44,7 +40,7 @@ function getCurrentLevel(isPinned) {
// If we're in the process of quitting and we're not autoresuming the session
// then we will use the deferred privacy level for non-pinned tabs.
if (!isPinned && Services.startup.shuttingDown && !willResumeAutomatically()) {
if (!isPinned && Services.startup.shuttingDown && !gSessionStartup.isAutomaticRestoreEnabled()) {
pref = PREF_DEFERRED;
}

View File

@ -45,6 +45,12 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
"@mozilla.org/base/telemetry;1", "nsITelemetry");
XPCOMUtils.defineLazyServiceGetter(this, "sessionStartup",
"@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
XPCOMUtils.defineLazyModuleGetter(this, "SessionWorker",
"resource:///modules/sessionstore/SessionWorker.jsm");
const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID";
this.SessionFile = {
/**
@ -75,43 +81,117 @@ this.SessionFile = {
gatherTelemetry: function(aData) {
return SessionFileInternal.gatherTelemetry(aData);
},
/**
* Create a backup copy, asynchronously.
* This is designed to perform backup on upgrade.
*/
createBackupCopy: function (ext) {
return SessionFileInternal.createBackupCopy(ext);
},
/**
* Remove a backup copy, asynchronously.
* This is designed to clean up a backup on upgrade.
*/
removeBackupCopy: function (ext) {
return SessionFileInternal.removeBackupCopy(ext);
},
/**
* Wipe the contents of the session file, asynchronously.
*/
wipe: function () {
SessionFileInternal.wipe();
return SessionFileInternal.wipe();
},
/**
* Return the paths to the files used to store, backup, etc.
* the state of the file.
*/
get Paths() {
return SessionFileInternal.Paths;
}
};
Object.freeze(SessionFile);
/**
* Utilities for dealing with promises and Task.jsm
*/
let SessionFileInternal = {
/**
* The path to sessionstore.js
*/
path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"),
let Path = OS.Path;
let profileDir = OS.Constants.Path.profileDir;
/**
* The path to sessionstore.bak
*/
backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"),
let SessionFileInternal = {
Paths: Object.freeze({
// The path to the latest version of sessionstore written during a clean
// shutdown. After startup, it is renamed `cleanBackup`.
clean: Path.join(profileDir, "sessionstore.js"),
// The path at which we store the previous version of `clean`. Updated
// whenever we successfully load from `clean`.
cleanBackup: Path.join(profileDir, "sessionstore-backups", "previous.js"),
// The directory containing all sessionstore backups.
backups: Path.join(profileDir, "sessionstore-backups"),
// The path to the latest version of the sessionstore written
// during runtime. Generally, this file contains more
// privacy-sensitive information than |clean|, and this file is
// therefore removed during clean shutdown. This file is designed to protect
// against crashes / sudden shutdown.
recovery: Path.join(profileDir, "sessionstore-backups", "recovery.js"),
// The path to the previous version of the sessionstore written
// during runtime (e.g. 15 seconds before recovery). In case of a
// clean shutdown, this file is removed. Generally, this file
// contains more privacy-sensitive information than |clean|, and
// this file is therefore removed during clean shutdown. This
// file is designed to protect against crashes that are nasty
// enough to corrupt |recovery|.
recoveryBackup: Path.join(profileDir, "sessionstore-backups", "recovery.bak"),
// The path to a backup created during an upgrade of Firefox.
// Having this backup protects the user essentially from bugs in
// Firefox or add-ons, especially for users of Nightly. This file
// does not contain any information more sensitive than |clean|.
upgradeBackupPrefix: Path.join(profileDir, "sessionstore-backups", "upgrade.js-"),
// The path to the backup of the version of the session store used
// during the latest upgrade of Firefox. During load/recovery,
// this file should be used if both |path|, |backupPath| and
// |latestStartPath| are absent/incorrect. May be "" if no
// upgrade backup has ever been performed. This file does not
// contain any information more sensitive than |clean|.
get upgradeBackup() {
let latestBackupID = SessionFileInternal.latestUpgradeBackupID;
if (!latestBackupID) {
return "";
}
return this.upgradeBackupPrefix + latestBackupID;
},
// The path to a backup created during an upgrade of Firefox.
// Having this backup protects the user essentially from bugs in
// Firefox, especially for users of Nightly.
get nextUpgradeBackup() {
return this.upgradeBackupPrefix + Services.appinfo.platformBuildID;
},
/**
* The order in which to search for a valid sessionstore file.
*/
get loadOrder() {
// If `clean` exists and has been written without corruption during
// the latest shutdown, we need to use it.
//
// Otherwise, `recovery` and `recoveryBackup` represent the most
// recent state of the session store.
//
// Finally, if nothing works, fall back to the last known state
// that can be loaded (`cleanBackup`) or, if available, to the
// backup performed during the latest upgrade.
let order = ["clean",
"recovery",
"recoveryBackup",
"cleanBackup"];
if (SessionFileInternal.latestUpgradeBackupID) {
// We have an upgradeBackup
order.push("upgradeBackup");
}
return order;
},
}),
// The ID of the latest version of Gecko for which we have an upgrade backup
// or |undefined| if no upgrade backup was ever written.
get latestUpgradeBackupID() {
try {
return Services.prefs.getCharPref(PREF_UPGRADE_BACKUP);
} catch (ex) {
return undefined;
}
},
/**
* The promise returned by the latest call to |write|.
@ -125,32 +205,57 @@ let SessionFileInternal = {
*/
_isClosed: false,
read: function () {
// We must initialize the worker during startup so it will be ready to
// perform the final write. If shutdown happens soon after startup and
// the worker has not started yet we may not write.
// See Bug 964531.
SessionWorker.post("init");
return Task.spawn(function*() {
for (let filename of [this.path, this.backupPath]) {
try {
let startMs = Date.now();
let data = yield OS.File.read(filename, { encoding: "utf-8" });
Telemetry.getHistogramById("FX_SESSION_RESTORE_READ_FILE_MS")
.add(Date.now() - startMs);
return data;
} catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
// Ignore exceptions about non-existent files.
read: Task.async(function* () {
let result;
// Attempt to load by order of priority from the various backups
for (let key of this.Paths.loadOrder) {
let corrupted = false;
let exists = true;
try {
let path = this.Paths[key];
let startMs = Date.now();
let source = yield OS.File.read(path, { encoding: "utf-8" });
let parsed = JSON.parse(source);
result = {
origin: key,
source: source,
parsed: parsed
};
Telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").
add(false);
Telemetry.getHistogramById("FX_SESSION_RESTORE_READ_FILE_MS").
add(Date.now() - startMs);
break;
} catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
exists = false;
} catch (ex if ex instanceof SyntaxError) {
// File is corrupted, try next file
corrupted = true;
} finally {
if (exists) {
Telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").
add(corrupted);
}
}
}
if (!result) {
// If everything fails, start with an empty session.
result = {
origin: "empty",
source: "",
parsed: null
};
}
return "";
}.bind(this));
},
// Initialize the worker to let it handle backups and also
// as a workaround for bug 964531.
SessionWorker.post("init", [
result.origin,
this.Paths,
]);
return result;
}),
gatherTelemetry: function(aStateString) {
return Task.spawn(function() {
@ -173,20 +278,32 @@ let SessionFileInternal = {
isFinalWrite = this._isClosed = true;
}
return this._latestWrite = Task.spawn(function task() {
return this._latestWrite = Task.spawn(function* task() {
TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
try {
let promise = SessionWorker.post("write", [aData]);
let performShutdownCleanup = isFinalWrite &&
!sessionStartup.isAutomaticRestoreEnabled();
let options = {
isFinalWrite: isFinalWrite,
performShutdownCleanup: performShutdownCleanup
};
let promise = SessionWorker.post("write", [aData, options]);
// At this point, we measure how long we stop the main thread
TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
// Now wait for the result and record how long the write took
let msg = yield promise;
this._recordTelemetry(msg.telemetry);
if (msg.ok && msg.ok.upgradeBackup) {
// We have just completed a backup-on-upgrade, store the information
// in preferences.
Services.prefs.setCharPref(PREF_UPGRADE_BACKUP, Services.appinfo.platformBuildID);
}
} catch (ex) {
TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
console.error("Could not write session state file ", this.path, ex);
console.error("Could not write session state file ", ex, ex.stack);
}
if (isFinalWrite) {
@ -195,16 +312,8 @@ let SessionFileInternal = {
}.bind(this));
},
createBackupCopy: function (ext) {
return SessionWorker.post("createBackupCopy", [ext]);
},
removeBackupCopy: function (ext) {
return SessionWorker.post("removeBackupCopy", [ext]);
},
wipe: function () {
SessionWorker.post("wipe");
return SessionWorker.post("wipe");
},
_recordTelemetry: function(telemetry) {
@ -224,31 +333,6 @@ let SessionFileInternal = {
}
};
// Interface to a dedicated thread handling I/O
let SessionWorker = (function () {
let worker = new PromiseWorker("resource:///modules/sessionstore/SessionWorker.js",
OS.Shared.LOG.bind("SessionWorker"));
return {
post: function post(...args) {
let promise = worker.post.apply(worker, args);
return promise.then(
null,
function onError(error) {
// Decode any serialized error
if (error instanceof PromiseWorker.WorkerError) {
throw OS.File.Error.fromMsg(error.data);
}
// Extract something meaningful from ErrorEvent
if (error instanceof ErrorEvent) {
throw new Error(error.message, error.filename, error.lineno);
}
throw error;
}
);
}
};
})();
// Ensure that we can write sessionstore.js cleanly before the profile
// becomes unaccessible.
AsyncShutdown.profileBeforeChange.addBlocker(

View File

@ -464,41 +464,10 @@ let SessionStoreInternal = {
this._prefBranch.getBoolPref("sessionstore.resume_session_once"))
this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
this._performUpgradeBackup();
TelemetryStopwatch.finish("FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS");
return state;
},
/**
* If this is the first time we launc this build of Firefox,
* backup sessionstore.js.
*/
_performUpgradeBackup: function ssi_performUpgradeBackup() {
// Perform upgrade backup, if necessary
const PREF_UPGRADE = "sessionstore.upgradeBackup.latestBuildID";
let buildID = Services.appinfo.platformBuildID;
let latestBackup = this._prefBranch.getCharPref(PREF_UPGRADE);
if (latestBackup == buildID) {
return Promise.resolve();
}
return Task.spawn(function task() {
try {
// Perform background backup
yield SessionFile.createBackupCopy("-" + buildID);
this._prefBranch.setCharPref(PREF_UPGRADE, buildID);
// In case of success, remove previous backup.
yield SessionFile.removeBackupCopy("-" + latestBackup);
} catch (ex) {
debug("Could not perform upgrade backup " + ex);
debug(ex.stack);
}
}.bind(this));
},
_initPrefs : function() {
this._prefBranch = Services.prefs.getBranch("browser.");

View File

@ -53,6 +53,39 @@ self.onmessage = function (msg) {
});
};
// The various possible states
/**
* We just started (we haven't written anything to disk yet) from
* `Paths.clean`. The backup directory may not exist.
*/
const STATE_CLEAN = "clean";
/**
* We know that `Paths.recovery` is good, either because we just read
* it (we haven't written anything to disk yet) or because have
* already written once to `Paths.recovery` during this session.
* `Paths.clean` is absent or invalid. The backup directory exists.
*/
const STATE_RECOVERY = "recovery";
/**
* We just started from `Paths.recoverBackupy` (we haven't written
* anything to disk yet). Both `Paths.clean` and `Paths.recovery` are
* absent or invalid. The backup directory exists.
*/
const STATE_RECOVERY_BACKUP = "recoveryBackup";
/**
* We just started from `Paths.upgradeBackup` (we haven't written
* anything to disk yet). Both `Paths.clean`, `Paths.recovery` and
* `Paths.recoveryBackup` are absent or invalid. The backup directory
* exists.
*/
const STATE_UPGRADE_BACKUP = "upgradeBackup";
/**
* We just started without a valid session store file (we haven't
* written anything to disk yet). The backup directory may not exist.
*/
const STATE_EMPTY = "empty";
let Agent = {
// Boolean that tells whether we already made a
// call to write(). We will only attempt to move
@ -60,49 +93,154 @@ let Agent = {
// first write.
hasWrittenState: false,
// The path to sessionstore.js
path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"),
// The path to sessionstore.bak
backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"),
// Path to the files used by the SessionWorker
Paths: null,
/**
* NO-OP to start the worker.
* The current state of the worker, as one of the following strings:
* - "permanent", once the first write has been completed;
* - "empty", before the first write has been completed,
* if we have started without any sessionstore;
* - one of "clean", "recovery", "recoveryBackup", "cleanBackup",
* "upgradeBackup", before the first write has been completed, if
* we have started by loading the corresponding file.
*/
init: function () {
state: null,
/**
* Initialize (or reinitialize) the worker
*
* @param {string} origin Which of sessionstore.js or its backups
* was used. One of the `STATE_*` constants defined above.
* @param {object} paths The paths at which to find the various files.
*/
init: function (origin, paths) {
if (!(origin in paths || origin == STATE_EMPTY)) {
throw new TypeError("Invalid origin: " + origin);
}
this.state = origin;
this.Paths = paths;
this.upgradeBackupNeeded = paths.nextUpgradeBackup != paths.upgradeBackup;
return {result: true};
},
/**
* Write the session to disk.
* Write the session to disk, performing any necessary backup
* along the way.
*
* @param {string} stateString The state to write to disk.
* @param {object} options
* - performShutdownCleanup If |true|, we should
* perform shutdown-time cleanup to ensure that private data
* is not left lying around;
* - isFinalWrite If |true|, write to Paths.clean instead of
* Paths.recovery
*/
write: function (stateString) {
write: function (stateString, options = {}) {
let exn;
let telemetry = {};
if (!this.hasWrittenState) {
try {
let startMs = Date.now();
File.move(this.path, this.backupPath);
telemetry.FX_SESSION_RESTORE_BACKUP_FILE_MS = Date.now() - startMs;
} catch (ex if isNoSuchFileEx(ex)) {
// Ignore exceptions about non-existent files.
} catch (ex) {
// Throw the exception after we wrote the state to disk
// so that the backup can't interfere with the actual write.
exn = ex;
let data = Encoder.encode(stateString);
let startWriteMs, stopWriteMs;
try {
if (this.state == STATE_CLEAN || this.state == STATE_EMPTY) {
// The backups directory may not exist yet. In all other cases,
// we have either already read from or already written to this
// directory, so we are satisfied that it exists.
File.makeDir(this.Paths.backups);
}
this.hasWrittenState = true;
if (this.state == STATE_CLEAN) {
// Move $Path.clean out of the way, to avoid any ambiguity as
// to which file is more recent.
File.move(this.Paths.clean, this.Paths.cleanBackup);
}
startWriteMs = Date.now();
if (options.isFinalWrite) {
// We are shutting down. At this stage, we know that
// $Paths.clean is either absent or corrupted. If it was
// originally present and valid, it has been moved to
// $Paths.cleanBackup a long time ago. We can therefore write
// with the guarantees that we erase no important data.
File.writeAtomic(this.Paths.clean, data, {
tmpPath: this.Paths.clean + ".tmp"
});
} else if (this.state == STATE_RECOVERY) {
// At this stage, either $Paths.recovery was written >= 15
// seconds ago during this session or we have just started
// from $Paths.recovery left from the previous session. Either
// way, $Paths.recovery is good. We can move $Path.backup to
// $Path.recoveryBackup without erasing a good file with a bad
// file.
File.writeAtomic(this.Paths.recovery, data, {
tmpPath: this.Paths.recovery + ".tmp",
backupTo: this.Paths.recoveryBackup
});
} else {
// In other cases, either $Path.recovery is not necessary, or
// it doesn't exist or it has been corrupted. Regardless,
// don't backup $Path.recovery.
File.writeAtomic(this.Paths.recovery, data, {
tmpPath: this.Paths.recovery + ".tmp"
});
}
stopWriteMs = Date.now();
} catch (ex) {
// Don't throw immediately
exn = exn || ex;
}
let ret = this._write(stateString, telemetry);
// If necessary, perform an upgrade backup
let upgradeBackupComplete = false;
if (this.upgradeBackupNeeded
&& (this.state == STATE_CLEAN || this.state == STATE_UPGRADE_BACKUP)) {
try {
// If we loaded from `clean`, the file has since then been renamed to `cleanBackup`.
let path = this.state == STATE_CLEAN ? this.Paths.cleanBackup : this.Paths.upgradeBackup;
File.copy(path, this.Paths.nextUpgradeBackup);
this.upgradeBackupNeeded = false;
upgradeBackupComplete = true;
} catch (ex) {
// Don't throw immediately
exn = exn || ex;
}
}
if (options.performShutdownCleanup && !exn) {
// During shutdown, if auto-restore is disabled, we need to
// remove possibly sensitive data that has been stored purely
// for crash recovery. Note that this slightly decreases our
// ability to recover from OS-level/hardware-level issue.
// If an exception was raised, we assume that we still need
// these files.
File.remove(this.Paths.recoveryBackup);
File.remove(this.Paths.recovery);
}
this.state = STATE_RECOVERY;
if (exn) {
throw exn;
}
return ret;
return {
result: {
upgradeBackup: upgradeBackupComplete
},
telemetry: {
FX_SESSION_RESTORE_WRITE_FILE_MS: stopWriteMs - startWriteMs,
FX_SESSION_RESTORE_FILE_SIZE_BYTES: data.byteLength,
}
};
},
/**
@ -115,66 +253,79 @@ let Agent = {
return Statistics.collect(stateString);
},
/**
* Write a stateString to disk
*/
_write: function (stateString, telemetry = {}) {
let bytes = Encoder.encode(stateString);
let startMs = Date.now();
let result = File.writeAtomic(this.path, bytes, {tmpPath: this.path + ".tmp"});
telemetry.FX_SESSION_RESTORE_WRITE_FILE_MS = Date.now() - startMs;
telemetry.FX_SESSION_RESTORE_FILE_SIZE_BYTES = bytes.byteLength;
return {result: result, telemetry: telemetry};
},
/**
* Creates a copy of sessionstore.js.
*/
createBackupCopy: function (ext) {
try {
return {result: File.copy(this.path, this.backupPath + ext)};
} catch (ex if isNoSuchFileEx(ex)) {
// Ignore exceptions about non-existent files.
return {result: true};
}
},
/**
* Removes a backup copy.
*/
removeBackupCopy: function (ext) {
try {
return {result: File.remove(this.backupPath + ext)};
} catch (ex if isNoSuchFileEx(ex)) {
// Ignore exceptions about non-existent files.
return {result: true};
}
},
/**
* Wipes all files holding session data from disk.
*/
wipe: function () {
let exn;
// Erase session state file
// Don't stop immediately in case of error.
let exn = null;
// Erase main session state file
try {
File.remove(this.path);
} catch (ex if isNoSuchFileEx(ex)) {
// Ignore exceptions about non-existent files.
File.remove(this.Paths.clean);
} catch (ex) {
// Don't stop immediately.
exn = ex;
exn = exn || ex;
}
// Erase any backup, any file named "sessionstore.bak[-buildID]".
let iter = new File.DirectoryIterator(OS.Constants.Path.profileDir);
for (let entry in iter) {
if (!entry.isDir && entry.path.startsWith(this.backupPath)) {
// Wipe the Session Restore directory
try {
this._wipeFromDir(this.Paths.backups, null);
} catch (ex) {
exn = exn || ex;
}
try {
File.removeDir(this.Paths.backups);
} catch (ex) {
exn = exn || ex;
}
// Wipe legacy Ression Restore files from the profile directory
try {
this._wipeFromDir(OS.Constants.Path.profileDir, "sessionstore.bak");
} catch (ex) {
exn = exn || ex;
}
this.state = STATE_EMPTY;
if (exn) {
throw exn;
}
return { result: true };
},
/**
* Wipe a number of files from a directory.
*
* @param {string} path The directory.
* @param {string|null} prefix If provided, only remove files whose
* name starts with a specific prefix.
*/
_wipeFromDir: function(path, prefix) {
// Sanity check
if (typeof prefix == "undefined" || prefix == "") {
throw new TypeError();
}
let exn = null;
let iterator = new File.DirectoryIterator(path);
if (!iterator.exists()) {
return;
}
for (let entry in iterator) {
if (entry.isDir) {
continue;
}
if (!prefix || entry.name.startsWith(prefix)) {
try {
File.remove(entry.path);
} catch (ex) {
// Don't stop immediately.
// Don't stop immediately
exn = exn || ex;
}
}
@ -183,9 +334,7 @@ let Agent = {
if (exn) {
throw exn;
}
return {result: true};
}
},
};
function isNoSuchFileEx(aReason) {

View File

@ -0,0 +1,43 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* Interface to a dedicated thread handling I/O
*/
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
Cu.import("resource://gre/modules/osfile.jsm", this);
this.EXPORTED_SYMBOLS = ["SessionWorker"];
this.SessionWorker = (function () {
let worker = new PromiseWorker("resource:///modules/sessionstore/SessionWorker.js",
OS.Shared.LOG.bind("SessionWorker"));
return {
post: function post(...args) {
let promise = worker.post.apply(worker, args);
return promise.then(
null,
function onError(error) {
// Decode any serialized error
if (error instanceof PromiseWorker.WorkerError) {
throw OS.File.Error.fromMsg(error.data);
}
// Extract something meaningful from ErrorEvent
if (error instanceof ErrorEvent) {
throw new Error(error.message, error.filename, error.lineno);
}
throw error;
}
);
}
};
})();

View File

@ -27,6 +27,7 @@ EXTRA_JS_MODULES = [
'SessionMigration.jsm',
'SessionStorage.jsm',
'SessionWorker.js',
'SessionWorker.jsm',
'TabAttributes.jsm',
'TabState.jsm',
'TabStateCache.jsm',

View File

@ -57,6 +57,11 @@ function debug(aMsg) {
aMsg = ("SessionStartup: " + aMsg).replace(/\S{80}/g, "$&\n");
Services.console.logStringMessage(aMsg);
}
function warning(aMsg, aException) {
let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
consoleMsg.init(aMsg, aException.fileName, null, aException.lineNumber, 0, Ci.nsIScriptError.warningFlag, "component javascript");
Services.console.logMessage(consoleMsg);
}
let gOnceInitializedDeferred = Promise.defer();
@ -107,27 +112,39 @@ SessionStartup.prototype = {
/**
* Complete initialization once the Session File has been read
*
* @param stateString
* string The Session State string read from disk
* @param source The Session State string read from disk.
* @param parsed The object obtained by parsing |source| as JSON.
*/
_onSessionFileRead: function (stateString) {
_onSessionFileRead: function ({source, parsed}) {
this._initialized = true;
// Let observers modify the state before it is used
let supportsStateString = this._createSupportsString(stateString);
let supportsStateString = this._createSupportsString(source);
Services.obs.notifyObservers(supportsStateString, "sessionstore-state-read", "");
stateString = supportsStateString.data;
let stateString = supportsStateString.data;
// No valid session found.
if (!stateString) {
if (stateString != source) {
// The session has been modified by an add-on, reparse.
try {
this._initialState = JSON.parse(stateString);
} catch (ex) {
// That's not very good, an add-on has rewritten the initial
// state to something that won't parse.
warning("Observer rewrote the state to something that won't parse", ex);
}
} else {
// No need to reparse
this._initialState = parsed;
}
if (this._initialState == null) {
// No valid session found.
this._sessionType = Ci.nsISessionStartup.NO_SESSION;
Services.obs.notifyObservers(null, "sessionstore-state-finalized", "");
gOnceInitializedDeferred.resolve();
return;
}
this._initialState = this._parseStateString(stateString);
let shouldResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
let shouldResumeSession = shouldResumeSessionOnce ||
Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION;
@ -194,29 +211,6 @@ SessionStartup.prototype = {
});
},
/**
* Convert the Session State string into a state object
*
* @param stateString
* string The Session State string read from disk
* @returns {State} a Session State object
*/
_parseStateString: function (stateString) {
let state = null;
let corruptFile = false;
try {
state = JSON.parse(stateString);
} catch (ex) {
debug("The session file contained un-parse-able JSON: " + ex);
corruptFile = true;
}
Services.telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").add(corruptFile);
return state;
},
/**
* Handle notifications
*/

View File

@ -60,6 +60,7 @@ support-files =
[browser_aboutPrivateBrowsing.js]
[browser_aboutSessionRestore.js]
[browser_attributes.js]
[browser_backup_recovery.js]
[browser_broadcast.js]
[browser_capabilities.js]
[browser_cleaner.js]
@ -180,7 +181,6 @@ skip-if = true # Needs to be rewritten as Marionette test, bug 995916
[browser_739805.js]
[browser_819510_perwindowpb.js]
skip-if = os == "linux" # Intermittent failures, bug 894063
[browser_833286_atomic_backup.js]
# Disabled for frequent intermittent failures
[browser_464620_a.js]

View File

@ -3,63 +3,56 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/** Private Browsing Test for Bug 394759 **/
function test() {
waitForExplicitFinish();
let windowsToClose = [];
let closedWindowCount = 0;
// Prevent VM timers issues, cache now and increment it manually.
let now = Date.now();
const TESTS = [
{ url: "about:config",
key: "bug 394759 Non-PB",
value: "uniq" + (++now) },
{ url: "about:mozilla",
key: "bug 394759 PB",
value: "uniq" + (++now) },
];
let closedWindowCount = 0;
// Prevent VM timers issues, cache now and increment it manually.
let now = Date.now();
registerCleanupFunction(function() {
Services.prefs.clearUserPref("browser.sessionstore.interval");
windowsToClose.forEach(function(win) {
win.close();
});
const TESTS = [
{ url: "about:config",
key: "bug 394759 Non-PB",
value: "uniq" + (++now) },
{ url: "about:mozilla",
key: "bug 394759 PB",
value: "uniq" + (++now) },
];
function promiseTestOpenCloseWindow(aIsPrivate, aTest) {
return Task.spawn(function*() {
let win = yield promiseNewWindowLoaded({ "private": aIsPrivate });
win.gBrowser.selectedBrowser.loadURI(aTest.url);
yield promiseBrowserLoaded(win.gBrowser.selectedBrowser);
yield Promise.resolve();
// Mark the window with some unique data to be restored later on.
ss.setWindowValue(win, aTest.key, aTest.value);
// Close.
yield promiseWindowClosed(win);
});
}
function testOpenCloseWindow(aIsPrivate, aTest, aCallback) {
whenNewWindowLoaded({ private: aIsPrivate }, function(win) {
whenBrowserLoaded(win.gBrowser.selectedBrowser, function() {
executeSoon(function() {
// Mark the window with some unique data to be restored later on.
ss.setWindowValue(win, aTest.key, aTest.value);
// Close.
win.close();
aCallback();
});
});
win.gBrowser.selectedBrowser.loadURI(aTest.url);
});
}
function promiseTestOnWindow(aIsPrivate, aValue) {
return Task.spawn(function*() {
let win = yield promiseNewWindowLoaded({ "private": aIsPrivate });
yield promiseCheckClosedWindows(aIsPrivate, aValue);
registerCleanupFunction(() => promiseWindowClosed(win));
});
}
function testOnWindow(aIsPrivate, aValue, aCallback) {
whenNewWindowLoaded({ private: aIsPrivate }, function(win) {
windowsToClose.push(win);
executeSoon(function() checkClosedWindows(aIsPrivate, aValue, aCallback));
});
}
function checkClosedWindows(aIsPrivate, aValue, aCallback) {
function promiseCheckClosedWindows(aIsPrivate, aValue) {
return Task.spawn(function*() {
let data = JSON.parse(ss.getClosedWindowData())[0];
is(ss.getClosedWindowCount(), 1, "Check the closed window count");
is(ss.getClosedWindowCount(), 1, "Check that the closed window count hasn't changed");
ok(JSON.stringify(data).indexOf(aValue) > -1,
"Check the closed window data was stored correctly");
aCallback();
}
});
}
function setupBlankState(aCallback) {
function promiseBlankState() {
return Task.spawn(function*() {
// Set interval to a large time so state won't be written while we setup
// environment.
Services.prefs.setIntPref("browser.sessionstore.interval", 100000);
registerCleanupFunction(() => Services.prefs.clearUserPref("browser.sessionstore.interval"));
// Set up the browser in a blank state. Popup windows in previous tests
// result in different states on different platforms.
@ -70,40 +63,39 @@ function test() {
}],
_closedWindows: []
});
ss.setBrowserState(blankState);
// Wait for the sessionstore.js file to be written before going on.
// Note: we don't wait for the complete event, since if asyncCopy fails we
// would timeout.
waitForSaveState(function(writing) {
ok(writing, "sessionstore.js is being written");
closedWindowCount = ss.getClosedWindowCount();
is(closedWindowCount, 0, "Correctly set window count");
executeSoon(aCallback);
});
yield forceSaveState();
closedWindowCount = ss.getClosedWindowCount();
is(closedWindowCount, 0, "Correctly set window count");
// Remove the sessionstore.js file before setting the interval to 0
let profilePath = Services.dirsvc.get("ProfD", Ci.nsIFile);
let sessionStoreJS = profilePath.clone();
sessionStoreJS.append("sessionstore.js");
if (sessionStoreJS.exists())
sessionStoreJS.remove(false);
info("sessionstore.js was correctly removed: " + (!sessionStoreJS.exists()));
yield SessionFile.wipe();
// Make sure that sessionstore.js can be forced to be created by setting
// the interval pref to 0.
Services.prefs.setIntPref("browser.sessionstore.interval", 0);
}
setupBlankState(function() {
testOpenCloseWindow(false, TESTS[0], function() {
testOpenCloseWindow(true, TESTS[1], function() {
testOnWindow(false, TESTS[0].value, function() {
testOnWindow(true, TESTS[0].value, finish);
});
});
});
yield forceSaveState();
});
}
add_task(function* init() {
while (ss.getClosedWindowCount() > 0) {
ss.forgetClosedWindow(0);
}
while (ss.getClosedTabCount(window) > 0) {
ss.forgetClosedTab(window, 0);
}
});
add_task(function* main() {
yield promiseTestOpenCloseWindow(false, TESTS[0]);
yield promiseTestOpenCloseWindow(true, TESTS[1]);
yield promiseTestOnWindow(false, TESTS[0].value);
yield promiseTestOnWindow(true, TESTS[0].value);
});

View File

@ -13,7 +13,7 @@ const PASS = "pwd-" + Math.random();
/**
* Bug 454908 - Don't save/restore values of password fields.
*/
add_task(function test_dont_save_passwords() {
add_task(function* test_dont_save_passwords() {
// Make sure we do save form data.
Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
@ -40,13 +40,12 @@ add_task(function test_dont_save_passwords() {
is(passwd, "", "password wasn't saved/restored");
// Write to disk and read our file.
yield SessionSaver.run();
let path = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
let data = yield OS.File.read(path);
let state = new TextDecoder().decode(data);
yield forceSaveState();
yield promiseForEachSessionRestoreFile((state, key) =>
// Ensure that we have not saved our password.
ok(!state.contains(PASS), "password has not been written to file " + key)
);
// Ensure that sessionstore.js doesn't contain our password.
is(state.indexOf(PASS), -1, "password has not been written to disk");
// Cleanup.
gBrowser.removeTab(tab);

View File

@ -36,7 +36,7 @@ add_task(function* new_window() {
yield promiseWindowClosed(newWin);
newWin = null;
let state = JSON.parse((yield promiseSaveFileContents()));
let state = JSON.parse((yield promiseRecoveryFileContents()));
is(state.windows.length, 2,
"observe1: 2 windows in data written to disk");
is(state._closedWindows.length, 0,
@ -60,7 +60,7 @@ add_task(function* new_tab() {
try {
newTab = gBrowser.addTab("about:mozilla");
let state = JSON.parse((yield promiseSaveFileContents()));
let state = JSON.parse((yield promiseRecoveryFileContents()));
is(state.windows.length, 1,
"observe2: 1 window in data being written to disk");
is(state._closedWindows.length, 1,

View File

@ -164,7 +164,7 @@ function waitForWindowClose(aWin, aCallback) {
}
function forceWriteState(aCallback) {
return promiseSaveFileContents().then(function(data) {
return promiseRecoveryFileContents().then(function(data) {
aCallback(JSON.parse(data));
});
}

View File

@ -1,99 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// This tests are for a sessionstore.js atomic backup.
// Each test will wait for a write to the Session Store
// before executing.
let tmp = {};
Cu.import("resource://gre/modules/osfile.jsm", tmp);
Cu.import("resource:///modules/sessionstore/SessionFile.jsm", tmp);
const {OS, SessionFile} = tmp;
const PREF_SS_INTERVAL = "browser.sessionstore.interval";
// Full paths for sessionstore.js and sessionstore.bak.
const path = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
const backupPath = OS.Path.join(OS.Constants.Path.profileDir,
"sessionstore.bak");
// A text decoder.
let gDecoder = new TextDecoder();
// Global variables that contain sessionstore.js and sessionstore.bak data for
// comparison between tests.
let gSSData;
let gSSBakData;
add_task(function* testAfterFirstWrite() {
// Ensure sessionstore.bak is not created. We start with a clean
// profile so there was nothing to move to sessionstore.bak before
// initially writing sessionstore.js
let ssExists = yield OS.File.exists(path);
let ssBackupExists = yield OS.File.exists(backupPath);
ok(ssExists, "sessionstore.js should exist.");
ok(!ssBackupExists, "sessionstore.bak should not have been created, yet");
// Save sessionstore.js data to compare to the sessionstore.bak data in the
// next test.
let array = yield OS.File.read(path);
gSSData = gDecoder.decode(array);
// Manually move to the backup since the first write has already happened
// and a backup would not be triggered again.
yield OS.File.move(path, backupPath);
yield forceSaveState();
});
add_task(function* testReadBackup() {
// Ensure sessionstore.bak is finally created.
let ssExists = yield OS.File.exists(path);
let ssBackupExists = yield OS.File.exists(backupPath);
ok(ssExists, "sessionstore.js exists.");
ok(ssBackupExists, "sessionstore.bak should now be created.");
// Read sessionstore.bak data.
let array = yield OS.File.read(backupPath);
gSSBakData = gDecoder.decode(array);
// Make sure that the sessionstore.bak is identical to the last
// sessionstore.js.
is(gSSBakData, gSSData, "sessionstore.js is backed up correctly.");
// Read latest sessionstore.js.
array = yield OS.File.read(path);
gSSData = gDecoder.decode(array);
// Read sessionstore.js with SessionFile.read.
let ssDataRead = yield SessionFile.read();
is(ssDataRead, gSSData, "SessionFile.read read sessionstore.js correctly.");
// Remove sessionstore.js to test fallback onto sessionstore.bak.
yield OS.File.remove(path);
ssExists = yield OS.File.exists(path);
ok(!ssExists, "sessionstore.js should be removed now.");
// Read sessionstore.bak with SessionFile.read.
ssDataRead = yield SessionFile.read();
is(ssDataRead, gSSBakData,
"SessionFile.read read sessionstore.bak correctly.");
yield forceSaveState();
});
add_task(function* testBackupUnchanged() {
// Ensure sessionstore.bak is backed up only once.
// Read sessionstore.bak data.
let array = yield OS.File.read(backupPath);
let ssBakData = gDecoder.decode(array);
// Ensure the sessionstore.bak did not change.
is(ssBakData, gSSBakData, "sessionstore.bak is unchanged.");
});
add_task(function* cleanup() {
// Cleaning up after the test: removing the sessionstore.bak file.
yield OS.File.remove(backupPath);
});

View File

@ -0,0 +1,132 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// This tests are for a sessionstore.js atomic backup.
// Each test will wait for a write to the Session Store
// before executing.
let OS = Cu.import("resource://gre/modules/osfile.jsm", {}).OS;
let {File, Constants, Path} = OS;
const PREF_SS_INTERVAL = "browser.sessionstore.interval";
const Paths = SessionFile.Paths;
// A text decoder.
let gDecoder = new TextDecoder();
// Global variables that contain sessionstore.js and sessionstore.bak data for
// comparison between tests.
let gSSData;
let gSSBakData;
function promiseRead(path) {
return File.read(path, {encoding: "utf-8"});
}
add_task(function* init() {
// Make sure that we are not racing with SessionSaver's time based
// saves.
Services.prefs.setIntPref(PREF_SS_INTERVAL, 10000000);
registerCleanupFunction(() => Services.prefs.clearUserPref(PREF_SS_INTERVAL));
});
add_task(function* test_creation() {
let OLD_BACKUP = Path.join(Constants.Path.profileDir, "sessionstore.bak");
let OLD_UPGRADE_BACKUP = Path.join(Constants.Path.profileDir, "sessionstore.bak-0000000");
yield File.writeAtomic(OLD_BACKUP, "sessionstore.bak");
yield File.writeAtomic(OLD_UPGRADE_BACKUP, "sessionstore upgrade backup");
yield SessionFile.wipe();
yield SessionFile.read(); // Reinitializes SessionFile
for (let k of Paths.loadOrder) {
ok(!(yield File.exists(Paths[k])), "After wipe " + k + " sessionstore file doesn't exist");
}
ok(!(yield File.exists(OLD_BACKUP)), "After wipe, old backup doesn't exist");
ok(!(yield File.exists(OLD_UPGRADE_BACKUP)), "After wipe, old upgrade backup doesn't exist");
let URL_BASE = "http://example.com/?atomic_backup_test_creation=" + Math.random();
let URL = URL_BASE + "?first_write";
let tab = gBrowser.addTab(URL);
info("Testing situation after a single write");
yield promiseBrowserLoaded(tab.linkedBrowser);
SyncHandlers.get(tab.linkedBrowser).flush();
yield SessionSaver.run();
ok((yield File.exists(Paths.recovery)), "After write, recovery sessionstore file exists again");
ok(!(yield File.exists(Paths.recoveryBackup)), "After write, recoveryBackup sessionstore doesn't exist");
ok((yield promiseRead(Paths.recovery)).indexOf(URL) != -1, "Recovery sessionstore file contains the required tab");
ok(!(yield File.exists(Paths.clean)), "After first write, clean shutdown sessionstore doesn't exist, since we haven't shutdown yet");
info("Testing situation after a second write");
let URL2 = URL_BASE + "?second_write";
tab.linkedBrowser.loadURI(URL2);
yield promiseBrowserLoaded(tab.linkedBrowser);
SyncHandlers.get(tab.linkedBrowser).flush();
yield SessionSaver.run();
ok((yield File.exists(Paths.recovery)), "After second write, recovery sessionstore file still exists");
ok((yield promiseRead(Paths.recovery)).indexOf(URL2) != -1, "Recovery sessionstore file contains the latest url");
ok((yield File.exists(Paths.recoveryBackup)), "After write, recoveryBackup sessionstore now exists");
let backup = yield promiseRead(Paths.recoveryBackup);
ok(backup.indexOf(URL2) == -1, "Recovery backup doesn't contain the latest url");
ok(backup.indexOf(URL) != -1, "Recovery backup contains the original url");
ok(!(yield File.exists(Paths.clean)), "After first write, clean shutdown sessinstore doesn't exist, since we haven't shutdown yet");
info("Reinitialize, ensure that we haven't leaked sensitive files");
yield SessionFile.read(); // Reinitializes SessionFile
yield SessionSaver.run();
ok(!(yield File.exists(Paths.clean)), "After second write, clean shutdown sessonstore doesn't exist, since we haven't shutdown yet");
ok(!(yield File.exists(Paths.upgradeBackup)), "After second write, clean shutdwn sessionstore doesn't exist, since we haven't shutdown yet");
ok(!(yield File.exists(Paths.nextUpgradeBackup)), "After second write, clean sutdown sessionstore doesn't exist, since we haven't shutdown yet");
gBrowser.removeTab(tab);
yield SessionFile.wipe();
});
let promiseSource = Task.async(function*(name) {
let URL = "http://example.com/?atomic_backup_test_recovery=" + Math.random() + "&name=" + name;
let tab = gBrowser.addTab(URL);
yield promiseBrowserLoaded(tab.linkedBrowser);
SyncHandlers.get(tab.linkedBrowser).flush();
yield SessionSaver.run();
gBrowser.removeTab(tab);
let SOURCE = yield promiseRead(Paths.recovery);
yield SessionFile.wipe();
return SOURCE;
});
add_task(function* test_recovery() {
yield SessionFile.wipe();
info("Attempting to recover from the recovery file");
let SOURCE = yield promiseSource("Paths.recovery");
// Ensure that we can recover from Paths.recovery
yield File.makeDir(Paths.backups);
yield File.writeAtomic(Paths.recovery, SOURCE);
is((yield SessionFile.read()).source, SOURCE, "Recovered the correct source from the recovery file");
yield SessionFile.wipe();
info("Corrupting recovery file, attempting to recover from recovery backup");
SOURCE = yield promiseSource("Paths.recoveryBackup");
yield File.makeDir(Paths.backups);
yield File.writeAtomic(Paths.recoveryBackup, SOURCE);
yield File.writeAtomic(Paths.recovery, "<Invalid JSON>");
is((yield SessionFile.read()).source, SOURCE, "Recovered the correct source from the recovery file");
});
add_task(function* test_clean() {
yield SessionFile.wipe();
let SOURCE = yield promiseSource("Paths.clean");
yield File.writeAtomic(Paths.clean, SOURCE);
yield SessionFile.read();
yield SessionSaver.run();
is((yield promiseRead(Paths.cleanBackup)), SOURCE, "After first read/write, clean shutdown file has been moved to cleanBackup");
});
add_task(function* cleanup() {
yield SessionFile.wipe();
});

View File

@ -34,10 +34,8 @@ add_task(function() {
SyncHandlers.get(tab2.linkedBrowser).flush();
info("Checking out state");
yield SessionSaver.run();
let path = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
let data = yield OS.File.read(path);
let state = new TextDecoder().decode(data);
let state = yield promiseRecoveryFileContents();
info("State: " + state);
// Ensure that sessionstore.js only knows about the public tab
ok(state.indexOf(URL_PUBLIC) != -1, "State contains public tab");

View File

@ -5,45 +5,36 @@ Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/osfile.jsm", this);
Cu.import("resource://gre/modules/Task.jsm", this);
function test() {
waitForExplicitFinish();
const Paths = SessionFile.Paths;
Task.spawn(function task() {
try {
// Wait until initialization is complete
yield SessionStore.promiseInitialized;
add_task(function* init() {
// Wait until initialization is complete
yield SessionStore.promiseInitialized;
yield SessionFile.wipe();
});
const PREF_UPGRADE = "browser.sessionstore.upgradeBackup.latestBuildID";
let buildID = Services.appinfo.platformBuildID;
add_task(function* test_upgrade_backup() {
const PREF_UPGRADE = "browser.sessionstore.upgradeBackup.latestBuildID";
let buildID = Services.appinfo.platformBuildID;
info("Let's check if we create an upgrade backup");
Services.prefs.setCharPref(PREF_UPGRADE, "");
let contents = JSON.stringify({"browser_upgrade_backup.js": Math.random()});
yield OS.File.writeAtomic(Paths.clean, contents);
yield SessionFile.read(); // First call to read() initializes the SessionWorker
yield SessionFile.write(""); // First call to write() triggers the backup
// Write state once before starting the test to
// ensure sessionstore.js writes won't happen in between.
yield forceSaveState();
is(Services.prefs.getCharPref(PREF_UPGRADE), buildID, "upgrade backup should be set");
// Force backup to take place with a file decided by us
Services.prefs.setCharPref(PREF_UPGRADE, "");
let contents = "browser_upgrade_backup.js";
let pathStore = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
yield OS.File.writeAtomic(pathStore, contents, { tmpPath: pathStore + ".tmp" });
yield SessionStore._internal._performUpgradeBackup();
is(Services.prefs.getCharPref(PREF_UPGRADE), buildID, "upgrade backup should be set (again)");
is((yield OS.File.exists(Paths.upgradeBackup)), true, "upgrade backup file has been created");
let pathBackup = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak-" + Services.appinfo.platformBuildID);
is((yield OS.File.exists(pathBackup)), true, "upgrade backup file has been created");
let data = yield OS.File.read(Paths.upgradeBackup);
is(contents, (new TextDecoder()).decode(data), "upgrade backup contains the expected contents");
let data = yield OS.File.read(pathBackup);
is(new TextDecoder().decode(data), contents, "upgrade backup contains the expected contents");
// Ensure that we don't re-backup by accident
yield OS.File.writeAtomic(pathStore, "something else entirely", { tmpPath: pathStore + ".tmp" });
yield SessionStore._internal._performUpgradeBackup();
data = yield OS.File.read(pathBackup);
is(new TextDecoder().decode(data), contents, "upgrade backup hasn't changed");
} catch (ex) {
ok(false, "Uncaught error: " + ex + " at " + ex.stack);
} finally {
finish();
}
});
}
info("Let's check that we don't overwrite this upgrade backup");
let new_contents = JSON.stringify({"something else entirely": Math.random()});
yield OS.File.writeAtomic(Paths.clean, new_contents);
yield SessionFile.read(); // Reinitialize the SessionWorker
yield SessionFile.write(""); // Next call to write() shouldn't trigger the backup
data = yield OS.File.read(Paths.upgradeBackup);
is(contents, (new TextDecoder()).decode(data), "upgrade backup hasn't changed");
});

View File

@ -39,9 +39,11 @@ registerCleanupFunction(() => {
let tmp = {};
Cu.import("resource://gre/modules/Promise.jsm", tmp);
Cu.import("resource://gre/modules/Task.jsm", tmp);
Cu.import("resource:///modules/sessionstore/SessionStore.jsm", tmp);
Cu.import("resource:///modules/sessionstore/SessionSaver.jsm", tmp);
let {Promise, SessionStore, SessionSaver} = tmp;
Cu.import("resource:///modules/sessionstore/SessionFile.jsm", tmp);
let {Promise, Task, SessionStore, SessionSaver, SessionFile} = tmp;
let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
@ -282,13 +284,26 @@ function forceSaveState() {
return SessionSaver.run();
}
function promiseSaveFileContents() {
function promiseRecoveryFileContents() {
let promise = forceSaveState();
return promise.then(function() {
return OS.File.read(OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"), { encoding: "utf-8" });
return OS.File.read(SessionFile.Paths.recovery, { encoding: "utf-8" });
});
}
let promiseForEachSessionRestoreFile = Task.async(function*(cb) {
for (let key of SessionFile.Paths.loadOrder) {
let data = "";
try {
data = yield OS.File.read(SessionFile.Paths[key], { encoding: "utf-8" });
} catch (ex if ex instanceof OS.File.Error
&& ex.becauseNoSuchFile) {
// Ignore missing files
}
cb(data, key);
}
});
function whenBrowserLoaded(aBrowser, aCallback = next, ignoreSubFrames = true) {
aBrowser.addEventListener("load", function onLoad(event) {
if (!ignoreSubFrames || event.target == aBrowser.contentDocument) {

View File

@ -5,29 +5,28 @@ let Ci = Components.interfaces;
Components.utils.import("resource://gre/modules/Services.jsm");
// Call a function once initialization of SessionStartup is complete
let afterSessionStartupInitialization =
function afterSessionStartupInitialization(cb) {
do_print("Waiting for session startup initialization");
let observer = function() {
try {
do_print("Session startup initialization observed");
Services.obs.removeObserver(observer, "sessionstore-state-finalized");
cb();
} catch (ex) {
do_throw(ex);
}
};
function afterSessionStartupInitialization(cb) {
do_print("Waiting for session startup initialization");
let observer = function() {
try {
do_print("Session startup initialization observed");
Services.obs.removeObserver(observer, "sessionstore-state-finalized");
cb();
} catch (ex) {
do_throw(ex);
}
};
// We need the Crash Monitor initialized for sessionstartup to run
// successfully.
Components.utils.import("resource://gre/modules/CrashMonitor.jsm");
CrashMonitor.init();
// We need the Crash Monitor initialized for sessionstartup to run
// successfully.
Components.utils.import("resource://gre/modules/CrashMonitor.jsm");
CrashMonitor.init();
// Start sessionstartup initialization.
let startup = Cc["@mozilla.org/browser/sessionstartup;1"].
getService(Ci.nsIObserver);
Services.obs.addObserver(startup, "final-ui-startup", false);
Services.obs.addObserver(startup, "quit-application", false);
Services.obs.notifyObservers(null, "final-ui-startup", "");
Services.obs.addObserver(observer, "sessionstore-state-finalized", false);
// Start sessionstartup initialization.
let startup = Cc["@mozilla.org/browser/sessionstartup;1"].
getService(Ci.nsIObserver);
Services.obs.addObserver(startup, "final-ui-startup", false);
Services.obs.addObserver(startup, "quit-application", false);
Services.obs.notifyObservers(null, "final-ui-startup", "");
Services.obs.addObserver(observer, "sessionstore-state-finalized", false);
};

View File

@ -1,42 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let toplevel = this;
Cu.import("resource://gre/modules/osfile.jsm");
function run_test() {
do_get_profile();
Cu.import("resource:///modules/sessionstore/SessionFile.jsm", toplevel);
pathStore = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
run_next_test();
}
let pathStore;
function pathBackup(ext) {
return OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak" + ext);
}
// Ensure that things proceed smoothly if there is no file to back up
add_task(function test_nothing_to_backup() {
yield SessionFile.createBackupCopy("");
});
// Create a file, back it up, remove it
add_task(function test_do_backup() {
let content = "test_1";
let ext = ".upgrade_test_1";
yield OS.File.writeAtomic(pathStore, content, {tmpPath: pathStore + ".tmp"});
do_print("Ensuring that the backup is created");
yield SessionFile.createBackupCopy(ext);
do_check_true((yield OS.File.exists(pathBackup(ext))));
let data = yield OS.File.read(pathBackup(ext));
do_check_eq((new TextDecoder()).decode(data), content);
do_print("Ensuring that we can remove the backup");
yield SessionFile.removeBackupCopy(ext);
do_check_false((yield OS.File.exists(pathBackup(ext))));
});

View File

@ -1,48 +1,151 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let toplevel = this;
Cu.import("resource://gre/modules/osfile.jsm");
"use strict";
let {OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
let {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
let {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
let {SessionWorker} = Cu.import("resource:///modules/sessionstore/SessionWorker.jsm", {});
let File = OS.File;
let Paths;
let SessionFile;
// We need a XULAppInfo to initialize SessionFile
let (XULAppInfo = {
vendor: "Mozilla",
name: "SessionRestoreTest",
ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}",
version: "1",
appBuildID: "2007010101",
platformVersion: "",
platformBuildID: "2007010101",
inSafeMode: false,
logConsoleErrors: true,
OS: "XPCShell",
XPCOMABI: "noarch-spidermonkey",
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIXULAppInfo,
Ci.nsIXULRuntime,
])
}) {
let XULAppInfoFactory = {
createInstance: function (outer, iid) {
if (outer != null)
throw Cr.NS_ERROR_NO_AGGREGATION;
return XULAppInfo.QueryInterface(iid);
}
};
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
registrar.registerFactory(Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}"),
"XULAppInfo", "@mozilla.org/xre/app-info;1",
XULAppInfoFactory);
};
function run_test() {
let profd = do_get_profile();
Cu.import("resource:///modules/sessionstore/SessionFile.jsm", toplevel);
decoder = new TextDecoder();
pathStore = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
pathBackup = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak");
let source = do_get_file("data/sessionstore_valid.js");
source.copyTo(profd, "sessionstore.js");
run_next_test();
}
add_task(function* init() {
// Make sure that we have a profile before initializing SessionFile
let profd = do_get_profile();
SessionFile = Cu.import("resource:///modules/sessionstore/SessionFile.jsm", {}).SessionFile;
Paths = SessionFile.Paths;
let source = do_get_file("data/sessionstore_valid.js");
source.copyTo(profd, "sessionstore.js");
// Finish initialization of SessionFile
yield SessionFile.read();
});
let pathStore;
let pathBackup;
let decoder;
// Write to the store, and check that a backup is created first
add_task(function test_first_write_backup() {
let content = "test_1";
let initial_content = decoder.decode(yield OS.File.read(pathStore));
function promise_check_exist(path, shouldExist) {
return Task.spawn(function*() {
do_print("Ensuring that " + path + (shouldExist?" exists":" does not exist"));
if ((yield OS.File.exists(path)) != shouldExist) {
throw new Error("File " + path + " should " + (shouldExist?"exist":"not exist"));
}
});
}
do_check_true(!(yield OS.File.exists(pathBackup)));
yield SessionFile.write(content);
do_check_true(yield OS.File.exists(pathBackup));
function promise_check_contents(path, expect) {
return Task.spawn(function*() {
do_print("Checking whether " + path + " has the right contents");
let actual = yield OS.File.read(path, { encoding: "utf-8"});
if (actual != expect) {
throw new Error("File " + path + " should contain\n\t" + expect + "\nbut contains " + actual);
}
});
}
let backup_content = decoder.decode(yield OS.File.read(pathBackup));
do_check_eq(initial_content, backup_content);
// Write to the store, and check that it creates:
// - $Path.recovery with the new data
// - $Path.nextUpgradeBackup with the old data
add_task(function* test_first_write_backup() {
let initial_content = "initial content " + Math.random();
let new_content = "test_1 " + Math.random();
do_print("Before the first write, none of the files should exist");
yield promise_check_exist(Paths.backups, false);
yield File.makeDir(Paths.backups);
yield File.writeAtomic(Paths.clean, initial_content, { encoding: "utf-8" });
yield SessionFile.write(new_content);
do_print("After first write, a few files should have been created");
yield promise_check_exist(Paths.backups, true);
yield promise_check_exist(Paths.clean, false);
yield promise_check_exist(Paths.cleanBackup, true);
yield promise_check_exist(Paths.recovery, true);
yield promise_check_exist(Paths.recoveryBackup, false);
yield promise_check_exist(Paths.nextUpgradeBackup, true);
yield promise_check_contents(Paths.recovery, new_content);
yield promise_check_contents(Paths.nextUpgradeBackup, initial_content);
});
// Write to the store again, and check that the backup is not updated
add_task(function test_second_write_no_backup() {
let content = "test_2";
let initial_content = decoder.decode(yield OS.File.read(pathStore));
let initial_backup_content = decoder.decode(yield OS.File.read(pathBackup));
// Write to the store again, and check that
// - $Path.clean is not written
// - $Path.recovery contains the new data
// - $Path.recoveryBackup contains the previous data
add_task(function* test_second_write_no_backup() {
let new_content = "test_2 " + Math.random();
let previous_backup_content = yield File.read(Paths.recovery, { encoding: "utf-8" });
yield SessionFile.write(content);
yield OS.File.remove(Paths.cleanBackup);
let written_content = decoder.decode(yield OS.File.read(pathStore));
do_check_eq(content, written_content);
yield SessionFile.write(new_content);
yield promise_check_exist(Paths.backups, true);
yield promise_check_exist(Paths.clean, false);
yield promise_check_exist(Paths.cleanBackup, false);
yield promise_check_exist(Paths.recovery, true);
yield promise_check_exist(Paths.nextUpgradeBackup, true);
yield promise_check_contents(Paths.recovery, new_content);
yield promise_check_contents(Paths.recoveryBackup, previous_backup_content);
});
// Make sure that we create $Paths.clean and remove $Paths.recovery*
// upon shutdown
add_task(function* test_shutdown() {
let output = "test_3 " + Math.random();
yield File.writeAtomic(Paths.recovery, "I should disappear");
yield File.writeAtomic(Paths.recoveryBackup, "I should also disappear");
yield SessionWorker.post("write", [output, { isFinalWrite: true, performShutdownCleanup: true}]);
do_check_false((yield File.exists(Paths.recovery)));
do_check_false((yield File.exists(Paths.recoveryBackup)));
let input = yield File.read(Paths.clean, { encoding: "utf-8"});
do_check_eq(input, output);
let backup_content = decoder.decode(yield OS.File.read(pathBackup));
do_check_eq(initial_backup_content, backup_content);
});

View File

@ -7,7 +7,6 @@ support-files =
data/sessionstore_invalid.js
data/sessionstore_valid.js
[test_backup.js]
[test_backup_once.js]
[test_startup_nosession_async.js]
[test_startup_session_async.js]

View File

@ -124,6 +124,16 @@ TranslationUI.prototype = {
return;
}
if (this.state == Translation.STATE_OFFER) {
if (this.detectedLanguage != aFrom)
TranslationHealthReport.recordDetectedLanguageChange(true);
} else {
if (this.translatedFrom != aFrom)
TranslationHealthReport.recordDetectedLanguageChange(false);
if (this.translatedTo != aTo)
TranslationHealthReport.recordTargetLanguageChange();
}
this.state = Translation.STATE_TRANSLATING;
this.translatedFrom = aFrom;
this.translatedTo = aTo;
@ -184,6 +194,7 @@ TranslationUI.prototype = {
this.originalShown = true;
this.showURLBarIcon();
this.browser.messageManager.sendAsyncMessage("Translation:ShowOriginal");
TranslationHealthReport.recordShowOriginalContent();
},
showTranslatedContent: function() {
@ -255,6 +266,11 @@ TranslationUI.prototype = {
}
break;
}
},
infobarClosed: function() {
if (this.state == Translation.STATE_OFFER)
TranslationHealthReport.recordDeniedTranslationOffer();
}
};
@ -297,7 +313,7 @@ let TranslationHealthReport = {
/**
* Record a change of the detected language in the health report. This should
* only be called when actually executing a translation not every time the
* only be called when actually executing a translation, not every time the
* user changes in the language in the UI.
*
* @param beforeFirstTranslation
@ -307,8 +323,17 @@ let TranslationHealthReport = {
* the user has manually adjusted the detected language false should
* be passed.
*/
recordLanguageChange: function (beforeFirstTranslation) {
this._withProvider(provider => provider.recordLanguageChange(beforeFirstTranslation));
recordDetectedLanguageChange: function (beforeFirstTranslation) {
this._withProvider(provider => provider.recordDetectedLanguageChange(beforeFirstTranslation));
},
/**
* Record a change of the target language in the health report. This should
* only be called when actually executing a translation, not every time the
* user changes in the language in the UI.
*/
recordTargetLanguageChange: function () {
this._withProvider(provider => provider.recordTargetLanguageChange());
},
/**
@ -318,6 +343,13 @@ let TranslationHealthReport = {
this._withProvider(provider => provider.recordDeniedTranslationOffer());
},
/**
* Record a "Show Original" command use.
*/
recordShowOriginalContent: function () {
this._withProvider(provider => provider.recordShowOriginalContent());
},
/**
* Retrieve the translation provider and pass it to the given function.
*
@ -376,7 +408,9 @@ TranslationMeasurement1.prototype = Object.freeze({
pageTranslatedCountsByLanguage: DAILY_LAST_TEXT_FIELD,
detectedLanguageChangedBefore: DAILY_COUNTER_FIELD,
detectedLanguageChangedAfter: DAILY_COUNTER_FIELD,
targetLanguageChanged: DAILY_COUNTER_FIELD,
deniedTranslationOffer: DAILY_COUNTER_FIELD,
showOriginalContent: DAILY_COUNTER_FIELD,
detectLanguageEnabled: DAILY_LAST_NUMERIC_FIELD,
showTranslationUI: DAILY_LAST_NUMERIC_FIELD,
},
@ -500,7 +534,7 @@ TranslationProvider.prototype = Object.freeze({
}.bind(this));
},
recordLanguageChange: function (beforeFirstTranslation) {
recordDetectedLanguageChange: function (beforeFirstTranslation) {
let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
TranslationMeasurement1.prototype.version);
@ -513,6 +547,15 @@ TranslationProvider.prototype = Object.freeze({
}.bind(this));
},
recordTargetLanguageChange: function () {
let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
TranslationMeasurement1.prototype.version);
return this._enqueueTelemetryStorageTask(function* recordTask() {
yield m.incrementDailyCounter("targetLanguageChanged");
}.bind(this));
},
recordDeniedTranslationOffer: function () {
let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
TranslationMeasurement1.prototype.version);
@ -522,6 +565,15 @@ TranslationProvider.prototype = Object.freeze({
}.bind(this));
},
recordShowOriginalContent: function () {
let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
TranslationMeasurement1.prototype.version);
return this._enqueueTelemetryStorageTask(function* recordTask() {
yield m.incrementDailyCounter("showOriginalContent");
}.bind(this));
},
collectDailyData: function () {
let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
TranslationMeasurement1.prototype.version);

View File

@ -85,8 +85,15 @@ TranslationContentHandler.prototype = {
return;
LanguageDetector.detectLanguage(string).then(result => {
if (!result.confident)
// Bail if we're not confident.
if (!result.confident) {
return;
}
// The window might be gone by now.
if (Cu.isDeadWrapper(content)) {
return;
}
content.detectedLanguage = result.language;

View File

@ -7,6 +7,62 @@ let tmp = {};
Cu.import("resource:///modules/translation/Translation.jsm", tmp);
let {Translation} = tmp;
let MetricsChecker = {
_metricsTime: new Date(),
_midnightError: new Error("Getting metrics around midnight may fail sometimes"),
updateMetrics: Task.async(function* () {
let svc = Cc["@mozilla.org/datareporting/service;1"].getService();
let reporter = svc.wrappedJSObject.healthReporter;
yield reporter.onInit();
// Get the provider.
let provider = reporter.getProvider("org.mozilla.translation");
let measurement = provider.getMeasurement("translation", 1);
let values = yield measurement.getValues();
let metricsTime = new Date();
let day = values.days.getDay(metricsTime);
if (!day) {
// This should never happen except when the test runs at midnight.
throw this._midnightError;
}
// .get() may return `undefined`, which we can't compute.
this._metrics = {
pageCount: day.get("pageTranslatedCount") || 0,
charCount: day.get("charactersTranslatedCount") || 0,
deniedOffers: day.get("deniedTranslationOffer") || 0,
showOriginal: day.get("showOriginalContent") || 0,
detectedLanguageChangedBefore: day.get("detectedLanguageChangedBefore") || 0,
detectedLanguageChangeAfter: day.get("detectedLanguageChangedAfter") || 0,
targetLanguageChanged: day.get("targetLanguageChanged") || 0
};
this._metricsTime = metricsTime;
}),
checkAdditions: Task.async(function* (additions) {
let prevMetrics = this._metrics, prevMetricsTime = this._metricsTime;
try {
yield this.updateMetrics();
} catch(ex if ex == this._midnightError) {
return;
}
// Check that it's still the same day of the month as when we started. This
// prevents intermittent failures when the test starts before and ends after
// midnight.
if (this._metricsTime.getDate() != prevMetricsTime.getDate()) {
for (let metric of Object.keys(prevMetrics)) {
prevMetrics[metric] = 0;
}
}
for (let metric of Object.keys(additions)) {
Assert.equal(prevMetrics[metric] + additions[metric], this._metrics[metric]);
}
})
};
add_task(function* setup() {
Services.prefs.setBoolPref("toolkit.telemetry.enabled", true);
Services.prefs.setBoolPref("browser.translation.detectLanguage", true);
@ -17,53 +73,100 @@ add_task(function* setup() {
Services.prefs.clearUserPref("browser.translation.detectLanguage");
Services.prefs.clearUserPref("browser.translation.ui.show");
});
// Make sure there are some initial metrics in place when the test starts.
yield translate("<h1>Hallo Welt!</h1>", "de");
yield MetricsChecker.updateMetrics();
});
add_task(function* test_fhr() {
let start = new Date();
// Translate a page.
yield translate("<h1>Hallo Welt!</h1>", "de", "en");
let [pageCount, charCount] = yield retrieveTranslationCounts();
yield translate("<h1>Hallo Welt!</h1>", "de");
// Translate another page.
yield translate("<h1>Hallo Welt!</h1><h1>Bratwurst!</h1>", "de", "en");
let [pageCount2, charCount2] = yield retrieveTranslationCounts();
// Check that it's still the same day of the month as when we started. This
// prevents intermittent failures when the test starts before and ends after
// midnight.
if (start.getDate() == new Date().getDate()) {
Assert.equal(pageCount2, pageCount + 1);
Assert.equal(charCount2, charCount + 21);
}
yield translate("<h1>Hallo Welt!</h1><h1>Bratwurst!</h1>", "de");
yield MetricsChecker.checkAdditions({ pageCount: 1, charCount: 21, deniedOffers: 0});
});
function retrieveTranslationCounts() {
return Task.spawn(function* task_retrieve_counts() {
let svc = Cc["@mozilla.org/datareporting/service;1"].getService();
let reporter = svc.wrappedJSObject.healthReporter;
yield reporter.onInit();
add_task(function* test_deny_translation_metric() {
function* offerAndDeny(elementAnonid) {
let tab = yield offerTranslatationFor("<h1>Hallo Welt!</h1>", "de", "en");
getInfobarElement(tab.linkedBrowser, elementAnonid).doCommand();
yield MetricsChecker.checkAdditions({ deniedOffers: 1 });
gBrowser.removeTab(tab);
}
// Get the provider.
let provider = reporter.getProvider("org.mozilla.translation");
let measurement = provider.getMeasurement("translation", 1);
let values = yield measurement.getValues();
yield offerAndDeny("notNow");
yield offerAndDeny("neverForSite");
yield offerAndDeny("neverForLanguage");
yield offerAndDeny("closeButton");
let day = values.days.getDay(new Date());
if (!day) {
// This should never happen except when the test runs at midnight.
return [0, 0];
}
// Test that the close button doesn't record a denied translation if
// the infobar is not in its "offer" state.
let tab = yield translate("<h1>Hallo Welt!</h1>", "de", false);
yield MetricsChecker.checkAdditions({ deniedOffers: 0 });
gBrowser.removeTab(tab);
});
// .get() may return `undefined`, which we can't compute.
return [day.get("pageTranslatedCount") || 0, day.get("charactersTranslatedCount") || 0];
add_task(function* test_show_original() {
let tab =
yield translate("<h1>Hallo Welt!</h1><h1>Bratwurst!</h1>", "de", false);
yield MetricsChecker.checkAdditions({ pageCount: 1, showOriginal: 0 });
getInfobarElement(tab.linkedBrowser, "showOriginal").doCommand();
yield MetricsChecker.checkAdditions({ pageCount: 0, showOriginal: 1 });
gBrowser.removeTab(tab);
});
add_task(function* test_language_change() {
for (let i of Array(4)) {
let tab = yield offerTranslatationFor("<h1>Hallo Welt!</h1>", "fr");
let browser = tab.linkedBrowser;
// In the offer state, translation is executed by the Translate button,
// so we expect just a single recoding.
let detectedLangMenulist = getInfobarElement(browser, "detectedLanguage");
simulateUserSelectInMenulist(detectedLangMenulist, "de");
simulateUserSelectInMenulist(detectedLangMenulist, "it");
simulateUserSelectInMenulist(detectedLangMenulist, "de");
yield acceptTranslationOffer(tab);
// In the translated state, a change in the form or to menulists
// triggers re-translation right away.
let fromLangMenulist = getInfobarElement(browser, "fromLanguage");
simulateUserSelectInMenulist(fromLangMenulist, "it");
simulateUserSelectInMenulist(fromLangMenulist, "de");
// Selecting the same item shouldn't count.
simulateUserSelectInMenulist(fromLangMenulist, "de");
let toLangMenulist = getInfobarElement(browser, "toLanguage");
simulateUserSelectInMenulist(toLangMenulist, "fr");
simulateUserSelectInMenulist(toLangMenulist, "en");
simulateUserSelectInMenulist(toLangMenulist, "it");
// Selecting the same item shouldn't count.
simulateUserSelectInMenulist(toLangMenulist, "it");
// Setting the target language to the source language is a no-op,
// so it shouldn't count.
simulateUserSelectInMenulist(toLangMenulist, "de");
gBrowser.removeTab(tab);
}
yield MetricsChecker.checkAdditions({
detectedLanguageChangedBefore: 4,
detectedLanguageChangeAfter: 8,
targetLanguageChanged: 12
});
});
function getInfobarElement(browser, anonid) {
let notif = browser.translationUI
.notificationBox.getNotificationWithValue("translation");
return notif._getAnonElt(anonid);
}
function translate(text, from, to) {
return Task.spawn(function* task_translate() {
function offerTranslatationFor(text, from) {
return Task.spawn(function* task_offer_translation() {
// Create some content to translate.
let tab = gBrowser.selectedTab =
gBrowser.addTab("data:text/html;charset=utf-8," + text);
@ -77,12 +180,27 @@ function translate(text, from, to) {
originalShown: true,
detectedLanguage: from});
// Translate the page.
browser.translationUI.translate(from, to);
yield waitForMessage(browser, "Translation:Finished");
return tab;
});
}
// Cleanup.
gBrowser.removeTab(tab);
function acceptTranslationOffer(tab) {
return Task.spawn(function* task_accept_translation_offer() {
let browser = tab.linkedBrowser;
getInfobarElement(browser, "translate").doCommand();
yield waitForMessage(browser, "Translation:Finished");
});
}
function translate(text, from, closeTab = true) {
return Task.spawn(function* task_translate() {
let tab = yield offerTranslatationFor(text, from);
yield acceptTranslationOffer(tab);
if (closeTab) {
gBrowser.removeTab(tab);
} else {
return tab;
}
});
}
@ -105,3 +223,8 @@ function promiseBrowserLoaded(browser) {
}, true);
});
}
function simulateUserSelectInMenulist(menulist, value) {
menulist.value = value;
menulist.doCommand();
}

View File

@ -173,13 +173,16 @@ add_task(function* test_record_translation() {
yield provider.init(storage);
let now = new Date();
// Record a language change before translation.
yield provider.recordLanguageChange(true);
// Record a change to the source language changes before translation.
yield provider.recordDetectedLanguageChange(true);
// Record two language changes after translation.
yield provider.recordLanguageChange(false);
yield provider.recordLanguageChange(false);
// Record two changes to the source language changes after translation.
yield provider.recordDetectedLanguageChange(false);
yield provider.recordDetectedLanguageChange(false);
// Record two changes to the target language.
yield provider.recordTargetLanguageChange();
yield provider.recordTargetLanguageChange();
let m = provider.getMeasurement("translation", 1);
let values = yield m.getValues();
@ -189,21 +192,24 @@ add_task(function* test_record_translation() {
Assert.ok(day.has("detectedLanguageChangedBefore"));
Assert.equal(day.get("detectedLanguageChangedBefore"), 1);
Assert.ok(day.has("detectedLanguageChangedAfter"));
Assert.equal(day.get("detectedLanguageChangedAfter"), 2);
Assert.ok(day.has("targetLanguageChanged"));
Assert.equal(day.get("targetLanguageChanged"), 2);
yield provider.shutdown();
yield storage.close();
});
add_task(function* test_denied_translation_offer() {
function* test_simple_counter(aProviderFuncName, aCounterName) {
let storage = yield Metrics.Storage("translation");
let provider = new TranslationProvider();
yield provider.init(storage);
let now = new Date();
yield provider.recordDeniedTranslationOffer();
yield provider.recordDeniedTranslationOffer();
yield provider[aProviderFuncName]();
yield provider[aProviderFuncName]();
let m = provider.getMeasurement("translation", 1);
let values = yield m.getValues();
@ -211,11 +217,19 @@ add_task(function* test_denied_translation_offer() {
Assert.ok(values.days.hasDay(now));
let day = values.days.getDay(now);
Assert.ok(day.has("deniedTranslationOffer"));
Assert.equal(day.get("deniedTranslationOffer"), 2);
Assert.ok(day.has(aCounterName));
Assert.equal(day.get(aCounterName), 2);
yield provider.shutdown();
yield storage.close();
}
add_task(function* test_denied_translation_offer() {
yield test_simple_counter("recordDeniedTranslationOffer", "deniedTranslationOffer");
});
add_task(function* test_show_original() {
yield test_simple_counter("recordShowOriginalContent", "showOriginalContent");
});
add_task(function* test_collect_daily() {
@ -268,15 +282,17 @@ add_task(function* test_healthreporter_json() {
yield reporter._providerManager.registerProvider(provider);
yield provider.recordTranslationOpportunity("fr", now);
yield provider.recordLanguageChange(true);
yield provider.recordDetectedLanguageChange(true);
yield provider.recordTranslation("fr", "en", 1000, now);
yield provider.recordLanguageChange(false);
yield provider.recordDetectedLanguageChange(false);
yield provider.recordTranslationOpportunity("es", now);
yield provider.recordTranslation("es", "en", 1000, now);
yield provider.recordDeniedTranslationOffer();
yield provider.recordShowOriginalContent();
yield reporter.collectMeasurements();
let payload = yield reporter.getJSONPayload(true);
let today = reporter._formatDate(now);
@ -312,6 +328,9 @@ add_task(function* test_healthreporter_json() {
Assert.ok("deniedTranslationOffer" in translations);
Assert.equal(translations["deniedTranslationOffer"], 1);
Assert.ok("showOriginalContent" in translations);
Assert.equal(translations["showOriginalContent"], 1);
} finally {
reporter._shutdown();
}
@ -329,15 +348,17 @@ add_task(function* test_healthreporter_json2() {
yield reporter._providerManager.registerProvider(provider);
yield provider.recordTranslationOpportunity("fr", now);
yield provider.recordLanguageChange(true);
yield provider.recordDetectedLanguageChange(true);
yield provider.recordTranslation("fr", "en", 1000, now);
yield provider.recordLanguageChange(false);
yield provider.recordDetectedLanguageChange(false);
yield provider.recordTranslationOpportunity("es", now);
yield provider.recordTranslation("es", "en", 1000, now);
yield provider.recordDeniedTranslationOffer();
yield provider.recordShowOriginalContent();
yield reporter.collectMeasurements();
let payload = yield reporter.getJSONPayload(true);
let today = reporter._formatDate(now);
@ -357,6 +378,7 @@ add_task(function* test_healthreporter_json2() {
Assert.ok(!("detectedLanguageChangedBefore" in translations));
Assert.ok(!("detectedLanguageChangedAfter" in translations));
Assert.ok(!("deniedTranslationOffer" in translations));
Assert.ok(!("showOriginalContent" in translations));
} finally {
reporter._shutdown();
}

View File

@ -38,7 +38,7 @@
oncommand="document.getBindingParent(this).translate();"/>
<xul:button class="translate-infobar-element"
label="&translation.notNow.button;" anonid="notNow"
oncommand="document.getBindingParent(this).close();"/>
oncommand="document.getBindingParent(this).closeCommand();"/>
</xul:hbox>
<!-- translating -->
@ -122,10 +122,11 @@
</xul:hbox>
<xul:toolbarbutton ondblclick="event.stopPropagation();"
anonid="closeButton"
class="messageCloseButton close-icon tabbable"
xbl:inherits="hidden=hideclose"
tooltiptext="&closeNotification.tooltip;"
oncommand="document.getBindingParent(this).close();"/>
oncommand="document.getBindingParent(this).closeCommand();"/>
</xul:hbox>
</content>
<implementation>
@ -216,6 +217,16 @@
</body>
</method>
<!-- To be called when the infobar should be closed per user's wish (e.g.
by clicking the notification's close button -->
<method name="closeCommand">
<body>
<![CDATA[
this.close();
this.translation.infobarClosed();
]]>
</body>
</method>
<method name="_handleButtonHiding">
<body>
<![CDATA[
@ -301,7 +312,7 @@
Services.prefs.setCharPref(kPrefName, val);
this.close();
this.closeCommand();
]]>
</body>
</method>
@ -313,7 +324,7 @@
let perms = Services.perms;
perms.add(uri, "translate", perms.DENY_ACTION);
this.close();
this.closeCommand();
]]>
</body>
</method>

View File

@ -210,7 +210,7 @@ exports.getHighlighterUtils = function(toolbox) {
yield toolbox.walker.highlight(nodeFront);
}
toolbox.emit("node-highlight", nodeFront);
toolbox.emit("node-highlight", nodeFront, options.toSource());
});
/**

View File

@ -7,6 +7,9 @@ support-files =
head.js
[browser_layoutview.js]
[browser_layoutview_guides.js]
# browser_layoutview_guides.js should be re-enabled when bug 1029451 is fixed.
skip-if = true
[browser_layoutview_rotate-labels-on-sides.js]
[browser_layoutview_update-after-navigation.js]
[browser_layoutview_update-after-reload.js]

View File

@ -0,0 +1,82 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let test = asyncTest(function*() {
let style = "div { position: absolute; top: 50px; left: 50px; height: 10px; " +
"width: 10px; border: 10px solid black; padding: 10px; margin: 10px;}";
let html = "<style>" + style + "</style><div></div>";
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(html));
let {inspector, view} = yield openLayoutView();
yield selectNode("div", inspector);
yield runTests(inspector, view);
});
addTest("Test that the initial values of the box model are correct",
function*(inspector, view) {
yield testGuideOnLayoutHover("margins", {
top: { x1: "0", x2: "100%", y1: "119.5", y2: "119.5" },
right: { x1: "50.5", x2: "50.5", y1: "0", y2: "100%" },
bottom: { x1: "0", x2: "100%", y1: "50.5", y2: "50.5" },
left: { x1: "119.5", x2: "119.5", y1: "0", y2: "100%" }
}, inspector, view);
yield testGuideOnLayoutHover("borders", {
top: { x1: "0", x2: "100%", y1: "109.5", y2: "109.5" },
right: { x1: "60.5", x2: "60.5", y1: "0", y2: "100%" },
bottom: { x1: "0", x2: "100%", y1: "60.5", y2: "60.5" },
left: { x1: "109.5", x2: "109.5", y1: "0", y2: "100%" }
}, inspector, view);
yield testGuideOnLayoutHover("padding", {
top: { x1: "0", x2: "100%", y1: "99.5", y2: "99.5" },
right: { x1: "70.5", x2: "70.5", y1: "0", y2: "100%" },
bottom: { x1: "0", x2: "100%", y1: "70.5", y2: "70.5" },
left: { x1: "99.5", x2: "99.5", y1: "0", y2: "100%" }
}, inspector, view);
yield testGuideOnLayoutHover("content", {
top: { x1: "0", x2: "100%", y1: "79.5", y2: "79.5" },
right: { x1: "90.5", x2: "90.5", y1: "0", y2: "100%" },
bottom: { x1: "0", x2: "100%", y1: "90.5", y2: "90.5" },
left: { x1: "79.5", x2: "79.5", y1: "0", y2: "100%" }
}, inspector, view);
gBrowser.removeCurrentTab();
});
function* testGuideOnLayoutHover(id, expected, inspector, view) {
info("Checking " + id);
let elt = view.doc.getElementById(id);
EventUtils.synthesizeMouse(elt, 2, 2, {type:'mouseover'}, view.doc.defaultView);
yield inspector.toolbox.once("highlighter-ready");
let guideTop = getGuideStatus("top");
let guideRight = getGuideStatus("right");
let guideBottom = getGuideStatus("bottom");
let guideLeft = getGuideStatus("left");
is(guideTop.x1, expected.top.x1, "top x1 is correct");
is(guideTop.x2, expected.top.x2, "top x2 is correct");
is(guideTop.y1, expected.top.y1, "top y1 is correct");
is(guideTop.y2, expected.top.y2, "top y2 is correct");
is(guideRight.x1, expected.right.x1, "right x1 is correct");
is(guideRight.x2, expected.right.x2, "right x2 is correct");
is(guideRight.y1, expected.right.y1, "right y1 is correct");
is(guideRight.y2, expected.right.y2, "right y2 is correct");
is(guideBottom.x1, expected.bottom.x1, "bottom x1 is correct");
is(guideBottom.x2, expected.bottom.x2, "bottom x2 is correct");
is(guideBottom.y1, expected.bottom.y1, "bottom y1 is correct");
is(guideBottom.y2, expected.bottom.y2, "bottom y2 is correct");
is(guideLeft.x1, expected.left.x1, "left x1 is correct");
is(guideLeft.x2, expected.left.x2, "left x2 is correct");
is(guideLeft.y1, expected.left.y1, "left y1 is correct");
is(guideLeft.y2, expected.left.y2, "left y2 is correct");
}

View File

@ -216,6 +216,28 @@ function waitForUpdate(inspector) {
return inspector.once("layoutview-updated");
}
function getHighlighter() {
return gBrowser.selectedBrowser.parentNode.querySelector(".highlighter-container");
}
function getBoxModelRoot() {
let highlighter = getHighlighter();
return highlighter.querySelector(".box-model-root");
}
function getGuideStatus(location) {
let root = getBoxModelRoot();
let guide = root.querySelector(".box-model-guide-" + location);
return {
visible: !guide.hasAttribute("hidden"),
x1: guide.getAttribute("x1"),
y1: guide.getAttribute("y1"),
x2: guide.getAttribute("x2"),
y2: guide.getAttribute("y2")
};
}
/**
* The addTest/runTests function couple provides a simple way to define
* subsequent test cases in a test file. Example:

View File

@ -455,7 +455,7 @@
@BINPATH@/components/FormHistoryStartup.js
@BINPATH@/components/nsInputListAutoComplete.js
@BINPATH@/components/formautofill.manifest
@BINPATH@/components/AutofillController.js
@BINPATH@/components/FormAutofillContentService.js
@BINPATH@/components/contentSecurityPolicy.manifest
@BINPATH@/components/contentSecurityPolicy.js
@BINPATH@/components/contentAreaDropListener.manifest

View File

@ -4,6 +4,8 @@
<!ENTITY prefWindow.titleWin "Options">
<!ENTITY prefWindow.title "Preferences">
<!-- LOCALIZATION NOTE (prefWindow.titleGNOME): This is not used for in-content preferences -->
<!ENTITY prefWindow.titleGNOME "&brandShortName; Preferences">
<!-- When making changes to prefWindow.styleWin test both Windows Classic and
Luna since widget heights are different based on the OS theme -->

View File

@ -29,7 +29,7 @@
#include "nsAutoPtr.h"
#include "nsTArray.h"
#include "nsIMutableArray.h"
#include "nsIAutofillController.h"
#include "nsIFormAutofillContentService.h"
// form submission
#include "nsIFormSubmitObserver.h"
@ -306,10 +306,12 @@ void
HTMLFormElement::RequestAutocomplete()
{
bool dummy;
nsCOMPtr<nsIDOMWindow> win = do_QueryInterface(OwnerDoc()->GetScriptHandlingObject(dummy));
nsCOMPtr<nsIAutofillController> controller(do_GetService("@mozilla.org/autofill-controller;1"));
nsCOMPtr<nsIDOMWindow> window =
do_QueryInterface(OwnerDoc()->GetScriptHandlingObject(dummy));
nsCOMPtr<nsIFormAutofillContentService> formAutofillContentService =
do_GetService("@mozilla.org/formautofill/content-service;1");
if (!controller || !win) {
if (!formAutofillContentService || !window) {
AutocompleteErrorEventInit init;
init.mBubbles = true;
init.mCancelable = false;
@ -317,11 +319,12 @@ HTMLFormElement::RequestAutocomplete()
nsRefPtr<AutocompleteErrorEvent> event =
AutocompleteErrorEvent::Constructor(this, NS_LITERAL_STRING("autocompleteerror"), init);
(new AsyncEventDispatcher(this, event))->PostDOMEvent();
return;
}
controller->RequestAutocomplete(this, win);
formAutofillContentService->RequestAutocomplete(this, window);
}
bool

View File

@ -51,6 +51,7 @@ BluetoothUuidHelper::GetBluetoothServiceClass(uint16_t aServiceUuid)
BluetoothServiceClass retValue = BluetoothServiceClass::UNKNOWN;
switch (aServiceUuid) {
case BluetoothServiceClass::A2DP:
case BluetoothServiceClass::A2DP_SINK:
case BluetoothServiceClass::HANDSFREE:
case BluetoothServiceClass::HANDSFREE_AG:
case BluetoothServiceClass::HEADSET:

View File

@ -25,6 +25,7 @@ class BluetoothProfileManagerBase;
enum BluetoothServiceClass
{
A2DP = 0x110D,
A2DP_SINK = 0x110B,
HANDSFREE = 0x111E,
HANDSFREE_AG = 0x111F,
HEADSET = 0x1108,

View File

@ -42,6 +42,12 @@
} \
} while(0)
#define MAX_UUID_SIZE 16
// Audio: Major service class = 0x100 (Bit 21 is set)
#define SET_AUDIO_BIT(cod) (cod |= 0x200000)
// Rendering: Major service class = 0x20 (Bit 18 is set)
#define SET_RENDERING_BIT(cod) (cod |= 0x40000)
using namespace mozilla;
using namespace mozilla::ipc;
USING_BLUETOOTH_NAMESPACE
@ -482,6 +488,7 @@ RemoteDevicePropertiesCallback(bt_status_t aStatus, bt_bdaddr_t *aBdAddress,
BdAddressTypeToString(aBdAddress, remoteDeviceBdAddress);
BT_APPEND_NAMED_VALUE(props, "Address", remoteDeviceBdAddress);
bool isCodInvalid = false;
for (int i = 0; i < aNumProperties; ++i) {
bt_property_t p = aProperties[i];
@ -490,11 +497,53 @@ RemoteDevicePropertiesCallback(bt_status_t aStatus, bt_bdaddr_t *aBdAddress,
BT_APPEND_NAMED_VALUE(props, "Name", propertyValue);
} else if (p.type == BT_PROPERTY_CLASS_OF_DEVICE) {
uint32_t cod = *(uint32_t*)p.val;
BT_APPEND_NAMED_VALUE(props, "Class", cod);
nsString icon;
ClassToIcon(cod, icon);
BT_APPEND_NAMED_VALUE(props, "Icon", icon);
if (!icon.IsEmpty()) {
// Valid CoD
BT_APPEND_NAMED_VALUE(props, "Class", cod);
BT_APPEND_NAMED_VALUE(props, "Icon", icon);
} else {
// If Cod is invalid, fallback to check UUIDs. It usually happens due to
// NFC directly trigger pairing. bluedroid sends wrong CoD due to missing
// EIR query records.
isCodInvalid = true;
}
} else if (p.type == BT_PROPERTY_UUIDS) {
InfallibleTArray<nsString> uuidsArray;
int uuidListLength = p.len / MAX_UUID_SIZE;
uint32_t cod = 0;
for (int i = 0; i < uuidListLength; i++) {
uint16_t uuidServiceClass = UuidToServiceClassInt((bt_uuid_t*)(p.val +
(i * MAX_UUID_SIZE)));
BluetoothServiceClass serviceClass = BluetoothUuidHelper::
GetBluetoothServiceClass(uuidServiceClass);
// Get Uuid string from BluetoothServiceClass
nsString uuid;
BluetoothUuidHelper::GetString(serviceClass, uuid);
uuidsArray.AppendElement(uuid);
// Restore CoD value
if (isCodInvalid) {
if (serviceClass == BluetoothServiceClass::HANDSFREE ||
serviceClass == BluetoothServiceClass::HEADSET) {
BT_LOGD("Restore Class Of Device to Audio bit");
SET_AUDIO_BIT(cod);
} else if (serviceClass == BluetoothServiceClass::A2DP_SINK) {
BT_LOGD("Restore Class of Device to Rendering bit");
SET_RENDERING_BIT(cod);
}
}
}
if (isCodInvalid) {
BT_APPEND_NAMED_VALUE(props, "Class", cod);
// 'audio-card' refers to 'Audio' device
BT_APPEND_NAMED_VALUE(props, "Icon", NS_LITERAL_STRING("audio-card"));
}
BT_APPEND_NAMED_VALUE(props, "UUIDS", uuidsArray);
} else {
BT_LOGD("Other non-handled device properties. Type: %d", p.type);
}

View File

@ -55,6 +55,15 @@ BdAddressTypeToString(bt_bdaddr_t* aBdAddressType, nsAString& aRetBdAddress)
aRetBdAddress = NS_ConvertUTF8toUTF16(bdstr);
}
uint16_t
UuidToServiceClassInt(bt_uuid_t* p_uuid)
{
// extract short UUID 0000xxxx-0000-1000-8000-00805f9b34fb
uint16_t shortUuid;
memcpy(&shortUuid, &(p_uuid->uu[2]), sizeof(uint16_t));
return ntohs(shortUuid);
}
bool
SetJsObject(JSContext* aContext,
const BluetoothValue& aValue,

View File

@ -29,6 +29,9 @@ void
BdAddressTypeToString(bt_bdaddr_t* aBdAddressType,
nsAString& aRetBdAddress);
uint16_t
UuidToServiceClassInt(bt_uuid_t* p_uuid);
bool
SetJsObject(JSContext* aContext,
const BluetoothValue& aValue,

View File

@ -88,9 +88,12 @@ USING_BLUETOOTH_NAMESPACE
* turn off Bluetooth.
*/
#define TIMEOUT_FORCE_TO_DISABLE_BT 5
#define BT_LAZY_THREAD_TIMEOUT_MS 3000
// Set Class of Device value bit
#define SET_AUDIO_BIT(cod) (cod |= 0x200000)
#define SET_RENDERING_BIT(cod) (cod |= 0x40000)
#ifdef MOZ_WIDGET_GONK
class Bluedroid
{
@ -821,15 +824,17 @@ HasAudioService(uint32_t aCodValue)
return ((aCodValue & 0x200000) == 0x200000);
}
static bool
ContainsIcon(const InfallibleTArray<BluetoothNamedValue>& aProperties)
static int
FindProperty(const InfallibleTArray<BluetoothNamedValue>& aProperties,
const char* aPropertyType)
{
for (uint8_t i = 0; i < aProperties.Length(); i++) {
if (aProperties[i].name().EqualsLiteral("Icon")) {
return true;
for (int i = 0; i < aProperties.Length(); ++i) {
if (aProperties[i].name().EqualsASCII(aPropertyType)) {
return i;
}
}
return false;
return -1;
}
static bool
@ -1766,7 +1771,7 @@ EventFilter(DBusConnection* aConn, DBusMessage* aMsg, void* aData)
BluetoothNamedValue(NS_LITERAL_STRING("Path"),
GetObjectPathFromAddress(signalPath, address)));
if (!ContainsIcon(properties)) {
if (FindProperty(properties, "Icon") < 0) {
for (uint32_t i = 0; i < properties.Length(); i++) {
// It is possible that property Icon missed due to CoD of major
// class is TOY but service class is "Audio", we need to assign
@ -2688,7 +2693,7 @@ public:
// Icon as audio-card. This is for PTS test TC_AG_COD_BV_02_I.
// As HFP specification defined that
// service class is "Audio" can be considered as HFP AG.
if (!ContainsIcon(devicePropertiesArray)) {
if (FindProperty(devicePropertiesArray, "Icon") < 0) {
for (uint32_t j = 0; j < devicePropertiesArray.Length(); ++j) {
BluetoothNamedValue& deviceProperty = devicePropertiesArray[j];
if (deviceProperty.name().EqualsLiteral("Class")) {
@ -2702,6 +2707,40 @@ public:
}
}
// Check whether the properties array contains CoD. If it doesn't, fallback to restore
// CoD value. This usually happens due to NFC directly triggers pairing that
// makes bluez not update CoD value.
if (FindProperty(devicePropertiesArray, "Class") < 0) {
uint32_t cod = 0;
int uuidIndex = FindProperty(devicePropertiesArray, "UUIDs");
if (uuidIndex >= 0) {
BluetoothNamedValue& deviceProperty = devicePropertiesArray[uuidIndex];
const InfallibleTArray<nsString>& uuids =
deviceProperty.value().get_ArrayOfnsString();
for (uint32_t i = 0; i < uuids.Length(); ++i) {
BluetoothServiceClass serviceClass =
BluetoothUuidHelper::GetBluetoothServiceClass(uuids[i]);
if (serviceClass == BluetoothServiceClass::HANDSFREE ||
serviceClass == BluetoothServiceClass::HEADSET) {
BT_LOGD("Restore CoD value, set Audio bit");
SET_AUDIO_BIT(cod);
} else if (serviceClass == BluetoothServiceClass::A2DP_SINK) {
BT_LOGD("Restore CoD value, set A2DP_SINK bit");
SET_RENDERING_BIT(cod);
}
}
// Add both CoD and Icon information anyway, 'audio-card' refers to
// 'Audio' device.
devicePropertiesArray.AppendElement(
BluetoothNamedValue(NS_LITERAL_STRING("Class"), cod));
devicePropertiesArray.AppendElement(
BluetoothNamedValue(NS_LITERAL_STRING("Icon"),
NS_LITERAL_STRING("audio-card")));
}
}
if (mFilterFunc(deviceProperties)) {
mValues.get_ArrayOfBluetoothNamedValue().AppendElement(
BluetoothNamedValue(mDeviceAddresses[i], deviceProperties));

View File

@ -84,8 +84,10 @@ public:
NS_IMETHODIMP
NotifyDialSuccess(uint32_t aCallIndex)
{
nsRefPtr<TelephonyCallId> id = mTelephony->CreateCallId(mNumber);
nsRefPtr<TelephonyCall> call =
mTelephony->CreateNewDialingCall(mServiceId, mNumber, aCallIndex);
mTelephony->CreateCall(id, mServiceId, aCallIndex,
nsITelephonyService::CALL_STATE_DIALING);
mPromise->MaybeResolve(call);
return NS_OK;
@ -240,7 +242,7 @@ Telephony::HasDialingCall()
already_AddRefed<Promise>
Telephony::DialInternal(uint32_t aServiceId, const nsAString& aNumber,
bool aIsEmergency)
bool aEmergency)
{
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetOwner());
if (!global) {
@ -262,7 +264,7 @@ Telephony::DialInternal(uint32_t aServiceId, const nsAString& aNumber,
nsCOMPtr<nsITelephonyCallback> callback =
new Callback(this, promise, aServiceId, aNumber);
nsresult rv = mService->Dial(aServiceId, aNumber, aIsEmergency, callback);
nsresult rv = mService->Dial(aServiceId, aNumber, aEmergency, callback);
if (NS_FAILED(rv)) {
promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
return promise.forget();
@ -271,19 +273,36 @@ Telephony::DialInternal(uint32_t aServiceId, const nsAString& aNumber,
return promise.forget();
}
already_AddRefed<TelephonyCall>
Telephony::CreateNewDialingCall(uint32_t aServiceId, const nsAString& aNumber,
uint32_t aCallIndex)
already_AddRefed<TelephonyCallId>
Telephony::CreateCallId(const nsAString& aNumber, uint16_t aNumberPresentation,
const nsAString& aName, uint16_t aNamePresentation)
{
nsRefPtr<TelephonyCall> call =
TelephonyCall::Create(this, aServiceId, aNumber,
nsITelephonyService::CALL_PRESENTATION_ALLOWED,
EmptyString(),
nsITelephonyService::CALL_PRESENTATION_ALLOWED,
nsITelephonyService::CALL_STATE_DIALING, aCallIndex);
NS_ASSERTION(call, "This should never fail!");
nsRefPtr<TelephonyCallId> id =
new TelephonyCallId(GetOwner(), aNumber, aNumberPresentation,
aName, aNamePresentation);
NS_ASSERTION(mCalls.Contains(call), "Should have auto-added new call!");
return id.forget();
}
already_AddRefed<TelephonyCall>
Telephony::CreateCall(TelephonyCallId* aId, uint32_t aServiceId,
uint32_t aCallIndex, uint16_t aCallState,
bool aEmergency, bool aConference,
bool aSwitchable, bool aMergeable)
{
// We don't have to create an already ended call.
if (aCallState == nsITelephonyService::CALL_STATE_DISCONNECTED) {
return nullptr;
}
nsRefPtr<TelephonyCall> call =
TelephonyCall::Create(this, aId, aServiceId, aCallIndex, aCallState,
aEmergency, aConference, aSwitchable, aMergeable);
NS_ASSERTION(call, "This should never fail!");
NS_ASSERTION(aConference ? mGroup->CallsArray().Contains(call)
: mCalls.Contains(call),
"Should have auto-added new call!");
return call.forget();
}
@ -518,25 +537,13 @@ Telephony::CallStateChanged(uint32_t aServiceId, uint32_t aCallIndex,
return NS_OK;
}
// Do nothing since we didn't know anything about it before now and it's
// ended already.
if (aCallState == nsITelephonyService::CALL_STATE_DISCONNECTED) {
return NS_OK;
}
// Didn't find this call in mCalls or mGroup. Create a new call.
nsRefPtr<TelephonyCallId> id = CreateCallId(aNumber, aNumberPresentation,
aName, aNamePresentation);
nsRefPtr<TelephonyCall> call =
TelephonyCall::Create(this, aServiceId, aNumber, aNumberPresentation,
aName, aNamePresentation, aCallState, aCallIndex,
aIsEmergency, aIsConference, aIsSwitchable,
aIsMergeable);
NS_ASSERTION(call, "This should never fail!");
CreateCall(id, aServiceId, aCallIndex, aCallState,
aIsEmergency, aIsConference, aIsSwitchable, aIsMergeable);
NS_ASSERTION(aIsConference ? mGroup->CallsArray().Contains(call) :
mCalls.Contains(call),
"Should have auto-added new call!");
if (aCallState == nsITelephonyService::CALL_STATE_INCOMING) {
if (call && aCallState == nsITelephonyService::CALL_STATE_INCOMING) {
nsresult rv = DispatchCallEvent(NS_LITERAL_STRING("incoming"), call);
NS_ENSURE_SUCCESS(rv, rv);
}
@ -576,28 +583,21 @@ Telephony::EnumerateCallState(uint32_t aServiceId, uint32_t aCallIndex,
bool aIsEmergency, bool aIsConference,
bool aIsSwitchable, bool aIsMergeable)
{
nsRefPtr<TelephonyCall> call;
// We request calls enumeration in constructor, and the asynchronous result
// will be sent back through the callback function EnumerateCallState().
// However, it is likely to have call state changes, i.e. CallStateChanged()
// being called, before the enumeration result comes back. We'd make sure
// we don't somehow add duplicates due to the race condition.
call = GetCallFromEverywhere(aServiceId, aCallIndex);
nsRefPtr<TelephonyCall> call = GetCallFromEverywhere(aServiceId, aCallIndex);
if (call) {
return NS_OK;
}
// Didn't know anything about this call before now.
call = TelephonyCall::Create(this, aServiceId, aNumber, aNumberPresentation,
aName, aNamePresentation, aCallState,
aCallIndex, aIsEmergency, aIsConference,
aIsSwitchable, aIsMergeable);
NS_ASSERTION(call, "This should never fail!");
NS_ASSERTION(aIsConference ? mGroup->CallsArray().Contains(call) :
mCalls.Contains(call),
"Should have auto-added new call!");
nsRefPtr<TelephonyCallId> id = CreateCallId(aNumber, aNumberPresentation,
aName, aNamePresentation);
CreateCall(id, aServiceId, aCallIndex, aCallState,
aIsEmergency, aIsConference, aIsSwitchable, aIsMergeable);
return NS_OK;
}

View File

@ -167,11 +167,19 @@ private:
HasDialingCall();
already_AddRefed<Promise>
DialInternal(uint32_t aServiceId, const nsAString& aNumber, bool isEmergency);
DialInternal(uint32_t aServiceId, const nsAString& aNumber, bool aEmergency);
already_AddRefed<TelephonyCallId>
CreateCallId(const nsAString& aNumber,
uint16_t aNumberPresentation = nsITelephonyService::CALL_PRESENTATION_ALLOWED,
const nsAString& aName = EmptyString(),
uint16_t aNamePresentation = nsITelephonyService::CALL_PRESENTATION_ALLOWED);
already_AddRefed<TelephonyCall>
CreateNewDialingCall(uint32_t aServiceId, const nsAString& aNumber,
uint32_t aCallIndex);
CreateCall(TelephonyCallId* aId,
uint32_t aServiceId, uint32_t aCallIndex, uint16_t aCallState,
bool aEmergency = false, bool aConference = false,
bool aSwitchable = true, bool aMergeable = true);
nsresult
NotifyCallsChanged(TelephonyCall* aCall);
@ -185,9 +193,6 @@ private:
already_AddRefed<TelephonyCall>
GetCall(uint32_t aServiceId, uint32_t aCallIndex);
already_AddRefed<TelephonyCall>
GetOutgoingCall();
already_AddRefed<TelephonyCall>
GetCallFromEverywhere(uint32_t aServiceId, uint32_t aCallIndex);
};

View File

@ -18,30 +18,26 @@ using mozilla::ErrorResult;
// static
already_AddRefed<TelephonyCall>
TelephonyCall::Create(Telephony* aTelephony, uint32_t aServiceId,
const nsAString& aNumber, uint16_t aNumberPresentation,
const nsAString& aName, uint16_t aNamePresentation,
uint16_t aCallState, uint32_t aCallIndex, bool aEmergency,
bool aIsConference, bool aSwitchable, bool aMergeable)
TelephonyCall::Create(Telephony* aTelephony, TelephonyCallId* aId,
uint32_t aServiceId, uint32_t aCallIndex,
uint16_t aCallState, bool aEmergency, bool aConference,
bool aSwitchable, bool aMergeable)
{
NS_ASSERTION(aTelephony, "Null pointer!");
NS_ASSERTION(!aNumber.IsEmpty(), "Empty number!");
NS_ASSERTION(aTelephony, "Null aTelephony pointer!");
NS_ASSERTION(aId, "Null aId pointer!");
NS_ASSERTION(aCallIndex >= 1, "Invalid call index!");
nsRefPtr<TelephonyCall> call = new TelephonyCall(aTelephony->GetOwner());
nsRefPtr<TelephonyCallId> id = new TelephonyCallId(aTelephony->GetOwner(),
aNumber, aNumberPresentation,
aName, aNamePresentation);
call->mTelephony = aTelephony;
call->mId = aId;
call->mServiceId = aServiceId;
call->mCallIndex = aCallIndex;
call->mError = nullptr;
call->mEmergency = aEmergency;
call->mGroup = aIsConference ? aTelephony->ConferenceGroup() : nullptr;
call->mGroup = aConference ? aTelephony->ConferenceGroup() : nullptr;
call->mSwitchable = aSwitchable;
call->mMergeable = aMergeable;
call->mId = id;
call->mError = nullptr;
call->ChangeStateInternal(aCallState, false);
@ -50,7 +46,6 @@ TelephonyCall::Create(Telephony* aTelephony, uint32_t aServiceId,
TelephonyCall::TelephonyCall(nsPIDOMWindow* aOwner)
: DOMEventTargetHelper(aOwner),
mCallState(nsITelephonyService::CALL_STATE_UNKNOWN),
mLive(false)
{
}

View File

@ -117,11 +117,9 @@ public:
IMPL_EVENT_HANDLER(groupchange)
static already_AddRefed<TelephonyCall>
Create(Telephony* aTelephony, uint32_t aServiceId,
const nsAString& aNumber, uint16_t aNumberPresentation,
const nsAString& aName, uint16_t aNamePresentation,
uint16_t aCallState, uint32_t aCallIndex,
bool aEmergency = false, bool aIsConference = false,
Create(Telephony* aTelephony, TelephonyCallId* aId,
uint32_t aServiceId, uint32_t aCallIndex, uint16_t aCallState,
bool aEmergency = false, bool aConference = false,
bool aSwitchable = true, bool aMergeable = true);
void

View File

@ -107,7 +107,25 @@ let emulator = (function() {
*/
function clearCalls() {
log("Clear existing calls.");
return emulator.run("gsm clear").then(waitForNoCall);
// Hang up all calls.
let hangUpPromises = [];
for (let call of telephony.calls) {
log(".. hangUp " + call.number);
hangUpPromises.push(hangUp(call));
}
for (let call of conference.calls) {
log(".. hangUp " + call.number);
hangUpPromises.push(hangUp(call));
}
return Promise.all(hangUpPromises)
.then(() => {
return emulator.run("gsm clear");
})
.then(waitForNoCall);
}
/**

View File

@ -27,5 +27,8 @@ startTest(function() {
.then(() => testEmergencyLabel("911", true))
.then(() => testEmergencyLabel("0912345678", false))
.then(() => testEmergencyLabel("777", false))
.then(null, () => {
ok(false, 'promise rejects during test.');
})
.then(finish);
});

View File

@ -379,7 +379,7 @@
@BINPATH@/components/FormHistoryStartup.js
@BINPATH@/components/nsInputListAutoComplete.js
@BINPATH@/components/formautofill.manifest
@BINPATH@/components/AutofillController.js
@BINPATH@/components/FormAutofillContentService.js
@BINPATH@/components/contentSecurityPolicy.manifest
@BINPATH@/components/contentSecurityPolicy.js
@BINPATH@/components/contentAreaDropListener.manifest

View File

@ -1556,10 +1556,16 @@ detectedLanguageChangedBefore
detectedLanguageChangedAfter
Integer count of the number of times the user manually adjusted the detected
language after having first translated the page.
targetLanguageChanged
Integer count of the number of times the user manually adjusted the target
language.
deniedTranslationOffer
Integer count of the numbers of times the user opted-out offered
Integer count of the number of times the user opted-out offered
page translation, either by the Not Now button or by the notification's
close button in the "offer" state.
showOriginalContent
Integer count of the number of times the user activated the Show Original
command.
Additional daily counts broken down by language are reported in the following
properties:
@ -1597,7 +1603,9 @@ Example
"charactersTranslatedCount": "1126",
"detectedLanguageChangedBefore": 1,
"detectedLanguageChangedAfter": 2,
"deniedTranslationOffer": 3
"targetLanguageChanged": 0,
"deniedTranslationOffer": 3,
"showOriginalContent": 2,
"translationOpportunityCountsByLanguage": {
"fr": 100,
"es": 34

View File

@ -1,32 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
function AutofillController() {}
AutofillController.prototype = {
_dispatchAsync: function (fn) {
Services.tm.currentThread.dispatch(fn, Ci.nsIThread.DISPATCH_NORMAL);
},
_dispatchDisabled: function (form, win, message) {
Services.console.logStringMessage("requestAutocomplete disabled: " + message);
let evt = new win.AutocompleteErrorEvent("autocompleteerror", { bubbles: true, reason: "disabled" });
form.dispatchEvent(evt);
},
requestAutocomplete: function (form, win) {
this._dispatchAsync(() => this._dispatchDisabled(form, win, "not implemented"));
},
classID: Components.ID("{ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutofillController])
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AutofillController]);

View File

@ -0,0 +1,41 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* Implements a service used by DOM content to request Form Autofill, in
* particular when the requestAutocomplete method of Form objects is invoked.
*
* See the nsIFormAutofillContentService documentation for details.
*/
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
function FormAutofillContentService() {
}
FormAutofillContentService.prototype = {
classID: Components.ID("{ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormAutofillContentService]),
// nsIFormAutofillContentService
requestAutocomplete: function (aForm, aWindow) {
Services.console.logStringMessage("requestAutocomplete not implemented.");
// We will return "disabled" for now.
let event = new aWindow.AutocompleteErrorEvent("autocompleteerror",
{ bubbles: true,
reason: "disabled" });
// Ensure the event is always dispatched on the next tick.
Services.tm.currentThread.dispatch(() => aForm.dispatchEvent(event),
Ci.nsIThread.DISPATCH_NORMAL);
},
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FormAutofillContentService]);

View File

@ -1,2 +1,2 @@
component {ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6} AutofillController.js
contract @mozilla.org/autofill-controller;1 {ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6}
component {ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6} FormAutofillContentService.js
contract @mozilla.org/formautofill/content-service;1 {ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6}

View File

@ -18,12 +18,12 @@ XPCSHELL_TESTS_MANIFESTS += [
]
XPIDL_SOURCES += [
'nsIAutofillController.idl',
'nsIFormAutofillContentService.idl',
]
XPIDL_MODULE = 'toolkit_formautofill'
EXTRA_COMPONENTS += [
'AutofillController.js',
'formautofill.manifest',
'FormAutofillContentService.js',
]

View File

@ -1,14 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
interface nsIDOMHTMLFormElement;
interface nsIDOMWindow;
[scriptable, uuid(cbf47f9d-a13d-4fad-8221-8964086b1b8a)]
interface nsIAutofillController : nsISupports
{
void requestAutocomplete(in nsIDOMHTMLFormElement form, in nsIDOMWindow window);
};

View File

@ -0,0 +1,46 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
interface nsIDOMHTMLFormElement;
interface nsIDOMWindow;
/**
* Defines a service used by DOM content to request Form Autofill, in particular
* when the requestAutocomplete method of Form objects is invoked.
*
* This service lives in the process that hosts the requesting DOM content.
* This means that, in a multi-process (e10s) environment, there can be an
* instance of the service for each content process, in addition to an instance
* for the chrome process.
*
* @remarks The service implementation uses a child-side message manager to
* communicate with a parent-side message manager living in the chrome
* process, where most of the processing is located.
*/
[scriptable, uuid(1db29340-99df-4845-9102-0c5d281b2fe8)]
interface nsIFormAutofillContentService : nsISupports
{
/**
* Invoked by the requestAutocomplete method of the DOM Form object.
*
* The application is expected to display a user interface asking for the
* details that are relevant to the form being filled in. The application
* should use the "autocomplete" attributes on the input elements as hints
* about which type of information is being requested.
*
* The processing will result in either an "autocomplete" simple DOM Event or
* an AutocompleteErrorEvent being fired on the form.
*
* @param aForm
* The form on which the requestAutocomplete method was invoked.
* @param aWindow
* The window where the form is located. This must be specified even
* for elements that are not in a document, and is used to generate the
* DOM events resulting from the operation.
*/
void requestAutocomplete(in nsIDOMHTMLFormElement aForm,
in nsIDOMWindow aWindow);
};

View File

@ -4,3 +4,4 @@ support-files =
head.js
[test_infrastructure.html]
[test_requestAutocomplete_disabled.html]

View File

@ -0,0 +1,31 @@
<!DOCTYPE HTML><html><head><meta charset="utf-8"></head><body>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<form id="form">
</form>
<script type="application/javascript;version=1.7" src="head.js"></script>
<script type="application/javascript;version=1.7">
/*
* Tests the cases where the requestAutocomplete method returns "disabled".
*/
"use strict";
/**
* Tests the case where the feature is disabled globally.
*/
add_task(function* test_disabled_globally() {
let promise = TestUtils.waitForEvent($("form"), "autocompleteerror");
$("form").requestAutocomplete();
let errorEvent = yield promise;
Assert.equal(errorEvent.reason, "disabled");
});
add_task(terminationTaskFn);
</script>
</body></html>

View File

@ -1026,7 +1026,7 @@
*
* Note: This function will remove a symlink even if it points a directory.
*/
File.removeDir = function(path, options) {
File.removeDir = function(path, options = {}) {
let isSymLink;
try {
let info = File.stat(path, {unixNoFollowingLinks: true});

View File

@ -1031,7 +1031,7 @@
* @throws {OS.File.Error} In case of I/O error, in particular if |path| is
* not a directory.
*/
File.removeDir = function(path, options) {
File.removeDir = function(path, options = {}) {
// We can't use File.stat here because it will follow the symlink.
let attributes = WinFile.GetFileAttributes(path);
if (attributes == Const.INVALID_FILE_ATTRIBUTES) {

View File

@ -3522,14 +3522,6 @@
"kind": "boolean",
"description": "Session restore: Whether the file read on startup contained parse-able JSON"
},
"FX_SESSION_RESTORE_BACKUP_FILE_MS": {
"expires_in_version": "never",
"kind": "exponential",
"high": "30000",
"n_buckets": 10,
"extended_statistics_ok": true,
"description": "Session restore: Time to make a backup copy of the session file (ms)"
},
"FX_SESSION_RESTORE_RESTORE_WINDOW_MS": {
"expires_in_version": "never",
"kind": "exponential",

View File

@ -622,6 +622,10 @@ function isToolbarItem(aElt)
function onToolbarDragExit(aEvent)
{
if (isUnwantedDragEvent(aEvent)) {
return;
}
if (gCurrentDragOverItem)
setDragActive(gCurrentDragOverItem, false);
}
@ -645,6 +649,10 @@ function onToolbarDragStart(aEvent)
function onToolbarDragOver(aEvent)
{
if (isUnwantedDragEvent(aEvent)) {
return;
}
var documentId = gToolboxDocument.documentElement.id;
if (!aEvent.dataTransfer.types.contains("text/toolbarwrapper-id/" + documentId.toLowerCase()))
return;
@ -697,6 +705,10 @@ function onToolbarDragOver(aEvent)
function onToolbarDrop(aEvent)
{
if (isUnwantedDragEvent(aEvent)) {
return;
}
if (!gCurrentDragOverItem)
return;
@ -767,13 +779,19 @@ function onToolbarDrop(aEvent)
function onPaletteDragOver(aEvent)
{
if (isUnwantedDragEvent(aEvent)) {
return;
}
var documentId = gToolboxDocument.documentElement.id;
if (aEvent.dataTransfer.types.contains("text/toolbarwrapper-id/" + documentId.toLowerCase()))
aEvent.preventDefault();
}
function onPaletteDrop(aEvent)
{
{
if (isUnwantedDragEvent(aEvent)) {
return;
}
var documentId = gToolboxDocument.documentElement.id;
var itemId = aEvent.dataTransfer.getData("text/toolbarwrapper-id/" + documentId);
@ -798,3 +816,18 @@ function onPaletteDrop(aEvent)
toolboxChanged();
}
function isUnwantedDragEvent(aEvent) {
/* Discard drag events that originated from a separate window to
prevent content->chrome privilege escalations. */
let mozSourceNode = aEvent.dataTransfer.mozSourceNode;
// mozSourceNode is null in the dragStart event handler or if
// the drag event originated in an external application.
if (!mozSourceNode) {
return true;
}
let sourceWindow = mozSourceNode.ownerDocument.defaultView;
return sourceWindow != window && sourceWindow != gToolboxDocument.defaultView;
}

View File

@ -352,12 +352,16 @@ XULBasedHighlighter.prototype = {
/**
* Show the highlighter on a given node
* @param {DOMNode} node
* @param {Object} options
* Object used for passing options
*/
show: function(node) {
show: function(node, options={}) {
if (!isNodeValid(node) || node === this.currentNode) {
return;
}
this.options = options;
this._detachPageListeners();
this.currentNode = node;
this._attachPageListeners();
@ -375,6 +379,7 @@ XULBasedHighlighter.prototype = {
this._hide();
this._detachPageListeners();
this.currentNode = null;
this.options = null;
},
/**
@ -630,12 +635,38 @@ BoxModelHighlighter.prototype = Heritage.extend(XULBasedHighlighter.prototype, {
this._currentNode = null;
},
/**
* Show the highlighter on a given node. We override this method so that the
* same node can be rehighlighted e.g. to highlight different regions from the
* layout view.
*
* @param {DOMNode} node
* @param {Object} options
* Object used for passing options
*/
show: function(node, options={}) {
if (!isNodeValid(node)) {
return;
}
this.options = options;
if (!this.options.region) {
this.options.region = "content";
}
this._detachPageListeners();
this.currentNode = node;
this._attachPageListeners();
this._show();
},
/**
* Show the highlighter on a given node
* @param {Object} options
* Object used for passing options
*/
_show: function(options={}) {
_show: function() {
this._update();
this._trackMutations();
this.emit("ready");
@ -664,13 +695,9 @@ BoxModelHighlighter.prototype = Heritage.extend(XULBasedHighlighter.prototype, {
* Update the highlighter on the current highlighted node (the one that was
* passed as an argument to show(node)).
* Should be called whenever node size or attributes change
* @param {Object} options
* Object used for passing options. Valid options are:
* - box: "content", "padding", "border" or "margin." This specifies
* the box that the guides should outline. Default is content.
*/
_update: function(options={}) {
if (this._updateBoxModel(options)) {
_update: function() {
if (this._updateBoxModel()) {
this._showInfobar();
this._showBoxModel();
} else {
@ -720,16 +747,10 @@ BoxModelHighlighter.prototype = Heritage.extend(XULBasedHighlighter.prototype, {
/**
* Update the box model as per the current node.
*
* @param {Object} options
* Object used for passing options. Valid options are:
* - region: "content", "padding", "border" or "margin." This specifies
* the region that the guides should outline. Default is content.
* @return {boolean}
* True if the current node has a box model to be highlighted
*/
_updateBoxModel: function(options) {
options.region = options.region || "content";
_updateBoxModel: function() {
if (this._nodeNeedsHighlighting()) {
for (let boxType in this._boxModelNodes) {
let {p1, p2, p3, p4} =
@ -742,7 +763,7 @@ BoxModelHighlighter.prototype = Heritage.extend(XULBasedHighlighter.prototype, {
p3.x + "," + p3.y + " " +
p4.x + "," + p4.y);
if (boxType === options.region) {
if (boxType === this.options.region) {
this._showGuides(p1, p2, p3, p4);
}
}

View File

@ -79,7 +79,10 @@ function test_socket_shutdown()
onClosed: function(aStatus) {
do_print("test_socket_shutdown onClosed called at " + new Date().toTimeString());
do_check_eq(aStatus, Cr.NS_ERROR_CONNECTION_REFUSED);
// The connection should be refused here, but on slow or overloaded
// machines it may just time out.
let expected = [ Cr.NS_ERROR_CONNECTION_REFUSED, Cr.NS_ERROR_NET_TIMEOUT ];
do_check_neq(expected.indexOf(aStatus), -1);
run_next_test();
}
};

View File

@ -308,7 +308,8 @@ nsUnknownContentTypeDialog.prototype = {
// Remove the file so that it's not there when we ensure non-existence later;
// this is safe because for the file to exist, the user would have had to
// confirm that he wanted the file overwritten.
if (result.exists())
// Only remove file if final name exists
if (result.exists() && this.getFinalLeafName(result.leafName) == result.leafName)
result.remove(false);
}
catch (ex) {
@ -342,6 +343,18 @@ nsUnknownContentTypeDialog.prototype = {
}.bind(this)).then(null, Components.utils.reportError);
},
getFinalLeafName: function (aLeafName, aFileExt)
{
// Remove any leading periods, since we don't want to save hidden files
// automatically.
aLeafName = aLeafName.replace(/^\.+/, "");
if (aLeafName == "")
aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : "");
return aLeafName;
},
/**
* Ensures that a local folder/file combination does not already exist in
* the file system (or finds such a combination with a reasonably similar
@ -366,12 +379,8 @@ nsUnknownContentTypeDialog.prototype = {
throw new Components.Exception("Destination directory non-existing or permission error",
Components.results.NS_ERROR_FILE_ACCESS_DENIED);
}
// Remove any leading periods, since we don't want to save hidden files
// automatically.
aLeafName = aLeafName.replace(/^\.+/, "");
if (aLeafName == "")
aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : "");
aLeafName = this.getFinalLeafName(aLeafName, aFileExt);
aLocalFolder.append(aLeafName);
// The following assignment can throw an exception, but