Merge fx-team to m-c a=merge CLOSED TREE

This commit is contained in:
Wes Kocher 2015-03-24 18:12:58 -07:00
commit 3ea14aaaa3
92 changed files with 3140 additions and 1245 deletions

View File

@ -354,7 +354,6 @@ function nukeModules() {
// the addon is unload.
unloadSandbox(cuddlefishSandbox.loaderSandbox);
unloadSandbox(cuddlefishSandbox.xulappSandbox);
// Bug 764840: We need to unload cuddlefish otherwise it will stay alive
// and keep a reference to this compartment.

View File

@ -3,8 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var readParam = require("./node-scripts/utils").readParam;
var path = require("path");
var Promise = require("promise");
var Mocha = require("mocha");
var mocha = new Mocha({
ui: "bdd",
@ -12,16 +11,19 @@ var mocha = new Mocha({
timeout: 900000
});
var type = readParam("type");
exports.run = function(type) {
return new Promise(function(resolve) {
type = type || "";
[
(/^(modules)?$/.test(type)) && require.resolve("../bin/node-scripts/test.modules"),
(/^(addons)?$/.test(type)) && require.resolve("../bin/node-scripts/test.addons"),
(/^(examples)?$/.test(type)) && require.resolve("../bin/node-scripts/test.examples"),
].sort().forEach(function(filepath) {
filepath && mocha.addFile(filepath);
})
[
(!type || type == "modules") && require.resolve("../bin/node-scripts/test.modules"),
(!type || type == "addons") && require.resolve("../bin/node-scripts/test.addons"),
(!type || type == "examples") && require.resolve("../bin/node-scripts/test.examples"),
].sort().forEach(function(filepath) {
filepath && mocha.addFile(filepath);
})
mocha.run(function (failures) {
process.exit(failures);
});
mocha.run(function(failures) {
resolve(failures);
});
});
}

View File

@ -202,7 +202,7 @@ var Connection = Class({
},
poolFor: function(id) {
for (let pool of this.pools.values()) {
if (pool.has(id))
if pool.has(id)
return pool;
}
},
@ -797,7 +797,7 @@ var Tab = Client.from({
"storageActor": "storage",
"gcliActor": "gcli",
"memoryActor": "memory",
"eventLoopLag": "eventLoopLag",
"eventLoopLag": "eventLoopLag"
"trace": "trace", // missing
}

View File

@ -0,0 +1,23 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var gulp = require('gulp');
gulp.task('test', function(done) {
require("./bin/jpm-test").run().then(done);
});
gulp.task('test:addons', function(done) {
require("./bin/jpm-test").run("addons").then(done);
});
gulp.task('test:examples', function(done) {
require("./bin/jpm-test").run("examples").then(done);
});
gulp.task('test:modules', function(done) {
require("./bin/jpm-test").run("modules").then(done);
});

View File

@ -16,6 +16,31 @@ const assetsURI = require('../self').data.url();
const hideSheetUri = "data:text/css,:root {visibility: hidden !important;}";
function translateElementAttributes(element) {
// Translateable attributes
const attrList = ['title', 'accesskey', 'alt', 'label', 'placeholder'];
const ariaAttrMap = {
'ariaLabel': 'aria-label',
'ariaValueText': 'aria-valuetext',
'ariaMozHint': 'aria-moz-hint'
};
const attrSeparator = '.';
// Try to translate each of the attributes
for (let attribute of attrList) {
const data = core.get(element.dataset.l10nId + attrSeparator + attribute);
if (data)
element.setAttribute(attribute, data);
}
// Look for the aria attribute translations that match fxOS's aliases
for (let attrAlias in ariaAttrMap) {
const data = core.get(element.dataset.l10nId + attrSeparator + attrAlias);
if (data)
element.setAttribute(ariaAttrMap[attrAlias], data);
}
}
// Taken from Gaia:
// https://github.com/andreasgal/gaia/blob/04fde2640a7f40314643016a5a6c98bf3755f5fd/webapi.js#L1470
function translateElement(element) {
@ -32,6 +57,8 @@ function translateElement(element) {
var data = core.get(key);
if (data)
child.textContent = data;
translateElementAttributes(child);
}
}
exports.translateElement = translateElement;

View File

@ -320,6 +320,8 @@ TestRunner.prototype = {
});
PromiseDebugging.flushUncaughtErrors();
PromiseDebugging.removeUncaughtErrorObserver(this._uncaughtErrorObserver);
return all(winPromises).then(() => {
let browserWins = wins.filter(isBrowser);
@ -537,7 +539,8 @@ TestRunner.prototype = {
this.test.errors = {};
this.test.last = 'START';
PromiseDebugging.clearUncaughtErrorObservers();
PromiseDebugging.addUncaughtErrorObserver(this._uncaughtErrorObserver.bind(this));
this._uncaughtErrorObserver = this._uncaughtErrorObserver.bind(this);
PromiseDebugging.addUncaughtErrorObserver(this._uncaughtErrorObserver);
this.isDone = false;
this.onDone = function(self) {

View File

@ -21,6 +21,10 @@ let getWindowFrom = x =>
null;
function removeFromListeners() {
this.removeEventListener("DOMWindowClose", removeFromListeners);
for (let cleaner of listeners.get(this))
cleaner();
listeners.delete(this);
}
@ -45,26 +49,25 @@ function open(target, type, options) {
if (!window)
throw new Error("Unable to obtain the owner window from the target given.");
let cleaners = listeners.get(window) || [];
let cleaners = listeners.get(window);
if (!cleaners) {
cleaners = [];
listeners.set(window, cleaners);
// We need to remove from our map the `window` once is closed, to prevent
// memory leak
window.addEventListener("DOMWindowClose", removeFromListeners);
}
cleaners.push(() => target.removeEventListener(type, listener, capture));
listeners.set(window, cleaners);
// We need to remove from our map the `window` once is closed, to prevent
// memory leak
window.addEventListener("DOMWindowClose", removeFromListeners);
target.addEventListener(type, listener, capture);
return output;
}
unload(() => {
for (let [window, cleaners] of listeners) {
cleaners.forEach(callback => callback())
}
listeners.clear();
for (let window of listeners.keys())
removeFromListeners.call(window);
});
exports.open = open;

View File

@ -692,8 +692,6 @@ function writeSync(fd, buffer, offset, length, position) {
else if (length + offset !== buffer.length) {
buffer = buffer.slice(offset, offset + length);
}
let writeStream = new WriteStream(fd, { position: position,
length: length });
let output = BinaryOutputStream(nsIFileOutputStream(fd));
nsIBinaryOutputStream(fd, output);

View File

@ -23,6 +23,7 @@ const historyService = Cc['@mozilla.org/browser/nav-history-service;1']
const { mapBookmarkItemType } = require('./utils');
const { EventTarget } = require('../event/target');
const { emit } = require('../event/core');
const { when } = require('../system/unload');
const emitter = EventTarget();
@ -119,4 +120,9 @@ historyService.addObserver(historyObserver, false);
let bookmarkObserver = createObserverInstance(BOOKMARK_EVENTS, BOOKMARK_ARGS);
bookmarkService.addObserver(bookmarkObserver, false);
when(() => {
historyService.removeObserver(historyObserver);
bookmarkService.removeObserver(bookmarkObserver);
});
exports.events = emitter;

View File

@ -169,6 +169,15 @@ function serializeStack(frames) {
Loader.serializeStack = serializeStack;
function readURI(uri) {
let nsURI = NetUtil.newURI(uri);
if (nsURI.scheme == "resource") {
// Resolve to a real URI, this will catch any obvious bad paths without
// logging assertions in debug builds, see bug 1135219
let proto = Cc["@mozilla.org/network/protocol;1?name=resource"].
getService(Ci.nsIResProtocolHandler);
uri = proto.resolveURI(nsURI);
}
let stream = NetUtil.newChannel2(uri,
'UTF-8',
null,
@ -420,6 +429,10 @@ const nodeResolve = iced(function nodeResolve(id, requirer, { rootURI }) {
// Resolve again
id = Loader.resolve(id, requirer);
// If this is already an absolute URI then there is no resolution to do
if (isAbsoluteURI(id))
return void 0;
// we assume that extensions are correct, i.e., a directory doesnt't have '.js'
// and a js file isn't named 'file.json.js'
let fullId = join(rootURI, id);
@ -431,9 +444,14 @@ const nodeResolve = iced(function nodeResolve(id, requirer, { rootURI }) {
if ((resolvedPath = loadAsDirectory(fullId)))
return stripBase(rootURI, resolvedPath);
// If the requirer is an absolute URI then the node module resolution below
// won't work correctly as we prefix everything with rootURI
if (isAbsoluteURI(requirer))
return void 0;
// If manifest has dependencies, attempt to look up node modules
// in the `dependencies` list
let dirs = getNodeModulePaths(dirname(join(rootURI, requirer))).map(dir => join(dir, id));
let dirs = getNodeModulePaths(dirname(requirer)).map(dir => join(rootURI, dir, id));
for (let i = 0; i < dirs.length; i++) {
if ((resolvedPath = loadAsFile(dirs[i])))
return stripBase(rootURI, resolvedPath);
@ -509,6 +527,7 @@ function getNodeModulePaths (start) {
let dir = join(parts.slice(0, i + 1).join('/'), moduleDir);
dirs.push(dir);
}
dirs.push(moduleDir);
return dirs;
}

View File

@ -8,10 +8,7 @@
"license": "MPL 2.0",
"unpack": true,
"scripts": {
"test": "node ./bin/jpm-test.js",
"modules": "node ./bin/jpm-test.js --type modules",
"addons": "node ./bin/jpm-test.js --type addons",
"examples": "node ./bin/jpm-test.js --type examples"
"test": "gulp test"
},
"homepage": "https://github.com/mozilla/addon-sdk",
"repository": {
@ -25,6 +22,7 @@
"async": "0.9.0",
"chai": "2.1.1",
"glob": "4.4.2",
"gulp": "3.8.11",
"jpm": "0.0.29",
"lodash": "3.3.1",
"mocha": "2.1.0",

View File

@ -56,6 +56,7 @@ DEFAULT_NO_CONNECTIONS_PREFS = {
'browser.aboutHomeSnippets.updateUrl': 'https://localhost/snippet-dummy',
'browser.newtab.url' : 'about:blank',
'browser.search.update': False,
'browser.search.suggest.enabled' : False,
'browser.safebrowsing.enabled' : False,
'browser.safebrowsing.updateURL': 'http://localhost/safebrowsing-dummy/update',
'browser.safebrowsing.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',

View File

@ -20,5 +20,10 @@
<div data-l10n-id="Translated">
A data-l10n-id value can be used in multiple elements
</div>
<a data-l10n-id="link-attributes" title="Certain whitelisted attributes get translated too" alt="No" accesskey="A"></a>
<input data-l10n-id="input" type="text" placeholder="Form placeholders are translateable">
<menu>
<menuitem data-l10n-id="contextitem" label="Labels of select options and context menus are translateable">
</menu>
</body>
</html

View File

@ -26,3 +26,13 @@ first_identifier[one]=first entry is %s and the second one is %s.
first_identifier=the entries are %s and %s.
second_identifier[other]=first entry is %s and the second one is %s.
third_identifier=first entry is %s and the second one is %s.
# bug 824489 allow translation of element attributes
link-attributes.title=Yes
link-attributes.alt=Yes
link-attributes.accesskey=B
input.placeholder=Yes
contextitem.label=Yes
link-attributes.ariaLabel=Yes
link-attributes.ariaValueText=Value
link-attributes.ariaMozHint=Hint

View File

@ -105,7 +105,15 @@ exports.testHtmlLocalizationPageWorker = createTest("en-GB", function(assert, lo
self.postMessage([nodes[0].innerHTML,
nodes[1].innerHTML,
nodes[2].innerHTML,
nodes[3].innerHTML]);
nodes[3].innerHTML,
nodes[4].title,
nodes[4].getAttribute("alt"),
nodes[4].getAttribute("accesskey"),
nodes[4].getAttribute("aria-label"),
nodes[4].getAttribute("aria-valuetext"),
nodes[4].getAttribute("aria-moz-hint"),
nodes[5].placeholder,
nodes[6].label]);
},
onMessage: function (data) {
assert.equal(
@ -121,6 +129,19 @@ exports.testHtmlLocalizationPageWorker = createTest("en-GB", function(assert, lo
);
assert.equal(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
// Attribute translation tests
assert.equal(data[4], "Yes", "Title attributes gets translated.");
assert.equal(data[5], "Yes", "Alt attributes gets translated.");
assert.equal(data[6], "B", "Accesskey gets translated.");
assert.equal(data[7], "Yes", "Aria-Label gets translated.");
assert.equal(data[8], "Value", "Aria-valuetext gets translated.");
assert.equal(data[9], "Hint", "Aria-moz-hint gets translated.");
assert.equal(data[10], "Yes", "Form placeholders are translateable.");
assert.equal(data[11], "Yes", "Labels of select options and context menus are translateable.");
done();
}
});
@ -144,7 +165,15 @@ exports.testHtmlLocalization = createTest("en-GB", function(assert, loader, done
self.postMessage([nodes[0].innerHTML,
nodes[1].innerHTML,
nodes[2].innerHTML,
nodes[3].innerHTML]);
nodes[3].innerHTML,
nodes[4].title,
nodes[4].getAttribute("alt"),
nodes[4].getAttribute("accesskey"),
nodes[4].getAttribute("aria-label"),
nodes[4].getAttribute("aria-valuetext"),
nodes[4].getAttribute("aria-moz-hint"),
nodes[5].placeholder,
nodes[6].label]);
},
onMessage: function (data) {
assert.equal(
@ -160,6 +189,19 @@ exports.testHtmlLocalization = createTest("en-GB", function(assert, loader, done
);
assert.equal(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
// Attribute translation tests
assert.equal(data[4], "Yes", "Title attributes gets translated.");
assert.equal(data[5], "Yes", "Alt attributes gets translated.");
assert.equal(data[6], "B", "Accesskey gets translated.");
assert.equal(data[7], "Yes", "Aria-Label gets translated.");
assert.equal(data[8], "Value", "Aria-valuetext gets translated.");
assert.equal(data[9], "Hint", "Aria-moz-hint gets translated.");
assert.equal(data[10], "Yes", "Form placeholders are translateable.");
assert.equal(data[11], "Yes", "Labels of select options and context menus are translateable.");
tab.close(done);
}
});

View File

@ -5,7 +5,7 @@
"use strict";
const LOCAL_URI = "about:robots";
const REMOTE_URI = "about:home";
const REMOTE_URI = "data:text/html;charset=utf-8,remote";
const { Loader } = require('sdk/test/loader');
const { getTabs, openTab, closeTab, setTabURL, getBrowserForTab, getURI } = require('sdk/tabs/utils');
@ -21,6 +21,19 @@ const { set } = require('sdk/preferences/service');
// The hidden preload browser messes up our frame counts
set('browser.newtab.preload', false);
function promiseTabFrameAttach(frames) {
return new Promise(resolve => {
let listener = function(frame, ...args) {
if (!frame.isTab)
return;
frames.off("attach", listener);
resolve([frame, ...args]);
}
frames.on("attach", listener);
});
}
// Check that we see a process stop and start
exports["test process restart"] = function*(assert) {
if (!isE10S) {
@ -44,7 +57,7 @@ exports["test process restart"] = function*(assert) {
// Switch the remote tab to a local URI which should kill the remote process
let frameDetach = promiseEventOnItemAndContainer(assert, remoteFrame, frames, 'detach');
let frameAttach = promiseEvent(frames, 'attach');
let frameAttach = promiseTabFrameAttach(frames);
let processDetach = promiseEventOnItemAndContainer(assert, remoteProcess, processes, 'detach');
setTabURL(tab, LOCAL_URI);
// The load should kill the remote frame
@ -57,7 +70,7 @@ exports["test process restart"] = function*(assert) {
frameDetach = promiseEventOnItemAndContainer(assert, newFrame, frames, 'detach');
let processAttach = promiseEvent(processes, 'attach');
frameAttach = promiseEvent(frames, 'attach');
frameAttach = promiseTabFrameAttach(frames);
setTabURL(tab, REMOTE_URI);
// The load should kill the remote frame
yield frameDetach;
@ -149,7 +162,7 @@ exports["test frame list"] = function*(assert) {
assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
let promise = promiseEvent(frames, 'attach');
let promise = promiseTabFrameAttach(frames);
let tab1 = openTab(window, LOCAL_URI);
let [frame1] = yield promise;
assert.ok(!!frame1, "Should have seen the new frame");
@ -158,7 +171,7 @@ exports["test frame list"] = function*(assert) {
assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
promise = promiseEvent(frames, 'attach');
promise = promiseTabFrameAttach(frames);
let tab2 = openTab(window, REMOTE_URI);
let [frame2] = yield promise;
assert.ok(!!frame2, "Should have seen the new frame");
@ -256,7 +269,7 @@ exports["test unload"] = function*(assert) {
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = promiseEvent(frames, 'attach');
let promise = promiseTabFrameAttach(frames);
let tab = openTab(window, "data:,<html/>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
@ -280,7 +293,7 @@ exports["test frame detach on unload"] = function*(assert) {
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = promiseEvent(frames, 'attach');
let promise = promiseTabFrameAttach(frames);
let tab = openTab(window, "data:,<html/>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
@ -304,7 +317,7 @@ exports["test frame event listeners"] = function*(assert) {
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = promiseEvent(frames, 'attach');
let promise = promiseTabFrameAttach(frames);
let tab = openTab(window, "data:text/html,<html></html>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
@ -339,7 +352,7 @@ exports["test frames event listeners"] = function*(assert) {
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = promiseEvent(frames, 'attach');
let promise = promiseTabFrameAttach(frames);
let tab = openTab(window, "data:text/html,<html></html>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
@ -377,8 +390,8 @@ exports["test unload removes frame event listeners"] = function*(assert) {
let loader2 = new Loader(module);
let { frames: frames2 } = yield waitForProcesses(loader2);
let promise = promiseEvent(frames, 'attach');
let promise2 = promiseEvent(frames2, 'attach');
let promise = promiseTabFrameAttach(frames);
let promise2 = promiseTabFrameAttach(frames2);
let tab = openTab(window, "data:text/html,<html></html>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
@ -418,8 +431,8 @@ exports["test unload removes frames event listeners"] = function*(assert) {
let loader2 = new Loader(module);
let { frames: frames2 } = yield waitForProcesses(loader2);
let promise = promiseEvent(frames, 'attach');
let promise2 = promiseEvent(frames2, 'attach');
let promise = promiseTabFrameAttach(frames);
let promise2 = promiseTabFrameAttach(frames2);
let tab = openTab(window, "data:text/html,<html></html>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);

View File

@ -20,5 +20,10 @@
<div data-l10n-id="Translated">
A data-l10n-id value can be used in multiple elements
</div>
<a data-l10n-id="link-attributes" title="Certain whitelisted attributes get translated too" alt="No" accesskey="A"></a>
<input data-l10n-id="input" type="text" placeholder="Form placeholders are translateable">
<menu>
<menuitem data-l10n-id="contextitem" label="Labels of select options and context menus are translateable">
</menu>
</body>
</html

View File

@ -26,3 +26,13 @@ first_identifier[one]=first entry is %s and the second one is %s.
first_identifier=the entries are %s and %s.
second_identifier[other]=first entry is %s and the second one is %s.
third_identifier=first entry is %s and the second one is %s.
# bug 824489 allow translation of element attributes
link-attributes.title=Yes
link-attributes.alt=Yes
link-attributes.accesskey=B
input.placeholder=Yes
contextitem.label=Yes
link-attributes.ariaLabel=Yes
link-attributes.ariaValueText=Value
link-attributes.ariaMozHint=Hint

View File

@ -105,7 +105,15 @@ exports.testHtmlLocalizationPageWorker = createTest("en-GB", function(assert, lo
self.postMessage([nodes[0].innerHTML,
nodes[1].innerHTML,
nodes[2].innerHTML,
nodes[3].innerHTML]);
nodes[3].innerHTML,
nodes[4].title,
nodes[4].getAttribute("alt"),
nodes[4].getAttribute("accesskey"),
nodes[4].getAttribute("aria-label"),
nodes[4].getAttribute("aria-valuetext"),
nodes[4].getAttribute("aria-moz-hint"),
nodes[5].placeholder,
nodes[6].label]);
},
onMessage: function (data) {
assert.equal(
@ -120,6 +128,19 @@ exports.testHtmlLocalizationPageWorker = createTest("en-GB", function(assert, lo
"Content from .properties is text content; HTML can't be injected."
);
assert.equal(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
// Attribute translation tests
assert.equal(data[4], "Yes", "Title attributes gets translated.");
assert.equal(data[5], "Yes", "Alt attributes gets translated.");
assert.equal(data[6], "B", "Accesskey gets translated.");
assert.equal(data[7], "Yes", "Aria-Label gets translated.");
assert.equal(data[8], "Value", "Aria-valuetext gets translated.");
assert.equal(data[9], "Hint", "Aria-moz-hint gets translated.");
assert.equal(data[10], "Yes", "Form placeholders are translateable.");
assert.equal(data[11], "Yes", "Labels of select options and context menus are translateable.");
done();
}
@ -144,7 +165,15 @@ exports.testHtmlLocalization = createTest("en-GB", function(assert, loader, done
self.postMessage([nodes[0].innerHTML,
nodes[1].innerHTML,
nodes[2].innerHTML,
nodes[3].innerHTML]);
nodes[3].innerHTML,
nodes[4].title,
nodes[4].getAttribute("alt"),
nodes[4].getAttribute("accesskey"),
nodes[4].getAttribute("aria-label"),
nodes[4].getAttribute("aria-valuetext"),
nodes[4].getAttribute("aria-moz-hint"),
nodes[5].placeholder,
nodes[6].label]);
},
onMessage: function (data) {
assert.equal(
@ -160,6 +189,19 @@ exports.testHtmlLocalization = createTest("en-GB", function(assert, loader, done
);
assert.equal(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
// Attribute translation tests
assert.equal(data[4], "Yes", "Title attributes gets translated.");
assert.equal(data[5], "Yes", "Alt attributes gets translated.");
assert.equal(data[6], "B", "Accesskey gets translated.");
assert.equal(data[7], "Yes", "Aria-Label gets translated.");
assert.equal(data[8], "Value", "Aria-valuetext gets translated.");
assert.equal(data[9], "Hint", "Aria-moz-hint gets translated.");
assert.equal(data[10], "Yes", "Form placeholders are translateable.");
assert.equal(data[11], "Yes", "Labels of select options and context menus are translateable.");
tab.close(done);
}
});

View File

@ -5,7 +5,7 @@
"use strict";
const LOCAL_URI = "about:robots";
const REMOTE_URI = "about:home";
const REMOTE_URI = "data:text/html;charset=utf-8,remote";
const { Loader } = require('sdk/test/loader');
const { getTabs, openTab, closeTab, setTabURL, getBrowserForTab, getURI } = require('sdk/tabs/utils');
@ -21,6 +21,19 @@ const { set } = require('sdk/preferences/service');
// The hidden preload browser messes up our frame counts
set('browser.newtab.preload', false);
function promiseTabFrameAttach(frames) {
return new Promise(resolve => {
let listener = function(frame, ...args) {
if (!frame.isTab)
return;
frames.off("attach", listener);
resolve([frame, ...args]);
}
frames.on("attach", listener);
});
}
// Check that we see a process stop and start
exports["test process restart"] = function*(assert) {
if (!isE10S) {
@ -44,7 +57,7 @@ exports["test process restart"] = function*(assert) {
// Switch the remote tab to a local URI which should kill the remote process
let frameDetach = promiseEventOnItemAndContainer(assert, remoteFrame, frames, 'detach');
let frameAttach = promiseEvent(frames, 'attach');
let frameAttach = promiseTabFrameAttach(frames);
let processDetach = promiseEventOnItemAndContainer(assert, remoteProcess, processes, 'detach');
setTabURL(tab, LOCAL_URI);
// The load should kill the remote frame
@ -57,7 +70,7 @@ exports["test process restart"] = function*(assert) {
frameDetach = promiseEventOnItemAndContainer(assert, newFrame, frames, 'detach');
let processAttach = promiseEvent(processes, 'attach');
frameAttach = promiseEvent(frames, 'attach');
frameAttach = promiseTabFrameAttach(frames);
setTabURL(tab, REMOTE_URI);
// The load should kill the remote frame
yield frameDetach;
@ -149,7 +162,7 @@ exports["test frame list"] = function*(assert) {
assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
let promise = promiseEvent(frames, 'attach');
let promise = promiseTabFrameAttach(frames);
let tab1 = openTab(window, LOCAL_URI);
let [frame1] = yield promise;
assert.ok(!!frame1, "Should have seen the new frame");
@ -158,7 +171,7 @@ exports["test frame list"] = function*(assert) {
assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
promise = promiseEvent(frames, 'attach');
promise = promiseTabFrameAttach(frames);
let tab2 = openTab(window, REMOTE_URI);
let [frame2] = yield promise;
assert.ok(!!frame2, "Should have seen the new frame");
@ -256,7 +269,7 @@ exports["test unload"] = function*(assert) {
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = promiseEvent(frames, 'attach');
let promise = promiseTabFrameAttach(frames);
let tab = openTab(window, "data:,<html/>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
@ -280,7 +293,7 @@ exports["test frame detach on unload"] = function*(assert) {
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = promiseEvent(frames, 'attach');
let promise = promiseTabFrameAttach(frames);
let tab = openTab(window, "data:,<html/>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
@ -304,7 +317,7 @@ exports["test frame event listeners"] = function*(assert) {
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = promiseEvent(frames, 'attach');
let promise = promiseTabFrameAttach(frames);
let tab = openTab(window, "data:text/html,<html></html>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
@ -339,7 +352,7 @@ exports["test frames event listeners"] = function*(assert) {
let loader = new Loader(module);
let { frames } = yield waitForProcesses(loader);
let promise = promiseEvent(frames, 'attach');
let promise = promiseTabFrameAttach(frames);
let tab = openTab(window, "data:text/html,<html></html>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
@ -377,8 +390,8 @@ exports["test unload removes frame event listeners"] = function*(assert) {
let loader2 = new Loader(module);
let { frames: frames2 } = yield waitForProcesses(loader2);
let promise = promiseEvent(frames, 'attach');
let promise2 = promiseEvent(frames2, 'attach');
let promise = promiseTabFrameAttach(frames);
let promise2 = promiseTabFrameAttach(frames2);
let tab = openTab(window, "data:text/html,<html></html>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);
@ -418,8 +431,8 @@ exports["test unload removes frames event listeners"] = function*(assert) {
let loader2 = new Loader(module);
let { frames: frames2 } = yield waitForProcesses(loader2);
let promise = promiseEvent(frames, 'attach');
let promise2 = promiseEvent(frames2, 'attach');
let promise = promiseTabFrameAttach(frames);
let promise2 = promiseTabFrameAttach(frames2);
let tab = openTab(window, "data:text/html,<html></html>");
let browser = getBrowserForTab(tab);
yield promiseDOMEvent(browser, "load", true);

View File

@ -62,6 +62,7 @@ skip-if = true
[test-environment.js]
[test-errors.js]
[test-event-core.js]
[test-event-dom.js]
[test-event-target.js]
[test-event-utils.js]
[test-events.js]

View File

@ -13,6 +13,7 @@
"browser.aboutHomeSnippets.updateUrl": "https://localhost/snippet-dummy",
"browser.newtab.url": "about:blank",
"browser.search.update": false,
"browser.search.suggest.enabled": false,
"browser.safebrowsing.enabled": false,
"browser.safebrowsing.updateURL": "http://localhost/safebrowsing-dummy/update",
"browser.safebrowsing.gethashURL": "http://localhost/safebrowsing-dummy/gethash",

View File

@ -10,6 +10,7 @@ const { getMode } = require('sdk/private-browsing/utils');
const { browserWindows: windows } = require('sdk/windows');
const { defer } = require('sdk/core/promise');
const tabs = require('sdk/tabs');
const { getMostRecentBrowserWindow } = require('sdk/window/utils');
// test openDialog() from window/utils with private option
// test isActive state in pwpb case
@ -80,27 +81,22 @@ exports.testIsPrivateOnWindowOpenFromPrivate = function(assert, done) {
};
exports.testOpenTabWithPrivateWindow = function*(assert) {
let { promise, resolve } = defer();
let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true });
let window = yield openPromise(null, {
features: {
private: true,
toolbar: true
}
});
yield focus(window);
assert.pass("loading new private window");
yield promise(window, 'load').then(focus);
assert.equal(isPrivate(window), true, 'the focused window is private');
tabs.open({
yield new Promise(resolve => tabs.open({
url: 'about:blank',
onOpen: (tab) => {
assert.equal(isPrivate(tab), false, 'the opened tab is not private');
tab.close(resolve);
}
});
}));
yield promise;
yield close(window);
};

View File

@ -0,0 +1,24 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { openTab: makeTab, getTabContentWindow } = require("sdk/tabs/utils");
function openTab(rawWindow, url) {
return new Promise(resolve => {
let tab = makeTab(rawWindow, url);
let window = getTabContentWindow(tab);
if (window.document.readyState == "complete") {
return resolve();
}
window.addEventListener("load", function onLoad() {
window.removeEventListener("load", onLoad, true);
resolve();
}, true);
return null;
})
}
exports.openTab = openTab;

View File

@ -0,0 +1,92 @@
/* 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 { openWindow, closeWindow } = require('./util');
const { Loader } = require('sdk/test/loader');
const { getMostRecentBrowserWindow } = require('sdk/window/utils');
const { Cc, Ci } = require('chrome');
const els = Cc["@mozilla.org/eventlistenerservice;1"].
getService(Ci.nsIEventListenerService);
function countListeners(target, type) {
let listeners = els.getListenerInfoFor(target, {});
return listeners.filter(listener => listener.type == type).length;
}
exports['test window close clears listeners'] = function(assert) {
let window = yield openWindow();
let loader = Loader(module);
// Any element will do here
let gBrowser = window.gBrowser;
// Other parts of the app may be listening for this
let windowListeners = countListeners(window, "DOMWindowClose");
// We can assume we're the only ones using the test events
assert.equal(countListeners(gBrowser, "TestEvent1"), 0, "Should be no listener for test event 1");
assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2");
let { open } = loader.require('sdk/event/dom');
open(gBrowser, "TestEvent1");
assert.equal(countListeners(window, "DOMWindowClose"), windowListeners + 1,
"Should have added a DOMWindowClose listener");
assert.equal(countListeners(gBrowser, "TestEvent1"), 1, "Should be a listener for test event 1");
assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2");
open(gBrowser, "TestEvent2");
assert.equal(countListeners(window, "DOMWindowClose"), windowListeners + 1,
"Should not have added another DOMWindowClose listener");
assert.equal(countListeners(gBrowser, "TestEvent1"), 1, "Should be a listener for test event 1");
assert.equal(countListeners(gBrowser, "TestEvent2"), 1, "Should be a listener for test event 2");
window = yield closeWindow(window);
assert.equal(countListeners(window, "DOMWindowClose"), windowListeners,
"Should have removed a DOMWindowClose listener");
assert.equal(countListeners(gBrowser, "TestEvent1"), 0, "Should be no listener for test event 1");
assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2");
loader.unload();
};
exports['test unload clears listeners'] = function(assert) {
let window = getMostRecentBrowserWindow();
let loader = Loader(module);
// Any element will do here
let gBrowser = window.gBrowser;
// Other parts of the app may be listening for this
let windowListeners = countListeners(window, "DOMWindowClose");
// We can assume we're the only ones using the test events
assert.equal(countListeners(gBrowser, "TestEvent1"), 0, "Should be no listener for test event 1");
assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2");
let { open } = loader.require('sdk/event/dom');
open(gBrowser, "TestEvent1");
assert.equal(countListeners(window, "DOMWindowClose"), windowListeners + 1,
"Should have added a DOMWindowClose listener");
assert.equal(countListeners(gBrowser, "TestEvent1"), 1, "Should be a listener for test event 1");
assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2");
open(gBrowser, "TestEvent2");
assert.equal(countListeners(window, "DOMWindowClose"), windowListeners + 1,
"Should not have added another DOMWindowClose listener");
assert.equal(countListeners(gBrowser, "TestEvent1"), 1, "Should be a listener for test event 1");
assert.equal(countListeners(gBrowser, "TestEvent2"), 1, "Should be a listener for test event 2");
loader.unload();
assert.equal(countListeners(window, "DOMWindowClose"), windowListeners,
"Should have removed a DOMWindowClose listener");
assert.equal(countListeners(gBrowser, "TestEvent1"), 0, "Should be no listener for test event 1");
assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2");
};
require('sdk/test').run(exports);

View File

@ -48,6 +48,14 @@ exports['test nodeResolve'] = function (assert) {
'./node_modules/test-math/node_modules/test-add/index.js',
'Dependencies\' dependencies can be found');
resolveTest('resource://gre/modules/commonjs/sdk/tabs.js', './index.js', undefined,
'correctly ignores absolute URIs.');
resolveTest('../tabs', 'resource://gre/modules/commonjs/sdk/addon/bootstrap.js', undefined,
'correctly ignores attempts to resolve from a module at an absolute URI.');
resolveTest('sdk/tabs', 'resource://gre/modules/commonjs/sdk/addon/bootstrap.js', undefined,
'correctly ignores attempts to resolve from a module at an absolute URI.');
function resolveTest (id, requirer, expected, msg) {
let result = nodeResolve(id, requirer, { manifest: manifest, rootURI: rootURI });

View File

@ -1491,12 +1491,17 @@ exports.testShowHideRawWindowArg = function*(assert) {
const { Sidebar } = require('sdk/ui/sidebar');
let testName = 'testShowHideRawWindowArg';
assert.pass("Creating sidebar");
let sidebar = Sidebar({
id: testName,
title: testName,
url: 'data:text/html;charset=utf-8,' + testName
});
assert.pass("Created sidebar");
let mainWindow = getMostRecentBrowserWindow();
let newWindow = yield windowPromise(mainWindow.OpenBrowserWindow(), 'load');
assert.pass("Created the new window");
@ -1504,21 +1509,26 @@ exports.testShowHideRawWindowArg = function*(assert) {
yield focus(newWindow);
assert.pass("Focused the new window");
yield focus(mainWindow);
assert.pass("Focused the old window");
let newWindow2 = yield windowPromise(mainWindow.OpenBrowserWindow(), 'load');
assert.pass("Created the second new window");
yield focus(newWindow2);
assert.pass("Focused the second new window");
yield sidebar.show(newWindow);
assert.pass('the sidebar was shown');
assert.equal(isSidebarShowing(mainWindow), false, 'sidebar is not showing in main window');
assert.equal(isSidebarShowing(newWindow2), false, 'sidebar is not showing in second window');
assert.equal(isSidebarShowing(newWindow), true, 'sidebar is showing in new window');
assert.ok(isFocused(mainWindow), 'main window is still focused');
assert.ok(isFocused(newWindow2), 'main window is still focused');
yield sidebar.hide(newWindow);
assert.equal(isFocused(mainWindow), true, 'main window is still focused');
assert.equal(isFocused(newWindow2), true, 'second window is still focused');
assert.equal(isSidebarShowing(mainWindow), false, 'sidebar is not showing in main window');
assert.equal(isSidebarShowing(newWindow2), false, 'sidebar is not showing in second window');
assert.equal(isSidebarShowing(newWindow), false, 'sidebar is not showing in new window');
sidebar.destroy();

View File

@ -31,7 +31,7 @@ const openWindow = () => {
exports.openWindow = openWindow;
const closeWindow = (window) => {
const closed = when(window, "unload", true).then(_target);
const closed = when(window, "unload", true).then(_ => window);
window.close();
return closed;
};

View File

@ -7,7 +7,7 @@ const { Cc, Ci } = require('chrome');
const { setTimeout } = require('sdk/timers');
const { Loader } = require('sdk/test/loader');
const { onFocus, getMostRecentWindow, windows, isBrowser, getWindowTitle, isFocused } = require('sdk/window/utils');
const { open, close, focus } = require('sdk/window/helpers');
const { open, close, focus, promise: windowPromise } = require('sdk/window/helpers');
const { browserWindows } = require("sdk/windows");
const tabs = require("sdk/tabs");
const winUtils = require("sdk/deprecated/window-utils");
@ -17,6 +17,9 @@ const { viewFor } = require("sdk/view/core");
const { defer } = require("sdk/lang/functional");
const { cleanUI } = require("sdk/test/utils");
const { after } = require("sdk/test/utils");
const { merge } = require("sdk/util/object");
const self = require("sdk/self");
const { openTab } = require("../tabs/utils");
// TEST: open & close window
exports.testOpenAndCloseWindow = function(assert, done) {
@ -59,12 +62,9 @@ exports.testNeWindowIsFocused = function(assert, done) {
});
}
exports.testOpenRelativePathWindow = function(assert, done) {
exports.testOpenRelativePathWindow = function*(assert) {
assert.equal(browserWindows.length, 1, "Only one window open");
const { merge } = require("sdk/util/object");
const self = require("sdk/self");
let loader = Loader(module, null, null, {
modules: {
"sdk/self": merge({}, self, {
@ -72,17 +72,31 @@ exports.testOpenRelativePathWindow = function(assert, done) {
})
}
});
assert.pass("Created a new loader");
loader.require("sdk/windows").browserWindows.open({
url: "./test.html",
onOpen: (window) => {
window.tabs.activeTab.once("ready", (tab) => {
assert.equal(tab.title, "foo",
"tab opened a document with relative path");
done();
});
}
})
let tabReady = new Promise(resolve => {
loader.require("sdk/tabs").on("ready", (tab) => {
if (!/test\.html$/.test(tab.url))
return;
assert.equal(tab.title, "foo",
"tab opened a document with relative path");
resolve();
});
});
yield new Promise(resolve => {
loader.require("sdk/windows").browserWindows.open({
url: "./test.html",
onOpen: (window) => {
assert.pass("Created a new window");
resolve();
}
})
});
yield tabReady;
loader.unload();
}
exports.testAutomaticDestroy = function(assert, done) {
@ -218,52 +232,26 @@ exports.testOnOpenOnCloseListeners = function(assert, done) {
exports.testActiveWindow = function*(assert) {
let windows = browserWindows;
// API window objects
let window2, window3;
let window = getMostRecentWindow();
// Raw window objects
let rawWindow2, rawWindow3;
let rawWindow2 = yield windowPromise(window.OpenBrowserWindow(), "load").then(focus);
assert.pass("Window 2 was created");
yield new Promise(resolve => {
windows.open({
url: "data:text/html;charset=utf-8,<title>window 2</title>",
onOpen: (window) => {
assert.pass('window 2 open');
// open a tab in window 2
yield openTab(rawWindow2, "data:text/html;charset=utf-8,<title>window 2</title>");
window.tabs.activeTab.once('ready', () => {
assert.pass('window 2 tab activated');
assert.equal(rawWindow2.content.document.title, "window 2", "Got correct raw window 2");
assert.equal(rawWindow2.document.title, windows[1].title, "Saw correct title on window 2");
window2 = window;
rawWindow2 = viewFor(window);
let rawWindow3 = yield windowPromise(window.OpenBrowserWindow(), "load").then(focus);;
assert.pass("Window 3 was created");
assert.equal(rawWindow2.content.document.title, "window 2", "Got correct raw window 2");
assert.equal(rawWindow2.document.title, window2.title, "Saw correct title on window 2");
// open a tab in window 3
yield openTab(rawWindow3, "data:text/html;charset=utf-8,<title>window 3</title>");
windows.open({
url: "data:text/html;charset=utf-8,<title>window 3</title>",
onOpen: (window) => {
assert.pass('window 3 open');
window.tabs.activeTab.once('ready', () => {
assert.pass('window 3 tab activated');
window3 = window;
rawWindow3 = viewFor(window);
assert.equal(rawWindow3.content.document.title, "window 3", "Got correct raw window 3");
assert.equal(rawWindow3.document.title, window3.title, "Saw correct title on window 3");
resolve();
});
}
});
});
}
});
});
yield focus(rawWindow3);
assert.equal(rawWindow3.content.document.title, "window 3", "Got correct raw window 3");
assert.equal(rawWindow3.document.title, windows[2].title, "Saw correct title on window 3");
assert.equal(windows.length, 3, "Correct number of browser windows");
@ -272,11 +260,13 @@ exports.testActiveWindow = function*(assert) {
count++;
}
assert.equal(count, 3, "Correct number of windows returned by iterator");
assert.equal(windows.activeWindow.title, window3.title, "Correct active window title - 3");
assert.equal(windows.activeWindow.title, windows[2].title, "Correct active window title - 3");
let window3 = windows[2];
yield focus(rawWindow2);
assert.equal(windows.activeWindow.title, window2.title, "Correct active window title - 2");
assert.equal(windows.activeWindow.title, windows[1].title, "Correct active window title - 2");
let window2 = windows[1];
yield new Promise(resolve => {
onFocus(rawWindow2).then(resolve);
@ -284,7 +274,7 @@ exports.testActiveWindow = function*(assert) {
assert.pass("activating window2");
});
assert.equal(windows.activeWindow.title, window2.title, "Correct active window - 2");
assert.equal(windows.activeWindow.title, windows[1].title, "Correct active window - 2");
yield new Promise(resolve => {
onFocus(rawWindow3).then(resolve);
@ -391,21 +381,32 @@ exports.testTrackWindows = function(assert, done) {
}
// test that it is not possible to open a private window by default
exports.testWindowOpenPrivateDefault = function(assert, done) {
browserWindows.open({
url: 'about:mozilla',
exports.testWindowOpenPrivateDefault = function*(assert) {
const TITLE = "yo";
const URL = "data:text/html,<title>" + TITLE + "</title>";
let tabReady = new Promise(resolve => {
tabs.on('ready', function onTabReady(tab) {
if (tab.url != URL)
return;
tabs.removeListener('ready', onTabReady);
assert.equal(tab.title, TITLE, 'opened correct tab');
assert.equal(isPrivate(tab), false, 'tab is not private');
resolve();
});
})
yield new Promise(resolve => browserWindows.open({
url: URL,
isPrivate: true,
onOpen: function(window) {
let tab = window.tabs[0];
tab.once('ready', function() {
assert.equal(tab.url, 'about:mozilla', 'opened correct tab');
assert.equal(isPrivate(tab), false, 'tab is not private');
done();
});
assert.pass("the new window was opened");
resolve();
}
});
}));
yield tabReady;
}
// test that it is not possible to find a private window in

View File

@ -490,7 +490,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/Panel
let wasVisible = false;
// Hide the infobar from the previous tab.
if (event.detail.previousTabfromTab) {
if (event.detail.previousTab) {
wasVisible = this._hideBrowserSharingInfoBar(false, event.detail.previousTab.linkedBrowser);
}

View File

@ -62,7 +62,7 @@
<popupnotification id="password-notification" hidden="true">
<popupnotificationcontent orient="vertical">
<textbox id="password-notification-username" disabled="true"/>
<textbox id="password-notification-username"/>
<textbox id="password-notification-password" type="password"
disabled="true"/>
</popupnotificationcontent>

View File

@ -14,6 +14,6 @@ function startMiddleClickTestCase(aTestNumber) {
}
function test() {
requestLongerTimeout(5); // slowwww shutdown on e10s
requestLongerTimeout(10); // slowwww shutdown on e10s
startReferrerTest(startMiddleClickTestCase);
}

View File

@ -17,6 +17,6 @@ function startNewPrivateWindowTestCase(aTestNumber) {
}
function test() {
requestLongerTimeout(5); // slowwww shutdown on e10s
requestLongerTimeout(10); // slowwww shutdown on e10s
startReferrerTest(startNewPrivateWindowTestCase);
}

View File

@ -16,6 +16,6 @@ function startNewTabTestCase(aTestNumber) {
}
function test() {
requestLongerTimeout(5); // slowwww shutdown on e10s
requestLongerTimeout(10); // slowwww shutdown on e10s
startReferrerTest(startNewTabTestCase);
}

View File

@ -17,6 +17,6 @@ function startNewWindowTestCase(aTestNumber) {
}
function test() {
requestLongerTimeout(5); // slowwww shutdown on e10s
requestLongerTimeout(10); // slowwww shutdown on e10s
startReferrerTest(startNewWindowTestCase);
}

View File

@ -13,6 +13,6 @@ function startSimpleClickTestCase(aTestNumber) {
};
function test() {
requestLongerTimeout(5); // slowwww shutdown on e10s
requestLongerTimeout(10); // slowwww shutdown on e10s
startReferrerTest(startSimpleClickTestCase);
}

View File

@ -90,7 +90,7 @@
<hbox align="center">
<label control="defaultFont" accesskey="&defaultFont.accesskey;">&defaultFont.label;</label>
<menulist id="defaultFont" flex="1"/>
<label control="defaultFontSize" accesskey="&defaultSize.accesskey;">&defaultSize.label;</label>
<label id="defaultFontSizeLabel" control="defaultFontSize" accesskey="&defaultSize.accesskey;">&defaultSize.label;</label>
<menulist id="defaultFontSize">
<menupopup>
<menuitem value="9" label="9"/>

View File

@ -72,14 +72,9 @@ let SessionMigrationInternal = {
win._closedTabs = [];
return win;
});
let wrappedState = {
url: "about:welcomeback",
formdata: {
id: {"sessionData": state},
xpath: {}
}
};
return {windows: [{tabs: [{entries: [wrappedState]}]}]};
let url = "about:welcomeback";
let formdata = {id: {sessionData: state}, url};
return {windows: [{tabs: [{entries: [{url}], formdata}]}]};
},
/**
* Asynchronously read session restore state (JSON) from a path

View File

@ -470,14 +470,9 @@ let SessionStoreInternal = {
if (this._needsRestorePage(state, this._recentCrashes)) {
// replace the crashed session with a restore-page-only session
let pageData = {
url: "about:sessionrestore",
formdata: {
id: { "sessionData": state },
xpath: {}
}
};
state = { windows: [{ tabs: [{ entries: [pageData] }] }] };
let url = "about:sessionrestore";
let formdata = {id: {sessionData: state}, url};
state = { windows: [{ tabs: [{ entries: [{url}], formdata }] }] };
} else if (this._hasSingleTabWithURL(state.windows,
"about:welcomeback")) {
// On a single about:welcomeback URL that crashed, replace about:welcomeback

View File

@ -3,6 +3,8 @@
"use strict";
let HiddenFrame = Cu.import("resource:///modules/HiddenFrame.jsm", {}).HiddenFrame;
const HTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
@ -14,33 +16,25 @@ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
* The URL to open in the browser.
**/
function createHiddenBrowser(aURL) {
let deferred = Promise.defer();
let hiddenDoc = Services.appShell.hiddenDOMWindow.document;
let frame = new HiddenFrame();
return new Promise(resolve =>
frame.get().then(aFrame => {
let doc = aFrame.document;
let browser = doc.createElementNS(XUL_NS, "browser");
browser.setAttribute("type", "content");
browser.setAttribute("disableglobalhistory", "true");
browser.setAttribute("src", aURL);
// Create a HTML iframe with a chrome URL, then this can host the browser.
let iframe = hiddenDoc.createElementNS(HTML_NS, "iframe");
iframe.setAttribute("src", "chrome://global/content/mozilla.xhtml");
iframe.addEventListener("load", function onLoad() {
iframe.removeEventListener("load", onLoad, true);
let browser = iframe.contentDocument.createElementNS(XUL_NS, "browser");
browser.setAttribute("type", "content");
browser.setAttribute("disableglobalhistory", "true");
browser.setAttribute("src", aURL);
iframe.contentDocument.documentElement.appendChild(browser);
deferred.resolve({frame: iframe, browser: browser});
}, true);
hiddenDoc.documentElement.appendChild(iframe);
return deferred.promise;
};
doc.documentElement.appendChild(browser);
resolve({frame: frame, browser: browser});
}));
}
/**
* Remove the browser and the iframe.
* Remove the browser and the HiddenFrame.
*
* @param aFrame
* The iframe to dismiss.
* The HiddenFrame to dismiss.
* @param aBrowser
* The browser to dismiss.
*/
@ -49,9 +43,7 @@ function destroyHiddenBrowser(aFrame, aBrowser) {
aBrowser.remove();
// Take care of the frame holding our invisible browser.
if (!Cu.isDeadWrapper(aFrame)) {
aFrame.remove();
}
aFrame.destroy();
};
/**

View File

@ -496,10 +496,22 @@ ThreadState.prototype = {
// Ignore "interrupted" events, to avoid UI flicker. These are generated
// by the slow script dialog and internal events such as setting
// breakpoints. Pressing the resume button does need to be shown, though.
if (aEvent == "paused" &&
aPacket.why.type == "interrupted" &&
!this.interruptedByResumeButton) {
return;
if (aEvent == "paused") {
if (aPacket.why.type == "interrupted" &&
!this.interruptedByResumeButton) {
return;
} else if (aPacket.why.type == "breakpointConditionThrown" && aPacket.why.message) {
let where = aPacket.frame.where;
let aLocation = {
line: where.line,
column: where.column,
actor: where.source ? where.source.actor : null
};
DebuggerView.Sources.showBreakpointConditionThrownMessage(
aLocation,
aPacket.why.message
);
}
}
this.interruptedByResumeButton = false;
@ -590,6 +602,10 @@ StackFrames.prototype = {
case "breakpoint":
this._currentBreakpointLocation = aPacket.frame.where;
break;
case "breakpointConditionThrown":
this._currentBreakpointLocation = aPacket.frame.where;
this._conditionThrowMessage = aPacket.why.message;
break;
// If paused by a client evaluation, store the evaluated value.
case "clientEvaluated":
this._currentEvaluation = aPacket.why.frameFinished;

View File

@ -450,6 +450,19 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
this._unselectBreakpoint();
},
/**
* Display the message thrown on breakpoint condition
*/
showBreakpointConditionThrownMessage: function(aLocation, aMessage = "") {
let breakpointItem = this.getBreakpoint(aLocation);
if (!breakpointItem) {
return;
}
let attachment = breakpointItem.attachment;
attachment.view.container.classList.add("dbg-breakpoint-condition-thrown");
attachment.view.message.setAttribute("value", aMessage);
},
/**
* Update the checked/unchecked and enabled/disabled states of the buttons in
* the sources toolbar based on the currently selected source's state.
@ -689,12 +702,13 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
* - location: the breakpoint's source location and line number
* - disabled: the breakpoint's disabled state, boolean
* - text: the breakpoint's line text to be displayed
* - message: thrown string when the breakpoint condition throws,
* @return object
* An object containing the breakpoint container, checkbox,
* line number and line text nodes.
*/
_createBreakpointView: function(aOptions) {
let { location, disabled, text } = aOptions;
let { location, disabled, text, message } = aOptions;
let identifier = DebuggerController.Breakpoints.getIdentifier(location);
let checkbox = document.createElement("checkbox");
@ -714,6 +728,26 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
let tooltip = text ? text.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH) : "";
lineTextNode.setAttribute("tooltiptext", tooltip);
let thrownNode = document.createElement("label");
thrownNode.className = "plain dbg-breakpoint-condition-thrown-message dbg-breakpoint-text";
thrownNode.setAttribute("value", message);
thrownNode.setAttribute("crop", "end");
thrownNode.setAttribute("flex", "1");
let bpLineContainer = document.createElement("hbox");
bpLineContainer.className = "plain dbg-breakpoint-line-container";
bpLineContainer.setAttribute("flex", "1");
bpLineContainer.appendChild(lineNumberNode);
bpLineContainer.appendChild(lineTextNode);
let bpDetailContainer = document.createElement("vbox");
bpDetailContainer.className = "plain dbg-breakpoint-detail-container";
bpDetailContainer.setAttribute("flex", "1");
bpDetailContainer.appendChild(bpLineContainer);
bpDetailContainer.appendChild(thrownNode);
let container = document.createElement("hbox");
container.id = "breakpoint-" + identifier;
container.className = "dbg-breakpoint side-menu-widget-item-other";
@ -725,14 +759,14 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
checkbox.addEventListener("click", this._onBreakpointCheckboxClick, false);
container.appendChild(checkbox);
container.appendChild(lineNumberNode);
container.appendChild(lineTextNode);
container.appendChild(bpDetailContainer);
return {
container: container,
checkbox: checkbox,
lineNumber: lineNumberNode,
lineText: lineTextNode
lineText: lineTextNode,
message: thrownNode
};
},

View File

@ -48,3 +48,13 @@
#body[layout=vertical] #stackframes {
visibility: hidden;
}
#source-progress-container {
display: flex;
flex-flow: column;
justify-content: center;
}
#source-progress {
flex: none;
}

View File

@ -15,6 +15,7 @@
<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
macanimationtype="document"
fullscreenbutton="true"
screenX="4" screenY="4"
@ -421,12 +422,12 @@
label="&debuggerUI.blackBoxMessage.unBlackBoxButton;"
command="unBlackBoxCommand"/>
</vbox>
<vbox id="source-progress-container"
align="center"
pack="center">
<progressmeter id="source-progress"
mode="undetermined"/>
</vbox>
<html:div id="source-progress-container"
align="center">
<html:div id="hbox">
<html:progress id="source-progress"></html:progress>
</html:div>
</html:div>
</deck>
<splitter id="editor-and-instruments-splitter"
class="devtools-side-splitter"/>

View File

@ -143,6 +143,8 @@ skip-if = e10s # TODO
skip-if = e10s # Bug 1093535
[browser_dbg_breakpoints-button-01.js]
[browser_dbg_breakpoints-button-02.js]
[browser_dbg_breakpoints-condition-thrown-message.js]
skip-if = e10s && debug
[browser_dbg_breakpoints-contextmenu-add.js]
[browser_dbg_breakpoints-contextmenu.js]
[browser_dbg_breakpoints-disabled-reload.js]

View File

@ -0,0 +1,106 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Make sure that the message which breakpoint condition throws
* could be displayed on UI correctly
*/
const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html";
function test() {
let gTab, gPanel, gDebugger, gEditor;
let gSources, gLocation;
initDebugger(TAB_URL).then(([aTab,, aPanel]) => {
gTab = aTab;
gPanel = aPanel;
gDebugger = gPanel.panelWin;
gEditor = gDebugger.DebuggerView.editor;
gSources = gDebugger.DebuggerView.Sources;
waitForSourceAndCaretAndScopes(gPanel, ".html", 17)
.then(addBreakpoints)
.then(() => resumeAndTestThrownMessage(18))
.then(() => resumeAndTestNoThrownMessage(19))
.then(() => resumeAndTestThrownMessage(22))
.then(() => resumeAndFinishTest())
.then(() => closeDebuggerAndFinish(gPanel))
.then(null, aError => {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
});
callInTab(gTab, "ermahgerd");
});
function resumeAndTestThrownMessage(aLine) {
EventUtils.sendMouseEvent({ type: "mousedown" },
gDebugger.document.getElementById("resume"),
gDebugger);
let finished = waitForCaretUpdated(gPanel, aLine).then(() => {
//test that the thrown message is correctly shown
let attachment = gSources.getBreakpoint({ actor: gSources.values[0], line: aLine}).attachment;
ok(attachment.view.container.classList.contains('dbg-breakpoint-condition-thrown'),
"Message on line " + aLine + " should be shown when condition throws.");
});
return finished;
}
function resumeAndTestNoThrownMessage(aLine) {
EventUtils.sendMouseEvent({ type: "mousedown" },
gDebugger.document.getElementById("resume"),
gDebugger);
let finished = waitForCaretUpdated(gPanel, aLine).then(() => {
//test that the thrown message is correctly shown
let attachment = gSources.getBreakpoint({ actor: gSources.values[0], line: aLine}).attachment;
ok(!attachment.view.container.classList.contains("dbg-breakpoint-condition-thrown"),
"Message on line " + aLine + " should be hidden if condition doesn't throw.");
});
return finished;
}
function resumeAndFinishTest() {
let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED)
gDebugger.gThreadClient.resume();
return finished;
}
function addBreakpoints() {
return promise.resolve(null)
.then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue,
line: 18,
condition: " 1a"}))
.then(() => initialCheck(18))
.then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue,
line: 19,
condition: "true"}))
.then(() => initialCheck(19))
.then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue,
line: 20,
condition: "false"}))
.then(() => initialCheck(20))
.then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue,
line: 22,
condition: "randomVar"}))
.then(() => initialCheck(22));
}
function initialCheck(aCaretLine) {
let bp = gSources.getBreakpoint({ actor: gSources.values[0], line: aCaretLine})
let attachment = bp.attachment;
ok(attachment,
"There should be an item for line " + aCaretLine + " in the sources pane.");
let thrownNode = attachment.view.container.querySelector(".dbg-breakpoint-condition-thrown-message");
ok(thrownNode,
"The breakpoint item should contain a thrown message node.")
ok(!attachment.view.container.classList.contains("dbg-breakpoint-condition-thrown"),
"The thrown message on line " + aCaretLine + " should be hidden when condition has not been evaluated.")
}
}

View File

@ -435,11 +435,8 @@ function Rule(aElementStyle, aOptions) {
this.keyframes = aOptions.keyframes || null;
this._modificationDepth = 0;
if (this.domRule) {
let parentRule = this.domRule.parentRule;
if (parentRule && parentRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE) {
this.mediaText = parentRule.mediaText;
}
if (this.domRule && this.domRule.mediaText) {
this.mediaText = this.domRule.mediaText;
}
// Populate the text properties with the style's current cssText
@ -507,7 +504,7 @@ Rule.prototype = {
* The rule's line within a stylesheet
*/
get ruleLine() {
return this.domRule ? this.domRule.line : null;
return this.domRule ? this.domRule.line : "";
},
/**
@ -529,10 +526,12 @@ Rule.prototype = {
if (this._originalSourceStrings) {
return promise.resolve(this._originalSourceStrings);
}
return this.domRule.getOriginalLocation().then(({href, line}) => {
return this.domRule.getOriginalLocation().then(({href, line, mediaText}) => {
let mediaString = mediaText ? " @" + mediaText : "";
let sourceStrings = {
full: href + ":" + line,
short: CssLogic.shortSource({href: href}) + ":" + line
full: (href || CssLogic.l10n("rule.sourceInline")) + ":" + line + mediaString,
short: CssLogic.shortSource({href: href}) + ":" + line + mediaString
};
this._originalSourceStrings = sourceStrings;

View File

@ -12,8 +12,10 @@ add_task(function*() {
info("Creating the test document");
let style = "" +
"#testid {" +
" background-color: blue;" +
"@media screen and (min-width: 10px) {" +
" #testid {" +
" background-color: blue;" +
" }" +
"}" +
".testclass, .unmatched {" +
" background-color: green;" +
@ -35,6 +37,15 @@ function* testContentAfterNodeSelection(inspector, ruleView) {
"After highlighting null, has a no-results element again.");
yield selectNode("#testid", inspector);
let linkText = getRuleViewLinkTextByIndex(ruleView, 1);
is(linkText, "inline:1 @screen and (min-width: 10px)",
"link text at index 1 contains media query text.");
linkText = getRuleViewLinkTextByIndex(ruleView, 2);
is(linkText, "inline:1",
"link text at index 2 contains no media query text.");
let classEditor = getRuleViewRuleEditor(ruleView, 2);
is(classEditor.selectorText.querySelector(".ruleview-selector-matched").textContent,
".testclass", ".textclass should be matched.");

View File

@ -693,6 +693,17 @@ function getRuleViewLinkByIndex(view, index) {
return links[index];
}
/**
* Get rule-link text from the rule-view given its index
* @param {CssRuleView} view The instance of the rule-view panel
* @param {Number} index The index of the link to get
* @return {String} The string at this index
*/
function getRuleViewLinkTextByIndex(view, index) {
let link = getRuleViewLinkByIndex(view, index);
return link.querySelector(".source-link-label").value;
}
/**
* Get the rule editor from the rule-view given its index
* @param {CssRuleView} view The instance of the rule-view panel

View File

@ -302,9 +302,8 @@ let UI = {
},
busyUntil: function(promise, operationDescription) {
// Freeze the UI until the promise is resolved. A 30s timeout
// will unfreeze the UI, just in case the promise never gets
// resolved.
// Freeze the UI until the promise is resolved. A timeout will unfreeze the
// UI, just in case the promise never gets resolved.
this._busyPromise = promise;
this._busyOperationDescription = operationDescription;
this.setupBusyTimeout();
@ -469,7 +468,13 @@ let UI = {
// |busyUntil| will listen for rejections.
// Bug 1121100 may find a better way to silence these.
});
return this.busyUntil(promise, "Connecting to " + name);
promise = this.busyUntil(promise, "Connecting to " + name);
// Stop busy timeout for runtimes that take unknown or long amounts of time
// to connect.
if (runtime.prolongedConnection) {
this.cancelBusyTimeout();
}
return promise;
},
updateRuntimeButton: function() {

View File

@ -66,6 +66,10 @@ const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/
* |name| field
* A user-visible label to identify the runtime that will be displayed in a
* runtime list.
* |prolongedConnection| field
* A boolean value which should be |true| if the connection process is
* expected to take a unknown or large amount of time. A UI may use this as a
* hint to skip timeouts or other time-based code paths.
* connect()
* Configure the passed |connection| object with any settings need to
* successfully connect to the runtime, and call the |connection|'s connect()
@ -446,6 +450,8 @@ function WiFiRuntime(deviceName) {
WiFiRuntime.prototype = {
type: RuntimeTypes.WIFI,
// Mark runtime as taking a long time to connect
prolongedConnection: true,
connect: function(connection) {
let service = discovery.getRemoteService("devtools", this.deviceName);
if (!service) {

View File

@ -37,6 +37,7 @@ SimpleTest.registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.webide.autoinstallADBHelper");
Services.prefs.clearUserPref("devtools.webide.autoinstallFxdtAdapters");
Services.prefs.clearUserPref("devtools.webide.sidebars");
Services.prefs.clearUserPref("devtools.webide.busyTimeout");
});
function openWebIDE(autoInstallAddons) {

View File

@ -62,6 +62,30 @@
}
});
win.AppManager.runtimeList.usb.push({
connect: function(connection) {
let deferred = promise.defer();
return deferred.promise;
},
get name() {
return "infiniteRuntime";
}
});
win.AppManager.runtimeList.usb.push({
connect: function(connection) {
let deferred = promise.defer();
return deferred.promise;
},
prolongedConnection: true,
get name() {
return "prolongedRuntime";
}
});
win.AppManager.update("runtimelist");
let packagedAppLocation = getTestFilePath("app");
@ -71,7 +95,7 @@
let panelNode = win.document.querySelector("#runtime-panel");
let items = panelNode.querySelectorAll(".runtime-panel-item-usb");
is(items.length, 1, "Found one runtime button");
is(items.length, 3, "Found 3 runtime buttons");
let deferred = promise.defer();
win.AppManager.connection.once(
@ -104,7 +128,6 @@
ok(isPlayActive(), "play button is enabled 3");
ok(!isStopActive(), "stop button is disabled 3");
yield win.Cmds.disconnectRuntime();
is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
@ -137,6 +160,39 @@
yield win.Cmds.disconnectRuntime();
Services.prefs.setIntPref("devtools.webide.busyTimeout", 100);
// Wait for error message since connection never completes
let errorDeferred = promise.defer();
win.UI.reportError = errorName => {
if (errorName === "error_operationTimeout") {
errorDeferred.resolve();
}
};
// Click the infinite runtime
items[1].click();
ok(win.document.querySelector("window").className, "busy", "UI is busy");
yield errorDeferred.promise;
// Check for unexpected error message since this is prolonged
let noErrorDeferred = promise.defer();
win.UI.reportError = errorName => {
if (errorName === "error_operationTimeout") {
noErrorDeferred.reject();
}
};
// Click the prolonged runtime
items[2].click();
ok(win.document.querySelector("window").className, "busy", "UI is busy");
setTimeout(() => {
noErrorDeferred.resolve();
}, 1000);
yield noErrorDeferred.promise;
SimpleTest.finish();
});

View File

@ -13,7 +13,7 @@ pref("devtools.webide.enableLocalRuntime", false);
pref("devtools.webide.addonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/index.json");
pref("devtools.webide.simulatorAddonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/#VERSION#/#OS#/fxos_#SLASHED_VERSION#_simulator-#OS#-latest.xpi");
pref("devtools.webide.simulatorAddonID", "fxos_#SLASHED_VERSION#_simulator@mozilla.org");
pref("devtools.webide.simulatorAddonRegExp", "fxos_(.*)_simulator@mozilla\.org$");
pref("devtools.webide.simulatorAddonRegExp", "fxos_(.*)_simulator@mozilla\\.org$");
pref("devtools.webide.adbAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/adb-helper/#OS#/adbhelper-#OS#-latest.xpi");
pref("devtools.webide.adbAddonID", "adbhelper@mozilla.org");
pref("devtools.webide.adaptersAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxdt-adapters/#OS#/fxdt-adapters-#OS#-latest.xpi");

View File

@ -45,6 +45,16 @@
margin: 2px;
}
.dbg-breakpoint-condition-thrown-message {
display: none;
color: var(--theme-highlight-red);
}
.dbg-breakpoint.dbg-breakpoint-condition-thrown .dbg-breakpoint-condition-thrown-message {
display: block;
-moz-padding-start: 0;
}
/* Sources toolbar */
#sources-toolbar > .devtools-toolbarbutton,

View File

@ -151,6 +151,11 @@ treecol {
margin-right: 4px !important;
}
#defaultFontSizeLabel {
/* !important needed to override common !important rule */
-moz-margin-start: 4px !important;
}
/* Applications Pane Styles */
#applicationsContent {

View File

@ -17,6 +17,7 @@
#include "nsIRadioVisitor.h"
#include "nsIPhonetic.h"
#include "mozilla/Telemetry.h"
#include "nsIControllers.h"
#include "nsIStringBundle.h"
#include "nsFocusManager.h"
@ -4497,6 +4498,12 @@ HTMLInputElement::BindToTree(nsIDocument* aDocument, nsIContent* aParent,
// And now make sure our state is up to date
UpdateState(false);
#ifdef EARLY_BETA_OR_EARLIER
if (mType == NS_FORM_INPUT_PASSWORD) {
Telemetry::Accumulate(Telemetry::PWMGR_PASSWORD_INPUT_IN_FORM, !!mForm);
}
#endif
return rv;
}

View File

@ -2988,6 +2988,7 @@ public class BrowserApp extends GeckoApp
bookmark.setCheckable(true);
bookmark.setChecked(tab.isBookmark());
bookmark.setIcon(resolveBookmarkIconID(tab.isBookmark()));
bookmark.setTitle(resolveBookmarkTitleID(tab.isBookmark()));
reader.setEnabled(isAboutReader || !AboutPages.isAboutPage(tab.getURL()));
reader.setVisible(!inGuestMode);
@ -3116,6 +3117,10 @@ public class BrowserApp extends GeckoApp
}
}
private int resolveBookmarkTitleID(final boolean isBookmark) {
return (isBookmark ? R.string.bookmark_remove : R.string.bookmark);
}
private int resolveReadingListIconID(final boolean isInReadingList) {
return (isInReadingList ? R.drawable.ic_menu_reader_remove : R.drawable.ic_menu_reader_add);
}
@ -3146,10 +3151,12 @@ public class BrowserApp extends GeckoApp
Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.MENU, "bookmark");
tab.removeBookmark();
item.setIcon(resolveBookmarkIconID(false));
item.setTitle(resolveBookmarkTitleID(false));
} else {
Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, "bookmark");
tab.addBookmark();
item.setIcon(resolveBookmarkIconID(true));
item.setTitle(resolveBookmarkTitleID(true));
}
}
return true;

View File

@ -42,6 +42,7 @@
<!ENTITY url_bar_default_text2 "Search or enter address">
<!ENTITY bookmark "Bookmark">
<!ENTITY bookmark_remove "Remove bookmark">
<!ENTITY bookmark_added "Bookmark added">
<!ENTITY bookmark_removed "Bookmark removed">
<!ENTITY bookmark_updated "Bookmark updated">

View File

@ -78,6 +78,7 @@
<string name="quit">&quit;</string>
<string name="bookmark">&bookmark;</string>
<string name="bookmark_remove">&bookmark_remove;</string>
<string name="bookmark_added">&bookmark_added;</string>
<string name="bookmark_removed">&bookmark_removed;</string>
<string name="bookmark_updated">&bookmark_updated;</string>

View File

@ -33,10 +33,9 @@ public class ToolbarComponent extends BaseComponent {
private static final String URL_HTTP_PREFIX = "http://";
// We are waiting up to 60 seconds instead of the default waiting time
// because reader mode parsing can take quite some time on slower devices
// See Bug 1142699
private static final int READER_MODE_WAIT_MS = 60000;
// We are waiting up to 30 seconds instead of the default waiting time because reader mode
// parsing can take quite some time on slower devices (Bug 1142699)
private static final int READER_MODE_WAIT_MS = 30000;
public ToolbarComponent(final UITestContext testContext) {
super(testContext);

View File

@ -25,6 +25,14 @@ function getSelectionHandler() {
this._selectionHandler;
}
function getClipboard() {
return Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
}
function getTextValue(aElement) {
return aElement.value || aElement.textContent;
}
function todo(result, msg) {
return Messaging.sendRequestForResult({
type: TYPE_NAME,

View File

@ -9,8 +9,9 @@
const DIV_POINT_TEXT = "Under";
const INPUT_TEXT = "Text for select all in an <input>";
const TEXTAREA_TEXT = "Text for select all in a <textarea>";
const PASTE_TEXT = "Text for testing paste";
const READONLY_INPUT_TEXT = "readOnly text";
const TEXTAREA_TEXT = "Text for select all in a <textarea>";
/* =================================================================================
*
@ -25,6 +26,7 @@ function startTests() {
then(testReadonlyInput).
then(testCloseSelection).
then(testStartSelectionFail).
then(testPaste).
then(testAttachCaret).
then(testAttachCaretFail).
@ -351,6 +353,44 @@ function testAttachCaretFail() {
});
}
/* =================================================================================
*
* Tests to ensure we can paste text inside editable elements
*
*/
function testPaste() {
let sh = getSelectionHandler();
let clipboard = getClipboard();
clipboard.copyString(PASTE_TEXT, document);
// Add a contentEditable element to the document.
let div = document.createElement("div");
div.contentEditable = true;
div.dataset.editable = true;
document.body.appendChild(div);
let elements = document.querySelectorAll("div, input, textarea");
let promises = [];
for (var i = 0; i < elements.length; i++) {
sh.startSelection(elements[i]);
if (sh.isElementEditableText(elements[i]) && !elements[i].disabled) {
sh.actions.PASTE.action(elements[i]);
}
if (elements[i].dataset.editable) {
promises.push(is(getTextValue(elements[i]), PASTE_TEXT, "Pasted correctly"));
promises.push(ok(sh.isElementEditableText(elements[i]), "Element is editable"));
} else {
promises.push(isNot(getTextValue(elements[i]), PASTE_TEXT, "Paste failed correctly"));
}
}
document.body.removeChild(div);
div = null;
return Promise.all(promises);
}
</script>
</head>
@ -381,9 +421,9 @@ function testAttachCaretFail() {
nunc vel, fringilla turpis. Nulla lacinia, leo ut egestas hendrerit, risus
ligula interdum enim, vel varius libero sem ut ligula.</div><br>
<input id="inputNode" type="text"><br>
<input data-editable="true" id="inputNode" type="text"><br>
<textarea id="textareaNode"></textarea><br>
<textarea data-editable="true" id="textareaNode"></textarea><br>
<input id="readOnlyTextInput" type="text" readonly><br>

View File

@ -1,5 +1,6 @@
package org.mozilla.gecko.tests;
import org.mozilla.gecko.tests.helpers.GeckoHelper;
import org.mozilla.gecko.tests.helpers.NavigationHelper;
/**
@ -7,6 +8,8 @@ import org.mozilla.gecko.tests.helpers.NavigationHelper;
*/
public class testReaderModeTitle extends UITest {
public void testReaderModeTitle() {
GeckoHelper.blockForReady();
NavigationHelper.enterAndLoadUrl(StringHelper.ROBOCOP_READER_MODE_BASIC_ARTICLE);
mToolbar.pressReaderModeButton();

View File

@ -679,7 +679,8 @@ var SelectionHandler = {
order: 4,
selector: {
matches: function(aElement) {
return SelectionHandler.isElementEditableText(aElement) ?
// Disallow cut for contentEditable elements (until Bug 1112276 is fixed).
return !aElement.isContentEditable && SelectionHandler.isElementEditableText(aElement) ?
SelectionHandler.isSelectionActive() : false;
}
}
@ -711,10 +712,10 @@ var SelectionHandler = {
id: "paste_action",
icon: "drawable://ab_paste",
action: function(aElement) {
if (aElement && (aElement instanceof Ci.nsIDOMNSEditableElement)) {
let target = aElement.QueryInterface(Ci.nsIDOMNSEditableElement);
target.editor.paste(Ci.nsIClipboard.kGlobalClipboard);
target.focus();
if (aElement) {
let target = SelectionHandler._getEditor();
aElement.focus();
target.paste(Ci.nsIClipboard.kGlobalClipboard);
SelectionHandler._closeSelection();
UITelemetry.addEvent("action.1", "actionbar", null, "paste");
}
@ -895,7 +896,8 @@ var SelectionHandler = {
isElementEditableText: function (aElement) {
return (((aElement instanceof HTMLInputElement && aElement.mozIsTextField(false)) ||
(aElement instanceof HTMLTextAreaElement)) && !aElement.readOnly);
(aElement instanceof HTMLTextAreaElement)) && !aElement.readOnly) ||
aElement.isContentEditable;
},
_isNonTextInputElement: function(aElement) {
@ -963,7 +965,7 @@ var SelectionHandler = {
_moveCaret: function sh_moveCaret(aX, aY) {
// Get rect of text inside element
let range = document.createRange();
range.selectNodeContents(this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement).editor.rootElement);
range.selectNodeContents(this._getEditor().rootElement);
let textBounds = range.getBoundingClientRect();
// Get rect of editor

View File

@ -10,6 +10,10 @@ Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/SharedPromptUtils.jsm");
const LoginInfo =
Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
"nsILoginInfo", "init");
/* Constants for password prompt telemetry.
* Mirrored in mobile/android/components/LoginManagerPrompter.js */
const PROMPT_DISPLAYED = 0;
@ -792,11 +796,13 @@ LoginManagerPrompter.prototype = {
_showLoginCaptureDoorhanger(login, type) {
let { browser } = this._getNotifyWindow();
let msgNames = type == "password-save" ? {
let saveMsgNames = {
prompt: "rememberPasswordMsgNoUsername",
buttonLabel: "notifyBarRememberPasswordButtonText",
buttonAccessKey: "notifyBarRememberPasswordButtonAccessKey",
} : {
};
let changeMsgNames = {
// We reuse the existing message, even if it expects a username, until we
// switch to the final terminology in bug 1144856.
prompt: "updatePasswordMsg",
@ -804,28 +810,94 @@ LoginManagerPrompter.prototype = {
buttonAccessKey: "notifyBarUpdateButtonAccessKey",
};
let initialMsgNames = type == "password-save" ? saveMsgNames
: changeMsgNames;
let histogramName = type == "password-save" ? "PWMGR_PROMPT_REMEMBER_ACTION"
: "PWMGR_PROMPT_UPDATE_ACTION";
let histogram = Services.telemetry.getHistogramById(histogramName);
histogram.add(PROMPT_DISPLAYED);
let chromeDoc = browser.ownerDocument;
let currentNotification;
let updateButtonLabel = () => {
let foundLogins = Services.logins.findLogins({}, login.hostname,
login.formSubmitURL,
login.httpRealm);
let logins = foundLogins.filter(l => l.username == login.username);
let msgNames = (logins.length == 0) ? saveMsgNames : changeMsgNames;
// Update the label based on whether this will be a new login or not.
let label = this._getLocalizedString(msgNames.buttonLabel);
let accessKey = this._getLocalizedString(msgNames.buttonAccessKey);
// Update the labels for the next time the panel is opened.
currentNotification.mainAction.label = label;
currentNotification.mainAction.accessKey = accessKey;
// Update the labels in real time if the notification is displayed.
let element = [...currentNotification.owner.panel.childNodes]
.find(n => n.notification == currentNotification);
if (element) {
element.setAttribute("buttonlabel", label);
element.setAttribute("buttonaccesskey", accessKey);
}
};
let writeDataToUI = () => {
chromeDoc.getElementById("password-notification-username")
.setAttribute("placeholder", usernamePlaceholder);
chromeDoc.getElementById("password-notification-username")
.setAttribute("value", login.username);
chromeDoc.getElementById("password-notification-password")
.setAttribute("value", login.password);
updateButtonLabel();
};
let readDataFromUI = () => {
login.username =
chromeDoc.getElementById("password-notification-username").value;
login.password =
chromeDoc.getElementById("password-notification-password").value;
};
let onUsernameInput = () => {
readDataFromUI();
updateButtonLabel();
};
let persistData = () => {
let foundLogins = Services.logins.findLogins({}, login.hostname,
login.formSubmitURL,
login.httpRealm);
let logins = foundLogins.filter(l => l.username == login.username);
if (logins.length == 0) {
// The original login we have been provided with might have its own
// metadata, but we don't want it propagated to the newly created one.
Services.logins.addLogin(new LoginInfo(login.hostname,
login.formSubmitURL,
login.httpRealm,
login.username,
login.password,
login.usernameField,
login.passwordField));
} else if (logins.length == 1) {
this._updateLogin(logins[0], login.password);
} else {
Cu.reportError("Unexpected match of multiple logins.");
}
};
// The main action is the "Remember" or "Update" button.
let mainAction = {
label: this._getLocalizedString(msgNames.buttonLabel),
accessKey: this._getLocalizedString(msgNames.buttonAccessKey),
label: this._getLocalizedString(initialMsgNames.buttonLabel),
accessKey: this._getLocalizedString(initialMsgNames.buttonAccessKey),
callback: () => {
histogram.add(PROMPT_ADD_OR_UPDATE);
let foundLogins = Services.logins.findLogins({}, login.hostname,
login.formSubmitURL,
login.httpRealm);
let logins = foundLogins.filter(l => l.username == login.username);
if (logins.length == 0) {
Services.logins.addLogin(login);
} else if (logins.length == 1) {
this._updateLogin(logins[0], login.password);
} else {
Cu.reportError("Unexpected match of multiple logins.");
}
readDataFromUI();
persistData();
browser.focus();
}
};
@ -847,7 +919,7 @@ LoginManagerPrompter.prototype = {
this._getPopupNote().show(
browser,
"password",
this._getLocalizedString(msgNames.prompt, [displayHost]),
this._getLocalizedString(initialMsgNames.prompt, [displayHost]),
"password-notification-icon",
mainAction,
secondaryActions,
@ -856,18 +928,23 @@ LoginManagerPrompter.prototype = {
persistWhileVisible: true,
passwordNotificationType: type,
eventCallback: function (topic) {
if (topic != "showing") {
return false;
switch (topic) {
case "showing":
currentNotification = this;
writeDataToUI();
chromeDoc.getElementById("password-notification-username")
.addEventListener("input", onUsernameInput);
break;
case "dismissed":
readDataFromUI();
// Fall through.
case "removed":
currentNotification = null;
chromeDoc.getElementById("password-notification-username")
.removeEventListener("input", onUsernameInput);
break;
}
let chromeDoc = this.browser.ownerDocument;
chromeDoc.getElementById("password-notification-username")
.setAttribute("placeholder", usernamePlaceholder);
chromeDoc.getElementById("password-notification-username")
.setAttribute("value", login.username);
chromeDoc.getElementById("password-notification-password")
.setAttribute("value", login.password);
return false;
},
}
);

View File

@ -81,3 +81,119 @@ add_task(function* test_save_change() {
Services.logins.removeAllLogins();
}
});
/**
* Test changing the username inside the doorhanger notification for passwords.
*
* We have to test combination of existing and non-existing logins both for
* the original one from the webpage and the final one used by the dialog.
*
* We also check switching to and from empty usernames.
*/
add_task(function* test_edit_username() {
let testCases = [{
usernameInPage: "username",
usernameChangedTo: "newUsername",
}, {
usernameInPage: "username",
usernameInPageExists: true,
usernameChangedTo: "newUsername",
}, {
usernameInPage: "username",
usernameChangedTo: "newUsername",
usernameChangedToExists: true,
}, {
usernameInPage: "username",
usernameInPageExists: true,
usernameChangedTo: "newUsername",
usernameChangedToExists: true,
}, {
usernameInPage: "",
usernameChangedTo: "newUsername",
}, {
usernameInPage: "newUsername",
usernameChangedTo: "",
}, {
usernameInPage: "",
usernameChangedTo: "newUsername",
usernameChangedToExists: true,
}, {
usernameInPage: "newUsername",
usernameChangedTo: "",
usernameChangedToExists: true,
}];
for (let testCase of testCases) {
info("Test case: " + JSON.stringify(testCase));
// Create the pre-existing logins when needed.
if (testCase.usernameInPageExists) {
Services.logins.addLogin(LoginTestUtils.testData.formLogin({
hostname: "https://example.com",
formSubmitURL: "https://example.com",
username: testCase.usernameInPage,
password: "old password",
}));
}
if (testCase.usernameChangedToExists) {
Services.logins.addLogin(LoginTestUtils.testData.formLogin({
hostname: "https://example.com",
formSubmitURL: "https://example.com",
username: testCase.usernameChangedTo,
password: "old password",
}));
}
yield BrowserTestUtils.withNewTab({
gBrowser,
url: "https://example.com/browser/toolkit/components/" +
"passwordmgr/test/browser/form_basic.html",
}, function* (browser) {
// Submit the form in the content page with the credentials from the test
// case. This will cause the doorhanger notification to be displayed.
let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
"Shown");
yield ContentTask.spawn(browser, testCase.usernameInPage,
function* (usernameInPage) {
let doc = content.document;
doc.getElementById("form-basic-username").value = usernameInPage;
doc.getElementById("form-basic-password").value = "password";
doc.getElementById("form-basic").submit();
});
yield promiseShown;
// Modify the username in the dialog if requested.
if (testCase.usernameChangedTo) {
document.getElementById("password-notification-username")
.setAttribute("value", testCase.usernameChangedTo);
}
// We expect a modifyLogin notification if the final username used by the
// dialog exists in the logins database, otherwise an addLogin one.
let expectModifyLogin = testCase.usernameChangedTo
? testCase.usernameChangedToExists
: testCase.usernameInPageExists;
// Simulate the action on the notification to request the login to be
// saved, and wait for the data to be updated or saved based on the type
// of operation we expect.
let expectedNotification = expectModifyLogin ? "modifyLogin" : "addLogin";
let promiseLogin = TestUtils.topicObserved("passwordmgr-storage-changed",
(_, data) => data == expectedNotification);
let notificationElement = PopupNotifications.panel.childNodes[0];
notificationElement.button.doCommand();
let [result] = yield promiseLogin;
// Check that the values in the database match the expected values.
let login = expectModifyLogin ? result.QueryInterface(Ci.nsIArray)
.queryElementAt(1, Ci.nsILoginInfo)
: result.QueryInterface(Ci.nsILoginInfo);
Assert.equal(login.username, testCase.usernameChangedTo ||
testCase.usernameInPage);
Assert.equal(login.password, "password");
});
// Clean up the database before the next test case is executed.
Services.logins.removeAllLogins();
}
});

View File

@ -740,7 +740,12 @@ Database::InitSchema(bool* aDatabaseMigrated)
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 38 uses schema version 27.
if (currentSchemaVersion < 28) {
rv = MigrateV28Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 39 uses schema version 28.
// Schema Upgrades must add migration code here.
@ -1536,8 +1541,8 @@ Database::MigrateV27Up() {
"JOIN moz_bookmarks b ON b.fk = h.id "
"JOIN moz_keywords k ON k.id = b.keyword_id "
"LEFT JOIN moz_items_annos a ON a.item_id = b.id "
"LEFT JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id "
"AND n.name = 'bookmarkProperties/POSTData'"
"AND a.anno_attribute_id = (SELECT id FROM moz_anno_attributes "
"WHERE name = 'bookmarkProperties/POSTData') "
"WHERE k.place_id ISNULL "
"GROUP BY keyword"));
NS_ENSURE_SUCCESS(rv, rv);
@ -1563,6 +1568,38 @@ Database::MigrateV27Up() {
return NS_OK;
}
nsresult
Database::MigrateV28Up() {
MOZ_ASSERT(NS_IsMainThread());
// v27 migration was bogus and set some unrelated annotations as post_data for
// keywords having an annotated bookmark.
// The current v27 migration function is fixed, but we still need to handle
// users that hit the bogus version. Since we can't distinguish, we'll just
// set again all of the post data.
DebugOnly<nsresult> rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
"UPDATE moz_keywords "
"SET post_data = ( "
"SELECT content FROM moz_items_annos a "
"JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id "
"JOIN moz_bookmarks b on b.id = a.item_id "
"WHERE n.name = 'bookmarkProperties/POSTData' "
"AND b.keyword_id = moz_keywords.id "
"ORDER BY b.lastModified DESC "
"LIMIT 1 "
") "
"WHERE EXISTS(SELECT 1 FROM moz_bookmarks WHERE keyword_id = moz_keywords.id) "
));
// In case the update fails a constraint, we don't want to throw away the
// whole database for just a few keywords. In rare cases the user might have
// to recreate them. Though, at this point, there shouldn't be 2 keywords
// pointing to the same url and post data, cause the previous migration step
// removed them.
MOZ_ASSERT(NS_SUCCEEDED(rv));
return NS_OK;
}
void
Database::Shutdown()
{

View File

@ -16,7 +16,7 @@
// This is the schema version. Update it at any schema change and add a
// corresponding migrateVxx method below.
#define DATABASE_SCHEMA_VERSION 27
#define DATABASE_SCHEMA_VERSION 28
// Fired after Places inited.
#define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
@ -275,6 +275,7 @@ protected:
nsresult MigrateV25Up();
nsresult MigrateV26Up();
nsresult MigrateV27Up();
nsresult MigrateV28Up();
nsresult UpdateBookmarkRootTitles();

View File

@ -3,7 +3,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const CURRENT_SCHEMA_VERSION = 27;
const CURRENT_SCHEMA_VERSION = 28;
const FIRST_UPGRADABLE_SCHEMA_VERSION = 11;
const NS_APP_USER_PROFILE_50_DIR = "ProfD";

View File

@ -10,12 +10,15 @@ add_task(function* setup() {
yield db.execute(`INSERT INTO moz_places (url, guid)
VALUES ("http://test1.com/", "test1_______")
, ("http://test2.com/", "test2_______")
, ("http://test3.com/", "test3_______")
`);
// Add keywords.
yield db.execute(`INSERT INTO moz_keywords (keyword)
VALUES ("kw1")
, ("kw2")
, ("kw3")
, ("kw4")
, ("kw5")
`);
// Add bookmarks.
let now = Date.now() * 1000;
@ -38,15 +41,28 @@ add_task(function* setup() {
/* different uri, same keyword as 1 */
, (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now},
(SELECT id FROM moz_keywords WHERE keyword = 'kw3'), "bookmark6___")
, (1, (SELECT id FROM moz_places WHERE guid = 'test3_______'), 3, ${index++}, ${now}, ${now},
(SELECT id FROM moz_keywords WHERE keyword = 'kw4'), "bookmark7___")
/* same uri and post_data as bookmark7, different keyword */
, (1, (SELECT id FROM moz_places WHERE guid = 'test3_______'), 3, ${index++}, ${now}, ${now},
(SELECT id FROM moz_keywords WHERE keyword = 'kw5'), "bookmark8___")
`);
// Add postData.
yield db.execute(`INSERT INTO moz_anno_attributes (name)
VALUES ("bookmarkProperties/POSTData")`);
VALUES ("bookmarkProperties/POSTData")
, ("someOtherAnno")`);
yield db.execute(`INSERT INTO moz_items_annos(anno_attribute_id, item_id, content)
VALUES ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"),
(SELECT id FROM moz_bookmarks WHERE guid = "bookmark3___"), "postData1")
, ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"),
(SELECT id FROM moz_bookmarks WHERE guid = "bookmark5___"), "postData2")`);
(SELECT id FROM moz_bookmarks WHERE guid = "bookmark5___"), "postData2")
, ((SELECT id FROM moz_anno_attributes where name = "someOtherAnno"),
(SELECT id FROM moz_bookmarks WHERE guid = "bookmark5___"), "zzzzzzzzzz")
, ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"),
(SELECT id FROM moz_bookmarks WHERE guid = "bookmark7___"), "postData3")
, ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"),
(SELECT id FROM moz_bookmarks WHERE guid = "bookmark8___"), "postData3")
`);
yield db.close();
});
@ -69,7 +85,15 @@ add_task(function* test_keywords() {
Assert.equal(postData2, "postData2");
let [ url3, postData3 ] = PlacesUtils.getURLAndPostDataForKeyword("kw3");
Assert.equal(url3, "http://test1.com/");
Assert.equal(postData3, null);
let [ url4, postData4 ] = PlacesUtils.getURLAndPostDataForKeyword("kw4");
Assert.equal(url4, null);
Assert.equal(postData4, null);
let [ url5, postData5 ] = PlacesUtils.getURLAndPostDataForKeyword("kw5");
Assert.equal(url5, "http://test3.com/");
Assert.equal(postData5, "postData3");
Assert.equal((yield foreign_count("http://test1.com/")), 5); // 4 bookmark2 + 1 keywords
Assert.equal((yield foreign_count("http://test2.com/")), 4); // 2 bookmark2 + 2 keywords
Assert.equal((yield foreign_count("http://test3.com/")), 3); // 2 bookmark2 + 1 keywords
});

View File

@ -0,0 +1,77 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
add_task(function* setup() {
yield setupPlacesDatabase("places_v27.sqlite");
// Setup database contents to be migrated.
let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
let db = yield Sqlite.openConnection({ path });
// Add pages.
yield db.execute(`INSERT INTO moz_places (url, guid)
VALUES ("http://test1.com/", "test1_______")
, ("http://test2.com/", "test2_______")
`);
// Add keywords.
yield db.execute(`INSERT INTO moz_keywords (keyword, place_id, post_data)
VALUES ("kw1", (SELECT id FROM moz_places WHERE guid = "test2_______"), "broken data")
, ("kw2", (SELECT id FROM moz_places WHERE guid = "test2_______"), NULL)
, ("kw3", (SELECT id FROM moz_places WHERE guid = "test1_______"), "zzzzzzzzzz")
`);
// Add bookmarks.
let now = Date.now() * 1000;
let index = 0;
yield db.execute(`INSERT INTO moz_bookmarks (type, fk, parent, position, dateAdded, lastModified, keyword_id, guid)
VALUES (1, (SELECT id FROM moz_places WHERE guid = "test1_______"), 3, ${index++}, ${now}, ${now},
(SELECT id FROM moz_keywords WHERE keyword = "kw1"), "bookmark1___")
/* same uri, different keyword */
, (1, (SELECT id FROM moz_places WHERE guid = "test1_______"), 3, ${index++}, ${now}, ${now},
(SELECT id FROM moz_keywords WHERE keyword = "kw2"), "bookmark2___")
/* different uri, same keyword as 1 */
, (1, (SELECT id FROM moz_places WHERE guid = "test2_______"), 3, ${index++}, ${now}, ${now},
(SELECT id FROM moz_keywords WHERE keyword = "kw1"), "bookmark3___")
/* same uri, same keyword as 1 */
, (1, (SELECT id FROM moz_places WHERE guid = "test1_______"), 3, ${index++}, ${now}, ${now},
(SELECT id FROM moz_keywords WHERE keyword = "kw1"), "bookmark4___")
/* same uri, same keyword as 2 */
, (1, (SELECT id FROM moz_places WHERE guid = "test2_______"), 3, ${index++}, ${now}, ${now},
(SELECT id FROM moz_keywords WHERE keyword = "kw2"), "bookmark5___")
/* different uri, same keyword as 1 */
, (1, (SELECT id FROM moz_places WHERE guid = "test1_______"), 3, ${index++}, ${now}, ${now},
(SELECT id FROM moz_keywords WHERE keyword = "kw3"), "bookmark6___")
`);
// Add postData.
yield db.execute(`INSERT INTO moz_anno_attributes (name)
VALUES ("bookmarkProperties/POSTData")
, ("someOtherAnno")`);
yield db.execute(`INSERT INTO moz_items_annos(anno_attribute_id, item_id, content)
VALUES ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"),
(SELECT id FROM moz_bookmarks WHERE guid = "bookmark3___"), "postData1")
, ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"),
(SELECT id FROM moz_bookmarks WHERE guid = "bookmark5___"), "postData2")
, ((SELECT id FROM moz_anno_attributes where name = "someOtherAnno"),
(SELECT id FROM moz_bookmarks WHERE guid = "bookmark5___"), "zzzzzzzzzz")
`);
yield db.close();
});
add_task(function* database_is_valid() {
Assert.equal(PlacesUtils.history.databaseStatus,
PlacesUtils.history.DATABASE_STATUS_UPGRADED);
let db = yield PlacesUtils.promiseDBConnection();
Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
});
add_task(function* test_keywords() {
// When 2 urls have the same keyword, if one has postData it will be
// preferred.
let [ url1, postData1 ] = PlacesUtils.getURLAndPostDataForKeyword("kw1");
Assert.equal(url1, "http://test2.com/");
Assert.equal(postData1, "postData1");
let [ url2, postData2 ] = PlacesUtils.getURLAndPostDataForKeyword("kw2");
Assert.equal(url2, "http://test2.com/");
Assert.equal(postData2, "postData2");
let [ url3, postData3 ] = PlacesUtils.getURLAndPostDataForKeyword("kw3");
Assert.equal(url3, "http://test1.com/");
Assert.equal(postData3, null);
});

View File

@ -16,6 +16,7 @@ support-files =
places_v25.sqlite
places_v26.sqlite
places_v27.sqlite
places_v28.sqlite
[test_current_from_downgraded.js]
[test_current_from_v6.js]
@ -24,3 +25,4 @@ support-files =
[test_current_from_v24.js]
[test_current_from_v25.js]
[test_current_from_v26.js]
[test_current_from_v27.js]

View File

@ -7626,6 +7626,11 @@
"n_buckets" : 40,
"description": "Time in days each saved login was last used"
},
"PWMGR_PASSWORD_INPUT_IN_FORM": {
"expires_in_version": "never",
"kind": "boolean",
"description": "Whether an <input type=password> is associated with a <form> when it is added to a document"
},
"PWMGR_PROMPT_REMEMBER_ACTION" : {
"expires_in_version": "never",
"kind": "enumerated",

View File

@ -123,6 +123,28 @@ this.TelemetryFile = {
return Promise.all(p);
},
/**
* Add a ping to the saved pings directory so that it gets along with other pings. Note
* that the original ping file will not be modified.
*
* @param {String} aFilePath The path to the ping file that needs to be added to the
* saved pings directory.
* @return {Promise} A promise resolved when the ping is saved to the pings directory.
*/
addPendingPing: function(aPingPath) {
// Pings in the saved ping directory need to have the ping id or slug (old format) as
// the file name. We load the ping content, check that it is valid, and use it to save
// the ping file with the correct file name.
return loadPingFile(aPingPath).then(ping => {
// Append the ping to the pending list.
pendingPings.push(ping);
// Since we read a ping successfully, update the related histogram.
Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS").add(1);
// Save the ping to the saved pings directory.
return this.savePing(ping, false);
});
},
/**
* Remove the file for a ping
*
@ -277,29 +299,37 @@ function getPingDirectory() {
});
}
/**
* Loads a ping file.
* @param {String} aFilePath The path of the ping file.
* @return {Promise<Object>} A promise resolved with the ping content or rejected if the
* ping contains invalid data.
*/
let loadPingFile = Task.async(function* (aFilePath) {
let array = yield OS.File.read(aFilePath);
let decoder = new TextDecoder();
let string = decoder.decode(array);
let ping = JSON.parse(string);
// The ping's payload used to be stringified JSON. Deal with that.
if (typeof(ping.payload) == "string") {
ping.payload = JSON.parse(ping.payload);
}
return ping;
});
function addToPendingPings(file) {
function onLoad(success) {
let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS");
success_histogram.add(success);
}
return Task.spawn(function*() {
try {
let array = yield OS.File.read(file);
let decoder = new TextDecoder();
let string = decoder.decode(array);
let ping = JSON.parse(string);
// The ping's payload used to be stringified JSON. Deal with that.
if (typeof(ping.payload) == "string") {
ping.payload = JSON.parse(ping.payload);
}
return loadPingFile(file).then(ping => {
pendingPings.push(ping);
onLoad(true);
} catch (e) {
},
() => {
onLoad(false);
yield OS.File.remove(file);
}
});
return OS.File.remove(file);
});
}

View File

@ -14,6 +14,7 @@ Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/debug.js", this);
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/osfile.jsm", this);
Cu.import("resource://gre/modules/Promise.jsm", this);
Cu.import("resource://gre/modules/DeferredTask.jsm", this);
Cu.import("resource://gre/modules/Preferences.jsm");
@ -145,6 +146,19 @@ this.TelemetryPing = Object.freeze({
return Impl.setServer(aServer);
},
/**
* Adds a ping to the pending ping list by moving it to the saved pings directory
* and adding it to the pending ping list.
*
* @param {String} aPingPath The path of the ping to add to the pending ping list.
* @param {Boolean} [aRemoveOriginal] If true, deletes the ping at aPingPath after adding
* it to the saved pings directory.
* @return {Promise} Resolved when the ping is correctly moved to the saved pings directory.
*/
addPendingPing: function(aPingPath, aRemoveOriginal) {
return Impl.addPendingPing(aPingPath, aRemoveOriginal);
},
/**
* Send payloads to the server.
*
@ -205,8 +219,11 @@ this.TelemetryPing = Object.freeze({
* environment data.
* @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name,
* if found.
* @param {String} [aOptions.filePath] The path to save the ping to. Will save to default
* ping location if not provided.
*
* @returns {Promise} A promise that resolves when the ping is saved to disk.
* @returns {Promise<Integer>} A promise that resolves with the ping id when the ping is
* saved to disk.
*/
savePing: function(aType, aPayload, aOptions = {}) {
let options = aOptions;
@ -218,36 +235,6 @@ this.TelemetryPing = Object.freeze({
return Impl.savePing(aType, aPayload, options);
},
/**
* Only used for testing. Saves a ping to disk and return the ping id once done.
*
* @param {String} aType The type of the ping.
* @param {Object} aPayload The actual data payload for the ping.
* @param {Object} [aOptions] Options object.
* @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
* if sending fails.
* @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
* id, false otherwise.
* @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
* environment data.
* @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name,
* if found.
* @param {String} [aOptions.filePath] The path to save the ping to. Will save to default
* ping location if not provided.
*
* @returns {Promise<Integer>} A promise that resolves with the ping id when the ping is
* saved to disk.
*/
testSavePingToFile: function(aType, aPayload, aOptions = {}) {
let options = aOptions;
options.retentionDays = aOptions.retentionDays || DEFAULT_RETENTION_DAYS;
options.addClientId = aOptions.addClientId || false;
options.addEnvironment = aOptions.addEnvironment || false;
options.overwrite = aOptions.overwrite || false;
return Impl.testSavePingToFile(aType, aPayload, options);
},
/**
* The client id send with the telemetry ping.
*
@ -377,6 +364,23 @@ let Impl = {
this._server = aServer;
},
/**
* Adds a ping to the pending ping list by moving it to the saved pings directory
* and adding it to the pending ping list.
*
* @param {String} aPingPath The path of the ping to add to the pending ping list.
* @param {Boolean} [aRemoveOriginal] If true, deletes the ping at aPingPath after adding
* it to the saved pings directory.
* @return {Promise} Resolved when the ping is correctly moved to the saved pings directory.
*/
addPendingPing: function(aPingPath, aRemoveOriginal) {
return TelemetryFile.addPendingPing(aPingPath).then(() => {
if (aRemoveOriginal) {
return OS.File.remove(aPingPath);
}
}, error => this._log.error("addPendingPing - Unable to add the pending ping", error));
},
/**
* Build a complete ping and send data to the server. Record success/send-time in
* histograms.
@ -458,50 +462,26 @@ let Impl = {
* @param {Boolean} aOptions.addEnvironment true if the ping should contain the
* environment data.
* @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found.
*
* @returns {Promise} A promise that resolves when the ping is saved to disk.
*/
savePing: function savePing(aType, aPayload, aOptions) {
this._log.trace("savePing - Type " + aType + ", Server " + this._server +
", aOptions " + JSON.stringify(aOptions));
return this.assemblePing(aType, aPayload, aOptions)
.then(pingData => TelemetryFile.savePing(pingData, aOptions.overwrite),
error => this._log.error("savePing - Rejection", error));
},
/**
* Save a ping to disk and return the ping id when done.
*
* @param {String} aType The type of the ping.
* @param {Object} aPayload The actual data payload for the ping.
* @param {Object} aOptions Options object.
* @param {Number} aOptions.retentionDays The number of days to keep the ping on disk
* if sending fails.
* @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
* false otherwise.
* @param {Boolean} aOptions.addEnvironment true if the ping should contain the
* environment data.
* @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found.
* @param {String} [aOptions.filePath] The path to save the ping to. Will save to default
* ping location if not provided.
*
* @returns {Promise} A promise that resolves with the ping id when the ping is saved to
* disk.
*/
testSavePingToFile: function testSavePingToFile(aType, aPayload, aOptions) {
this._log.trace("testSavePingToFile - Type " + aType + ", Server " + this._server +
savePing: function savePing(aType, aPayload, aOptions) {
this._log.trace("savePing - Type " + aType + ", Server " + this._server +
", aOptions " + JSON.stringify(aOptions));
return this.assemblePing(aType, aPayload, aOptions)
.then(pingData => {
if (aOptions.filePath) {
return TelemetryFile.savePingToFile(pingData, aOptions.filePath, aOptions.overwrite)
.then(() => { return pingData.id; });
} else {
return TelemetryFile.savePing(pingData, aOptions.overwrite)
.then(() => { return pingData.id; });
}
}, error => this._log.error("testSavePing - Rejection", error));
.then(pingData => {
if ("filePath" in aOptions) {
return TelemetryFile.savePingToFile(pingData, aOptions.filePath, aOptions.overwrite)
.then(() => { return pingData.id; });
} else {
return TelemetryFile.savePing(pingData, aOptions.overwrite)
.then(() => { return pingData.id; });
}
}, error => this._log.error("savePing - Rejection", error));
},
finishPingRequest: function finishPingRequest(success, startTime, ping, isPersisted) {

View File

@ -21,6 +21,8 @@ Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
const myScope = this;
const IS_CONTENT_PROCESS = (function() {
// We cannot use Services.appinfo here because in telemetry xpcshell tests,
// appinfo is initially unavailable, and becomes available only later on.
@ -34,6 +36,7 @@ const PING_TYPE_MAIN = "main";
const PING_TYPE_SAVED_SESSION = "saved-session";
const RETENTION_DAYS = 14;
const REASON_ABORTED_SESSION = "aborted-session";
const REASON_DAILY = "daily";
const REASON_SAVED_SESSION = "saved-session";
const REASON_IDLE_DAILY = "idle-daily";
@ -67,6 +70,9 @@ const PREF_ASYNC_PLUGIN_INIT = "dom.ipc.plugins.asyncInit";
const MESSAGE_TELEMETRY_PAYLOAD = "Telemetry:Payload";
const MESSAGE_TELEMETRY_GET_CHILD_PAYLOAD = "Telemetry:GetChildPayload";
const DATAREPORTING_DIRECTORY = "datareporting";
const ABORTED_SESSION_FILE_NAME = "aborted-session-ping";
const SESSION_STATE_FILE_NAME = "session-state.json";
// Maximum number of content payloads that we are willing to store.
@ -78,12 +84,27 @@ const TELEMETRY_INTERVAL = 60000;
const TELEMETRY_DELAY = 60000;
// Delay before initializing telemetry if we're testing (ms)
const TELEMETRY_TEST_DELAY = 100;
// Execute a scheduler tick every 5 minutes.
const SCHEDULER_TICK_INTERVAL_MS = 5 * 60 * 1000;
// The maximum number of times a scheduled operation can fail.
const SCHEDULER_RETRY_ATTEMPTS = 3;
// The tolerance we have when checking if it's midnight (15 minutes).
const SCHEDULER_MIDNIGHT_TOLERANCE_MS = 15 * 60 * 1000;
// Coalesce the daily and aborted-session pings if they are both due within
// two minutes from each other.
const SCHEDULER_COALESCE_THRESHOLD_MS = 2 * 60 * 1000;
// Seconds of idle time before pinging.
// On idle-daily a gather-telemetry notification is fired, during it probes can
// start asynchronous tasks to gather data. On the next idle the data is sent.
const IDLE_TIMEOUT_SECONDS = 5 * 60;
// The frequency at which we persist session data to the disk to prevent data loss
// in case of aborted sessions (currently 5 minutes).
const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
var gLastMemoryPoll = null;
let gWasDebuggerAttached = false;
@ -147,8 +168,8 @@ let Policy = {
now: () => new Date(),
generateSessionUUID: () => generateUUID(),
generateSubsessionUUID: () => generateUUID(),
setDailyTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
clearDailyTimeout: (id) => clearTimeout(id),
setSchedulerTickTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
clearSchedulerTickTimeout: id => clearTimeout(id),
};
/**
@ -161,6 +182,38 @@ function truncateToDays(date) {
0, 0, 0, 0);
}
/**
* Check if the difference between the times is within the provided tolerance.
* @param {Number} t1 A time in milliseconds.
* @param {Number} t2 A time in milliseconds.
* @param {Number} tolerance The tolerance, in milliseconds.
* @return {Boolean} True if the absolute time difference is within the tolerance, false
* otherwise.
*/
function areTimesClose(t1, t2, tolerance) {
return Math.abs(t1 - t2) <= tolerance;
}
/**
* Get the midnight which is closer to the provided date.
* @param {Object} date The date object to check.
* @return {Object} The Date object representing the closes midnight, or null if midnight
* is not within the midnight tolerance.
*/
function getNearestMidnight(date) {
let lastMidnight = truncateToDays(date);
if (areTimesClose(date.getTime(), lastMidnight.getTime(), SCHEDULER_MIDNIGHT_TOLERANCE_MS)) {
return lastMidnight;
}
let nextMidnightDate = new Date(lastMidnight);
nextMidnightDate.setDate(nextMidnightDate.getDate() + 1);
if (areTimesClose(date.getTime(), nextMidnightDate.getTime(), SCHEDULER_MIDNIGHT_TOLERANCE_MS)) {
return nextMidnightDate;
}
return null;
}
/**
* Get the ping type based on the payload.
* @param {Object} aPayload The ping payload.
@ -258,11 +311,13 @@ let processInfo = {
* We are using this to synchronize saving to the file that TelemetrySession persists
* its state in.
*/
let gStateSaveSerializer = {
_queuedOperations: [],
_queuedInProgress: false,
_log: Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX),
function SaveSerializer() {
this._queuedOperations = [];
this._queuedInProgress = false;
this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
}
SaveSerializer.prototype = {
/**
* Enqueues an operation to a list to serialise their execution in order to prevent race
* conditions. Useful to serialise access to disk.
@ -340,6 +395,265 @@ let gStateSaveSerializer = {
},
};
/**
* TelemetryScheduler contains a single timer driving all regularly-scheduled
* Telemetry related jobs. Having a single place with this logic simplifies
* reasoning about scheduling actions in a single place, making it easier to
* coordinate jobs and coalesce them.
*/
let TelemetryScheduler = {
_lastDailyPingTime: 0,
_lastSessionCheckpointTime: 0,
// For sanity checking.
_lastAdhocPingTime: 0,
_lastTickTime: 0,
_log: null,
// The number of times a daily ping fails.
_dailyPingRetryAttempts: 0,
// The timer which drives the scheduler.
_schedulerTimer: null,
_shuttingDown: true,
/**
* Initialises the scheduler and schedules the first daily/aborted session pings.
*/
init: function() {
this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, "TelemetryScheduler::");
this._log.trace("init");
this._shuttingDown = false;
// Initialize the last daily ping and aborted session last due times to the current time.
// Otherwise, we might end up sending daily pings even if the subsession is not long enough.
let now = Policy.now();
this._lastDailyPingTime = now.getTime();
this._lastSessionCheckpointTime = now.getTime();
this._rescheduleTimeout();
},
/**
* Reschedules the tick timer.
*/
_rescheduleTimeout: function() {
this._log.trace("_rescheduleTimeout");
if (this._shuttingDown) {
this._log.warn("_rescheduleTimeout - already shutdown");
return;
}
if (this._schedulerTimer) {
Policy.clearSchedulerTickTimeout(this._schedulerTimer);
}
this._schedulerTimer =
Policy.setSchedulerTickTimeout(() => this._onSchedulerTick(), SCHEDULER_TICK_INTERVAL_MS);
},
/**
* Checks if we can send a daily ping or not.
* @param {Object} nowDate A date object.
* @return {Boolean} True if we can send the daily ping, false otherwise.
*/
_isDailyPingDue: function(nowDate) {
let nearestMidnight = getNearestMidnight(nowDate);
if (nearestMidnight) {
let subsessionLength = Math.abs(nowDate.getTime() - this._lastDailyPingTime);
if (subsessionLength < MIN_SUBSESSION_LENGTH_MS) {
// Generating a daily ping now would create a very short subsession.
return false;
} else if (areTimesClose(this._lastDailyPingTime, nearestMidnight.getTime(),
SCHEDULER_MIDNIGHT_TOLERANCE_MS)) {
// We've already sent a ping for this midnight.
return false;
}
return true;
}
let lastDailyPingDate = truncateToDays(new Date(this._lastDailyPingTime));
// This is today's date and also the previous midnight (0:00).
let todayDate = truncateToDays(nowDate);
// Check that _lastDailyPingTime isn't today nor within SCHEDULER_MIDNIGHT_TOLERANCE_MS of the
// *previous* midnight.
if ((lastDailyPingDate.getTime() != todayDate.getTime()) &&
!areTimesClose(this._lastDailyPingTime, todayDate.getTime(), SCHEDULER_MIDNIGHT_TOLERANCE_MS)) {
// Computer must have gone to sleep, the daily ping is overdue.
return true;
}
return false;
},
/**
* An helper function to save an aborted-session ping.
* @param {Number} now The current time, in milliseconds.
* @param {Object} [competingPayload=null] If we are coalescing the daily and the
* aborted-session pings, this is the payload for the former. Note
* that the reason field of this payload will be changed.
* @return {Promise} A promise resolved when the ping is saved.
*/
_saveAbortedPing: function(now, competingPayload=null) {
this._lastSessionCheckpointTime = now;
return Impl._saveAbortedSessionPing(competingPayload)
.catch(e => this._log.error("_saveAbortedPing - Failed", e));
},
/**
* Performs a scheduler tick. This function manages Telemetry recurring operations.
* @return {Promise} A promise, only used when testing, resolved when the scheduled
* operation completes.
*/
_onSchedulerTick: function() {
if (this._shuttingDown) {
this._log.warn("_onSchedulerTick - already shutdown.");
return;
}
let promise = Promise.resolve();
try {
promise = this._schedulerTickLogic();
} catch (e) {
this._log.error("_onSchedulerTick - There was an exception", e);
} finally {
this._rescheduleTimeout();
}
// This promise is returned to make testing easier.
return promise;
},
/**
* Implements the scheduler logic.
* @return {Promise} Resolved when the scheduled task completes. Only used in tests.
*/
_schedulerTickLogic: function() {
this._log.trace("_schedulerTickLogic");
let nowDate = Policy.now();
let now = nowDate.getTime();
if (now - this._lastTickTime > 1.1 * SCHEDULER_TICK_INTERVAL_MS) {
this._log.trace("_schedulerTickLogic - First scheduler tick after sleep or startup.");
}
this._lastTickTime = now;
// Check if aborted-session ping is due.
let isAbortedPingDue =
(now - this._lastSessionCheckpointTime) >= ABORTED_SESSION_UPDATE_INTERVAL_MS;
// Check if daily ping is due.
let shouldSendDaily = this._isDailyPingDue(nowDate);
// We can combine the daily-ping and the aborted-session ping in the following cases:
// - If both the daily and the aborted session pings are due (a laptop that wakes
// up after a few hours).
// - If either the daily ping is due and the other one would follow up shortly
// (whithin the coalescence threshold).
let nextSessionCheckpoint =
this._lastSessionCheckpointTime + ABORTED_SESSION_UPDATE_INTERVAL_MS;
let combineActions = (shouldSendDaily && isAbortedPingDue) || (shouldSendDaily &&
areTimesClose(now, nextSessionCheckpoint, SCHEDULER_COALESCE_THRESHOLD_MS));
if (combineActions) {
this._log.trace("_schedulerTickLogic - Combining pings.");
// Send the daily ping and also save its payload as an aborted-session ping.
return Impl._sendDailyPing(true).then(() => this._dailyPingSucceeded(now),
() => this._dailyPingFailed(now));
} else if (shouldSendDaily) {
this._log.trace("_schedulerTickLogic - Daily ping due.");
return Impl._sendDailyPing().then(() => this._dailyPingSucceeded(now),
() => this._dailyPingFailed(now));
} else if (isAbortedPingDue) {
this._log.trace("_schedulerTickLogic - Aborted session ping due.");
return this._saveAbortedPing(now);
}
// No ping is due.
this._log.trace("_schedulerTickLogic - No ping due.");
// It's possible, because of sleeps, that we're no longer within midnight tolerance for
// daily pings. Because of that, daily retry attempts would not be 0 on the next midnight.
// Reset that count on do-nothing ticks.
this._dailyPingRetryAttempts = 0;
return Promise.resolve();
},
/**
* Update the scheduled pings if some other ping was sent.
* @param {String} reason The reason of the ping that was sent.
* @param {Object} [competingPayload=null] The payload of the ping that was sent. The
* reason of this payload will be changed.
*/
reschedulePings: function(reason, competingPayload = null) {
if (this._shuttingDown) {
this._log.error("reschedulePings - already shutdown");
return;
}
this._log.trace("reschedulePings - reason: " + reason);
let now = Policy.now();
this._lastAdhocPingTime = now.getTime();
if (reason == REASON_ENVIRONMENT_CHANGE) {
// We just generated an environment-changed ping, save it as an aborted session and
// update the schedules.
this._saveAbortedPing(now.getTime(), competingPayload);
// If we're close to midnight, skip today's daily ping and reschedule it for tomorrow.
let nearestMidnight = getNearestMidnight(now);
if (nearestMidnight) {
this._lastDailyPingTime = now.getTime();
}
}
this._rescheduleTimeout();
},
/**
* Called when a scheduled operation successfully completes (ping sent or saved).
* @param {Number} now The current time, in milliseconds.
*/
_dailyPingSucceeded: function(now) {
this._log.trace("_dailyPingSucceeded");
this._lastDailyPingTime = now;
this._dailyPingRetryAttempts = 0;
},
/**
* Called when a scheduled operation fails (ping sent or saved).
* @param {Number} now The current time, in milliseconds.
*/
_dailyPingFailed: function(now) {
this._log.error("_dailyPingFailed");
this._dailyPingRetryAttempts++;
// If we reach the maximum number of retry attempts for a daily ping, log the error
// and skip this daily ping.
if (this._dailyPingRetryAttempts >= SCHEDULER_RETRY_ATTEMPTS) {
this._log.error("_pingFailed - The daily ping failed too many times. Skipping it.");
this._dailyPingRetryAttempts = 0;
this._lastDailyPingTime = now;
}
},
/**
* Stops the scheduler.
*/
shutdown: function() {
if (this._shuttingDown) {
if (this._log) {
this._log.error("shutdown - Already shut down");
} else {
Cu.reportError("TelemetryScheduler.shutdown - Already shut down");
}
return;
}
this._log.trace("shutdown");
if (this._schedulerTimer) {
Policy.clearSchedulerTickTimeout(this._schedulerTimer);
this._schedulerTimer = null;
}
this._shuttingDown = true;
}
};
this.EXPORTED_SYMBOLS = ["TelemetrySession"];
this.TelemetrySession = Object.freeze({
@ -490,12 +804,14 @@ let Impl = {
_profileSubsessionCounter: 0,
// Date of the last session split
_subsessionStartDate: null,
// The timer used for daily collections.
_dailyTimerId: null,
// A task performing delayed initialization of the chrome process
_delayedInitTask: null,
// The deferred promise resolved when the initialization task completes.
_delayedInitTaskDeferred: null,
// Used to serialize session state writes to disk.
_stateSaveSerializer: new SaveSerializer(),
// Used to serialize aborted session ping writes to disk.
_abortedSessionSerializer: new SaveSerializer(),
/**
* Gets a series of simple measurements (counters). At the moment, this
@ -1014,8 +1330,7 @@ let Impl = {
this.startNewSubsession();
// Persist session data to disk (don't wait until it completes).
let sessionData = this._getSessionDataObject();
gStateSaveSerializer.enqueueTask(() => this._saveSessionData(sessionData));
this._rescheduleDailyTimer();
this._stateSaveSerializer.enqueueTask(() => this._saveSessionData(sessionData));
}
return payload;
@ -1168,9 +1483,20 @@ let Impl = {
Telemetry.asyncFetchTelemetryData(function () {});
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
this._rescheduleDailyTimer();
// Check for a previously written aborted session ping.
yield this._checkAbortedSessionPing();
TelemetryEnvironment.registerChangeListener(ENVIRONMENT_CHANGE_LISTENER,
() => this._onEnvironmentChange());
// Write the first aborted-session ping as early as possible. Just do that
// if we are not testing, since calling Telemetry.reset() will make a previous
// aborted ping a pending ping.
if (!testing) {
yield this._saveAbortedSessionPing();
}
// Start the scheduler.
TelemetryScheduler.init();
#endif
this._delayedInitTaskDeferred.resolve();
@ -1321,7 +1647,7 @@ let Impl = {
overwrite: true,
filePath: file.path,
};
return TelemetryPing.testSavePingToFile(getPingType(payload), payload, options);
return TelemetryPing.savePing(getPingType(payload), payload, options);
},
/**
@ -1501,11 +1827,8 @@ let Impl = {
let cleanup = () => {
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
TelemetryEnvironment.unregisterChangeListener(ENVIRONMENT_CHANGE_LISTENER);
TelemetryScheduler.shutdown();
#endif
if (this._dailyTimerId) {
Policy.clearDailyTimeout(this._dailyTimerId);
this._dailyTimerId = null;
}
this.uninstall();
let reset = () => {
@ -1515,7 +1838,11 @@ let Impl = {
if (Telemetry.canSend || testing) {
return this.savePendingPings()
.then(() => gStateSaveSerializer.flushTasks())
.then(() => this._stateSaveSerializer.flushTasks())
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
.then(() => this._abortedSessionSerializer
.enqueueTask(() => this._removeAbortedSessionPing()))
#endif
.then(reset);
}
@ -1545,35 +1872,14 @@ let Impl = {
return this._delayedInitTask.finalize().then(cleanup);
},
_rescheduleDailyTimer: function() {
if (this._dailyTimerId) {
this._log.trace("_rescheduleDailyTimer - clearing existing timeout");
Policy.clearDailyTimeout(this._dailyTimerId);
}
let now = Policy.now();
let midnight = truncateToDays(now).getTime() + MS_IN_ONE_DAY;
let msUntilCollection = midnight - now.getTime();
if (msUntilCollection < MIN_SUBSESSION_LENGTH_MS) {
msUntilCollection += MS_IN_ONE_DAY;
}
this._log.trace("_rescheduleDailyTimer - now: " + now
+ ", scheduled: " + new Date(now.getTime() + msUntilCollection));
this._dailyTimerId = Policy.setDailyTimeout(() => this._onDailyTimer(), msUntilCollection);
},
_onDailyTimer: function() {
if (!this._initStarted) {
if (this._log) {
this._log.warn("_onDailyTimer - not initialized");
} else {
Cu.reportError("TelemetrySession._onDailyTimer - not initialized");
}
return;
}
this._log.trace("_onDailyTimer");
/**
* Gather and send a daily ping.
* @param {Boolean} [saveAsAborted=false] Also saves the payload as an aborted-session
* ping.
* @return {Promise} Resolved when the ping is sent.
*/
_sendDailyPing: function(saveAsAborted = false) {
this._log.trace("_sendDailyPing");
let payload = this.getSessionPayload(REASON_DAILY, true);
let options = {
@ -1581,10 +1887,14 @@ let Impl = {
addClientId: true,
addEnvironment: true,
};
let promise = TelemetryPing.send(getPingType(payload), payload, options);
this._rescheduleDailyTimer();
// Return the promise so tests can wait on the ping submission.
let promise = TelemetryPing.send(getPingType(payload), payload, options);
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
// If required, also save the payload as an aborted session.
if (saveAsAborted) {
return promise.then(() => this._saveAbortedSessionPing(payload));
}
#endif
return promise;
},
@ -1594,7 +1904,7 @@ let Impl = {
* loading has completed, with false otherwise.
*/
_loadSessionData: Task.async(function* () {
let dataFile = OS.Path.join(OS.Constants.Path.profileDir, "datareporting",
let dataFile = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
SESSION_STATE_FILE_NAME);
// Try to load the "profileSubsessionCounter" from the state file.
@ -1632,7 +1942,7 @@ let Impl = {
* Saves session data to disk.
*/
_saveSessionData: Task.async(function* (sessionData) {
let dataDir = OS.Path.join(OS.Constants.Path.profileDir, "datareporting");
let dataDir = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY);
yield OS.File.makeDir(dataDir);
let filePath = OS.Path.join(dataDir, SESSION_STATE_FILE_NAME);
@ -1647,12 +1957,15 @@ let Impl = {
this._log.trace("_onEnvironmentChange");
let payload = this.getSessionPayload(REASON_ENVIRONMENT_CHANGE, true);
let clonedPayload = Cu.cloneInto(payload, myScope);
TelemetryScheduler.reschedulePings(REASON_ENVIRONMENT_CHANGE, clonedPayload);
let options = {
retentionDays: RETENTION_DAYS,
addClientId: true,
addEnvironment: true,
};
let promise = TelemetryPing.send(getPingType(payload), payload, options);
TelemetryPing.send(getPingType(payload), payload, options);
},
_isClassicReason: function(reason) {
@ -1673,7 +1986,73 @@ let Impl = {
initialized: this._initialized,
initStarted: this._initStarted,
haveDelayedInitTask: !!this._delayedInitTask,
dailyTimerScheduled: !!this._dailyTimerId,
};
},
/**
* Deletes the aborted session ping. This is called during shutdown.
* @return {Promise} Resolved when the aborted session ping is removed or if it doesn't
* exist.
*/
_removeAbortedSessionPing: function() {
const FILE_PATH = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
ABORTED_SESSION_FILE_NAME);
try {
return OS.File.remove(FILE_PATH);
} catch (ex if ex.becauseNoSuchFile) { }
return Promise.resolve();
},
/**
* Check if there's any aborted session ping available. If so, tell TelemetryPing about
* it.
*/
_checkAbortedSessionPing: Task.async(function* () {
// Create the subdirectory that will contain te aborted session ping. We put it in a
// subdirectory so that it doesn't get picked up as a pending ping. Please note that
// this does nothing if the directory does not already exist.
const ABORTED_SESSIONS_DIR =
OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY);
yield OS.File.makeDir(ABORTED_SESSIONS_DIR, { ignoreExisting: true });
const FILE_PATH = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
ABORTED_SESSION_FILE_NAME);
let abortedExists = yield OS.File.exists(FILE_PATH);
if (abortedExists) {
this._log.trace("_checkAbortedSessionPing - aborted session found: " + FILE_PATH);
yield this._abortedSessionSerializer.enqueueTask(
() => TelemetryPing.addPendingPing(FILE_PATH, true));
}
}),
/**
* Saves the aborted session ping to disk.
* @param {Object} [aProvidedPayload=null] A payload object to be used as an aborted
* session ping. The reason of this payload is changed to aborted-session.
* If not provided, a new payload is gathered.
*/
_saveAbortedSessionPing: function(aProvidedPayload = null) {
const FILE_PATH = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
ABORTED_SESSION_FILE_NAME);
this._log.trace("_saveAbortedSessionPing - ping path: " + FILE_PATH);
let payload = null;
if (aProvidedPayload) {
payload = aProvidedPayload;
// Overwrite the original reason.
payload.info.reason = REASON_ABORTED_SESSION;
} else {
payload = this.getSessionPayload(REASON_ABORTED_SESSION, false);
}
let options = {
retentionDays: RETENTION_DAYS,
addClientId: true,
addEnvironment: true,
overwrite: true,
filePath: FILE_PATH,
};
return this._abortedSessionSerializer.enqueueTask(() =>
TelemetryPing.savePing(getPingType(payload), payload, options));
},
};

View File

@ -7,9 +7,10 @@ It includes the histograms and other performance and diagnostic data.
This ping is triggered by different scenarios, which is documented by the ``reason`` field:
* ``aborted-session`` - this ping is regularly saved to disk (every 5 minutes), overwriting itself, and deleted at shutdown. If a previous aborted session ping is found at startup, it gets sent to the server. The first aborted-session ping is generated as soon as Telemetry starts
* ``environment-change`` - the :doc:`environment` changed, so the session measurements got reset and a new subsession starts
* ``shutdown`` - triggered when the browser session ends
* ``daily`` - a session split triggered in 24h hour intervals at local midnight
* ``daily`` - a session split triggered in 24h hour intervals at local midnight. If an ``environment-change`` ping is generated by the time it should be sent, the daily ping is rescheduled for the next midnight
* ``saved-session`` - the *"classic"* Telemetry payload with measurements covering the whole browser session (only submitted for a transition period)
Most reasons lead to a session split, initiating a new *subsession*. We reset important measurements for those subsessions.

View File

@ -78,11 +78,11 @@ function createAppInfo(id, name, version, platformVersion) {
XULAPPINFO_CONTRACTID, XULAppInfoFactory);
}
// Fake setTimeout and clearTimeout for the daily timer in tests for controllable behavior.
function fakeDailyTimers(set, clear) {
// Fake the timeout functions for the TelemetryScheduler.
function fakeSchedulerTimer(set, clear) {
let session = Components.utils.import("resource://gre/modules/TelemetrySession.jsm");
session.Policy.setDailyTimeout = set;
session.Policy.clearDailyTimeout = clear;
session.Policy.setSchedulerTickTimeout = set;
session.Policy.clearSchedulerTickTimeout = clear;
}
// Set logging preferences for all the tests.
@ -91,4 +91,4 @@ Services.prefs.setBoolPref("toolkit.telemetry.log.dump", true);
TelemetryPing.initLogging();
// Avoid timers interrupting test behavior.
fakeDailyTimers(() => {}, () => {});
fakeSchedulerTimer(() => {}, () => {});

View File

@ -73,7 +73,7 @@ let createSavedPings = Task.async(function* (aPingInfos) {
let num = aPingInfos[type].num;
let age = now - aPingInfos[type].age;
for (let i = 0; i < num; ++i) {
let pingId = yield TelemetryPing.testSavePingToFile("test-ping", {}, { overwrite: true });
let pingId = yield TelemetryPing.savePing("test-ping", {}, { overwrite: true });
if (aPingInfos[type].age) {
// savePing writes to the file synchronously, so we're good to
// modify the lastModifedTime now.

View File

@ -31,6 +31,7 @@ const PING_FORMAT_VERSION = 4;
const PING_TYPE_MAIN = "main";
const PING_TYPE_SAVED_SESSION = "saved-session";
const REASON_ABORTED_SESSION = "aborted-session";
const REASON_SAVED_SESSION = "saved-session";
const REASON_SHUTDOWN = "shutdown";
const REASON_TEST_PING = "test-ping";
@ -65,6 +66,7 @@ const MS_IN_ONE_DAY = SEC_IN_ONE_DAY * 1000;
const PREF_BRANCH = "toolkit.telemetry.";
const PREF_ENABLED = PREF_BRANCH + "enabled";
const PREF_SERVER = PREF_BRANCH + "server";
const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
const PREF_FHR_SERVICE_ENABLED = "datareporting.healthreport.service.enabled";
@ -72,8 +74,16 @@ const HAS_DATAREPORTINGSERVICE = "@mozilla.org/datareporting/service;1" in Cc;
const SESSION_RECORDER_EXPECTED = HAS_DATAREPORTINGSERVICE &&
Preferences.get(PREF_FHR_SERVICE_ENABLED, true);
const DATAREPORTING_DIR = "datareporting";
const ABORTED_PING_FILE_NAME = "aborted-session-ping";
const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
XPCOMUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function() {
return OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR);
});
let gHttpServer = new HttpServer();
let gServerStarted = false;
let gRequestIterator = null;
@ -578,6 +588,7 @@ add_task(function* test_simplePing() {
gHttpServer.start(-1);
gServerStarted = true;
gRequestIterator = Iterator(new Request());
Preferences.set(PREF_SERVER, "http://localhost:" + gHttpServer.identity.primaryPort);
let now = new Date(2020, 1, 1, 12, 0, 0);
let expectedDate = new Date(2020, 1, 1, 0, 0, 0);
@ -849,29 +860,19 @@ add_task(function* test_dailyCollection() {
let now = new Date(2030, 1, 1, 12, 0, 0);
let nowDay = new Date(2030, 1, 1, 0, 0, 0);
let timerCallback = null;
let timerDelay = null;
let schedulerTickCallback = null;
gRequestIterator = Iterator(new Request());
fakeNow(now);
fakeDailyTimers((callback, timeout) => {
dump("fake setDailyTimeout(" + callback + ", " + timeout + ")\n");
timerCallback = callback;
timerDelay = timeout;
return 1;
}, () => {});
// Fake scheduler functions to control daily collection flow in tests.
fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
// Init and check timer.
yield TelemetrySession.setup();
TelemetryPing.setServer("http://localhost:" + gHttpServer.identity.primaryPort);
Assert.ok(!!timerCallback);
Assert.ok(Number.isFinite(timerDelay));
let timerDate = futureDate(now, timerDelay);
let expectedDate = futureDate(nowDay, MS_IN_ONE_DAY);
Assert.equal(timerDate.toISOString(), expectedDate.toISOString());
// Set histograms to expected state.
const COUNT_ID = "TELEMETRY_TEST_COUNT";
const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
@ -885,8 +886,16 @@ add_task(function* test_dailyCollection() {
keyed.add("b", 1);
keyed.add("b", 1);
// Trigger and collect daily ping.
yield timerCallback();
// Make sure the daily ping gets triggered.
let expectedDate = nowDay;
now = futureDate(nowDay, MS_IN_ONE_DAY);
fakeNow(now);
Assert.ok(!!schedulerTickCallback);
// Run a scheduler tick: it should trigger the daily ping.
yield schedulerTickCallback();
// Collect the daily ping.
let request = yield gRequestIterator.next();
Assert.ok(!!request);
let ping = decodeRequestPayload(request);
@ -894,14 +903,20 @@ add_task(function* test_dailyCollection() {
Assert.equal(ping.type, PING_TYPE_MAIN);
Assert.equal(ping.payload.info.reason, REASON_DAILY);
let subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
Assert.equal(subsessionStartDate.toISOString(), nowDay.toISOString());
Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["a"].sum, 1);
Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["b"].sum, 2);
// Trigger and collect another ping. The histograms should be reset.
yield timerCallback();
// The daily ping is rescheduled for "tomorrow".
expectedDate = futureDate(expectedDate, MS_IN_ONE_DAY);
now = futureDate(now, MS_IN_ONE_DAY);
fakeNow(now);
// Run a scheduler tick. Trigger and collect another ping. The histograms should be reset.
yield schedulerTickCallback();
request = yield gRequestIterator.next();
Assert.ok(!!request);
ping = decodeRequestPayload(request);
@ -909,7 +924,7 @@ add_task(function* test_dailyCollection() {
Assert.equal(ping.type, PING_TYPE_MAIN);
Assert.equal(ping.payload.info.reason, REASON_DAILY);
subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
Assert.equal(subsessionStartDate.toISOString(), nowDay.toISOString());
Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
Assert.equal(ping.payload.histograms[COUNT_ID].sum, 0);
Assert.deepEqual(ping.payload.keyedHistograms[KEYED_ID], {});
@ -919,7 +934,12 @@ add_task(function* test_dailyCollection() {
keyed.add("a", 1);
keyed.add("b", 1);
yield timerCallback();
// The daily ping is rescheduled for "tomorrow".
expectedDate = futureDate(expectedDate, MS_IN_ONE_DAY);
now = futureDate(now, MS_IN_ONE_DAY);
fakeNow(now);
yield schedulerTickCallback();
request = yield gRequestIterator.next();
Assert.ok(!!request);
ping = decodeRequestPayload(request);
@ -927,11 +947,115 @@ add_task(function* test_dailyCollection() {
Assert.equal(ping.type, PING_TYPE_MAIN);
Assert.equal(ping.payload.info.reason, REASON_DAILY);
subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
Assert.equal(subsessionStartDate.toISOString(), nowDay.toISOString());
Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["a"].sum, 1);
Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["b"].sum, 1);
// Shutdown to cleanup the aborted-session if it gets created.
yield TelemetrySession.shutdown();
});
add_task(function* test_dailyDuplication() {
if (gIsAndroid) {
// We don't do daily collections yet on Android.
return;
}
gRequestIterator = Iterator(new Request());
let schedulerTickCallback = null;
let now = new Date(2030, 1, 1, 0, 0, 0);
fakeNow(now);
// Fake scheduler functions to control daily collection flow in tests.
fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
yield TelemetrySession.setup();
// Make sure the daily ping gets triggered just before midnight.
let firstDailyDue = new Date(2030, 1, 1, 23, 45, 0);
fakeNow(firstDailyDue);
// Run a scheduler tick: it should trigger the daily ping.
Assert.ok(!!schedulerTickCallback);
yield schedulerTickCallback();
// Get the first daily ping.
let request = yield gRequestIterator.next();
Assert.ok(!!request);
let ping = decodeRequestPayload(request);
Assert.equal(ping.type, PING_TYPE_MAIN);
Assert.equal(ping.payload.info.reason, REASON_DAILY);
// We don't expect to receive any other daily ping in this test, so assert if we do.
registerPingHandler((req, res) => {
Assert.ok(false, "No more daily pings should be sent/received in this test.");
});
// Set the current time to a bit after midnight.
let secondDailyDue = new Date(firstDailyDue);
secondDailyDue.setDate(firstDailyDue.getDate() + 1);
secondDailyDue.setHours(0);
secondDailyDue.setMinutes(15);
fakeNow(secondDailyDue);
// Run a scheduler tick: it should NOT trigger the daily ping.
Assert.ok(!!schedulerTickCallback);
yield schedulerTickCallback();
// Shutdown to cleanup the aborted-session if it gets created.
yield TelemetrySession.shutdown();
});
add_task(function* test_dailyOverdue() {
if (gIsAndroid) {
// We don't do daily collections yet on Android.
return;
}
let schedulerTickCallback = null;
let now = new Date(2030, 1, 1, 11, 0, 0);
fakeNow(now);
// Fake scheduler functions to control daily collection flow in tests.
fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
yield TelemetrySession.setup();
// Skip one hour ahead: nothing should be due.
now.setHours(now.getHours() + 1);
fakeNow(now);
// Assert if we receive something!
registerPingHandler((req, res) => {
Assert.ok(false, "No daily ping should be received if not overdue!.");
});
// This tick should not trigger any daily ping.
Assert.ok(!!schedulerTickCallback);
yield schedulerTickCallback();
// Restore the non asserting ping handler. This is done by the Request() constructor.
gRequestIterator = Iterator(new Request());
// Simulate an overdue ping: we're not close to midnight, but the last daily ping
// time is too long ago.
let dailyOverdue = new Date(2030, 1, 2, 13, 00, 0);
fakeNow(dailyOverdue);
// Run a scheduler tick: it should trigger the daily ping.
Assert.ok(!!schedulerTickCallback);
yield schedulerTickCallback();
// Get the first daily ping.
let request = yield gRequestIterator.next();
Assert.ok(!!request);
let ping = decodeRequestPayload(request);
Assert.equal(ping.type, PING_TYPE_MAIN);
Assert.equal(ping.payload.info.reason, REASON_DAILY);
// Shutdown to cleanup the aborted-session if it gets created.
yield TelemetrySession.shutdown();
});
add_task(function* test_environmentChange() {
@ -948,7 +1072,6 @@ add_task(function* test_environmentChange() {
gRequestIterator = Iterator(new Request());
fakeNow(now);
fakeDailyTimers(() => {}, () => {});
const PREF_TEST = "toolkit.telemetry.test.pref1";
Preferences.reset(PREF_TEST);
@ -1046,11 +1169,10 @@ add_task(function* test_savedPingsOnShutdown() {
add_task(function* test_savedSessionData() {
// Create the directory which will contain the data file, if it doesn't already
// exist.
const dataDir = OS.Path.join(OS.Constants.Path.profileDir, "datareporting");
yield OS.File.makeDir(dataDir);
yield OS.File.makeDir(DATAREPORTING_PATH);
// Write test data to the session data file.
const dataFilePath = OS.Path.join(dataDir, "session-state.json");
const dataFilePath = OS.Path.join(DATAREPORTING_PATH, "session-state.json");
const sessionState = {
previousSubsessionId: null,
profileSubsessionCounter: 3785,
@ -1096,11 +1218,10 @@ add_task(function* test_savedSessionData() {
add_task(function* test_invalidSessionData() {
// Create the directory which will contain the data file, if it doesn't already
// exist.
const dataDir = OS.Path.join(OS.Constants.Path.profileDir, "datareporting");
yield OS.File.makeDir(dataDir);
yield OS.File.makeDir(DATAREPORTING_PATH);
// Write test data to the session data file.
const dataFilePath = OS.Path.join(dataDir, "session-state.json");
const dataFilePath = OS.Path.join(DATAREPORTING_PATH, "session-state.json");
const sessionState = {
profileSubsessionCounter: "not-a-number?",
someOtherField: 12,
@ -1123,6 +1244,265 @@ add_task(function* test_invalidSessionData() {
Assert.equal(data.previousSubsessionId, null);
});
add_task(function* test_abortedSession() {
if (gIsAndroid || gIsGonk) {
// We don't have the aborted session ping here.
return;
}
const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME);
// Make sure the aborted sessions directory does not exist to test its creation.
yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: true });
let schedulerTickCallback = null;
let now = new Date(2040, 1, 1, 0, 0, 0);
fakeNow(now);
// Fake scheduler functions to control aborted-session flow in tests.
fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
yield TelemetrySession.reset();
Assert.ok((yield OS.File.exists(DATAREPORTING_PATH)),
"Telemetry must create the aborted session directory when starting.");
// Fake now again so that the scheduled aborted-session save takes place.
now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
fakeNow(now);
// The first aborted session checkpoint must take place right after the initialisation.
Assert.ok(!!schedulerTickCallback);
// Execute one scheduler tick.
yield schedulerTickCallback();
// Check that the aborted session is due at the correct time.
Assert.ok((yield OS.File.exists(ABORTED_FILE)),
"There must be an aborted session ping.");
// This ping is not yet in the pending pings folder, so we can't access it using
// TelemetryFile.popPendingPings().
let pingContent = yield OS.File.read(ABORTED_FILE, { encoding: "utf-8" });
let abortedSessionPing = JSON.parse(pingContent);
// Validate the ping.
checkPingFormat(abortedSessionPing, PING_TYPE_MAIN, true, true);
Assert.equal(abortedSessionPing.payload.info.reason, REASON_ABORTED_SESSION);
// Trigger a another aborted-session ping and check that it overwrites the previous one.
now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
fakeNow(now);
yield schedulerTickCallback();
pingContent = yield OS.File.read(ABORTED_FILE, { encoding: "utf-8" });
let updatedAbortedSessionPing = JSON.parse(pingContent);
checkPingFormat(updatedAbortedSessionPing, PING_TYPE_MAIN, true, true);
Assert.equal(updatedAbortedSessionPing.payload.info.reason, REASON_ABORTED_SESSION);
Assert.notEqual(abortedSessionPing.id, updatedAbortedSessionPing.id);
Assert.notEqual(abortedSessionPing.creationDate, updatedAbortedSessionPing.creationDate);
yield TelemetrySession.shutdown();
Assert.ok(!(yield OS.File.exists(ABORTED_FILE)),
"No aborted session ping must be available after a shutdown.");
// Write the ping to the aborted-session file. TelemetrySession will add it to the
// saved pings directory when it starts.
yield TelemetryFile.savePingToFile(abortedSessionPing, ABORTED_FILE, false);
gRequestIterator = Iterator(new Request());
yield TelemetrySession.reset();
Assert.ok(!(yield OS.File.exists(ABORTED_FILE)),
"The aborted session ping must be removed from the aborted session ping directory.");
// TelemetryFile requires all the pings to have their ID as filename. When appending
// the aborted-session ping to the pending pings, we must verify that it exists.
const PENDING_PING_FILE =
OS.Path.join(TelemetryFile.pingDirectoryPath, abortedSessionPing.id);
Assert.ok((yield OS.File.exists(PENDING_PING_FILE)),
"The aborted session ping must exist in the saved pings directory.");
// Trick: make the aborted ping file overdue so that it gets sent immediately when
// resetting TelemetryPing.
const OVERDUE_PING_FILE_AGE = TelemetryFile.OVERDUE_PING_FILE_AGE + 60 * 1000;
yield OS.File.setDates(PENDING_PING_FILE, null, Date.now() - OVERDUE_PING_FILE_AGE);
yield TelemetryPing.reset();
// Wait for the aborted-session ping.
let request = yield gRequestIterator.next();
let receivedPing = decodeRequestPayload(request);
Assert.equal(receivedPing.payload.info.reason, REASON_ABORTED_SESSION);
Assert.equal(receivedPing.id, abortedSessionPing.id);
yield TelemetrySession.shutdown();
});
add_task(function* test_abortedDailyCoalescing() {
if (gIsAndroid || gIsGonk) {
// We don't have the aborted session or the daily ping here.
return;
}
const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME);
// Make sure the aborted sessions directory does not exist to test its creation.
yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: true });
let schedulerTickCallback = null;
gRequestIterator = Iterator(new Request());
let nowDate = new Date(2009, 10, 18, 00, 00, 0);
fakeNow(nowDate);
// Fake scheduler functions to control aborted-session flow in tests.
fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
yield TelemetrySession.reset();
Assert.ok((yield OS.File.exists(DATAREPORTING_PATH)),
"Telemetry must create the aborted session directory when starting.");
// Delay the callback around midnight so that the aborted-session ping gets merged with the
// daily ping.
let dailyDueDate = futureDate(nowDate, MS_IN_ONE_DAY);
fakeNow(dailyDueDate);
// Trigger both the daily ping and the saved-session.
Assert.ok(!!schedulerTickCallback);
// Execute one scheduler tick.
yield schedulerTickCallback();
// Wait for the daily ping.
let request = yield gRequestIterator.next();
let dailyPing = decodeRequestPayload(request);
Assert.equal(dailyPing.payload.info.reason, REASON_DAILY);
// Check that an aborted session ping was also written to disk.
Assert.ok((yield OS.File.exists(ABORTED_FILE)),
"There must be an aborted session ping.");
// Read aborted session ping and check that the session/subsession ids equal the
// ones in the daily ping.
let pingContent = yield OS.File.read(ABORTED_FILE, { encoding: "utf-8" });
let abortedSessionPing = JSON.parse(pingContent);
Assert.equal(abortedSessionPing.payload.info.sessionId, dailyPing.payload.info.sessionId);
Assert.equal(abortedSessionPing.payload.info.subsessionId, dailyPing.payload.info.subsessionId);
yield TelemetrySession.shutdown();
});
add_task(function* test_schedulerComputerSleep() {
if (gIsAndroid || gIsGonk) {
// We don't have the aborted session or the daily ping here.
return;
}
const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME);
gRequestIterator = Iterator(new Request());
// Remove any aborted-session ping from the previous tests.
yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: true });
// Set a fake current date and start Telemetry.
let nowDate = new Date(2009, 10, 18, 0, 00, 0);
fakeNow(nowDate);
let schedulerTickCallback = null;
fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
yield TelemetrySession.reset();
// Set the current time 3 days in the future at midnight, before running the callback.
let future = futureDate(nowDate, MS_IN_ONE_DAY * 3);
fakeNow(future);
Assert.ok(!!schedulerTickCallback);
// Execute one scheduler tick.
yield schedulerTickCallback();
let request = yield gRequestIterator.next();
let dailyPing = decodeRequestPayload(request);
Assert.equal(dailyPing.payload.info.reason, REASON_DAILY);
Assert.ok((yield OS.File.exists(ABORTED_FILE)),
"There must be an aborted session ping.");
yield TelemetrySession.shutdown();
});
add_task(function* test_schedulerEnvironmentReschedules() {
if (gIsAndroid || gIsGonk) {
// We don't have the aborted session or the daily ping here.
return;
}
// Reset the test preference.
const PREF_TEST = "toolkit.telemetry.test.pref1";
Preferences.reset(PREF_TEST);
let prefsToWatch = {};
prefsToWatch[PREF_TEST] = TelemetryEnvironment.RECORD_PREF_VALUE;
gRequestIterator = Iterator(new Request());
// Set a fake current date and start Telemetry.
let nowDate = new Date(2009, 10, 18, 0, 00, 0);
fakeNow(nowDate);
let schedulerTickCallback = null;
fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
yield TelemetrySession.reset();
TelemetryEnvironment._watchPreferences(prefsToWatch);
// Set the current time at midnight.
let future = futureDate(nowDate, MS_IN_ONE_DAY);
fakeNow(future);
// Trigger the environment change.
Preferences.set(PREF_TEST, 1);
// Wait for the environment-changed ping.
yield gRequestIterator.next();
// We don't expect to receive any daily ping in this test, so assert if we do.
registerPingHandler((req, res) => {
Assert.ok(false, "No ping should be sent/received in this test.");
});
// Execute one scheduler tick. It should not trigger a daily ping.
Assert.ok(!!schedulerTickCallback);
yield schedulerTickCallback();
yield TelemetrySession.shutdown();
});
add_task(function* test_schedulerNothingDue() {
if (gIsAndroid || gIsGonk) {
// We don't have the aborted session or the daily ping here.
return;
}
const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME);
// Remove any aborted-session ping from the previous tests.
yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: true });
// We don't expect to receive any ping in this test, so assert if we do.
registerPingHandler((req, res) => {
Assert.ok(false, "No ping should be sent/received in this test.");
});
// Set a current date/time away from midnight, so that the daily ping doesn't get
// sent.
let nowDate = new Date(2009, 10, 18, 11, 0, 0);
fakeNow(nowDate);
let schedulerTickCallback = null;
fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
yield TelemetrySession.reset();
// Delay the callback execution to a time when no ping should be due.
let nothingDueDate = futureDate(nowDate, ABORTED_SESSION_UPDATE_INTERVAL_MS / 2);
fakeNow(nothingDueDate);
Assert.ok(!!schedulerTickCallback);
// Execute one scheduler tick.
yield schedulerTickCallback();
// Check that no aborted session ping was written to disk.
Assert.ok(!(yield OS.File.exists(ABORTED_FILE)));
yield TelemetrySession.shutdown();
});
add_task(function* stopServer(){
gHttpServer.stop(do_test_finished);
});

View File

@ -8,7 +8,7 @@
const Services = require("Services");
const { Cc, Ci, Cu, components, ChromeWorker } = require("chrome");
const { ActorPool, OriginalLocation, GeneratedLocation, getOffsetColumn } = require("devtools/server/actors/common");
const { ActorPool, OriginalLocation, GeneratedLocation } = require("devtools/server/actors/common");
const { DebuggerServer } = require("devtools/server/main");
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
const { dbg_assert, dumpn, update, fetch } = DevToolsUtils;
@ -444,7 +444,6 @@ function ThreadActor(aParent, aGlobal)
this._threadLifetimePool = null;
this._tabClosed = false;
this._scripts = null;
this._sources = null;
this._pauseOnDOMEvents = null;
this._options = {
@ -452,14 +451,12 @@ function ThreadActor(aParent, aGlobal)
autoBlackBox: false
};
this.breakpointActorMap = new BreakpointActorMap;
this.sourceActorStore = new SourceActorStore;
this.blackBoxedSources = new Set;
this.prettyPrintedSources = new Map;
this.breakpointActorMap = new BreakpointActorMap();
this.sourceActorStore = new SourceActorStore();
// A map of actorID -> actor for breakpoints created and managed by the
// server.
this._hiddenBreakpoints = new Map;
this._hiddenBreakpoints = new Map();
this.global = aGlobal;
@ -522,11 +519,7 @@ ThreadActor.prototype = {
},
get sources() {
if (!this._sources) {
this._sources = new ThreadSources(this, this._options,
this._allowSource, this.onNewSource);
}
return this._sources;
return this._parent.sources;
},
get youngestFrame() {
@ -667,6 +660,10 @@ ThreadActor.prototype = {
this._state = "attached";
update(this._options, aRequest.options || {});
this.sources.reconfigure(this._options);
this.sources.on('newSource', (name, source) => {
this.onNewSource(source);
});
// Initialize an event loop stack. This can't be done in the constructor,
// because this.conn is not yet initialized by the actor pool at that time.
@ -721,8 +718,8 @@ ThreadActor.prototype = {
}
update(this._options, aRequest.options || {});
// Clear existing sources, so they can be recreated on next access.
this._sources = null;
// Update the global source store
this.sources.reconfigure(this._options);
return {};
},
@ -2013,18 +2010,6 @@ ThreadActor.prototype = {
});
},
/**
* Check if scripts from the provided source URL are allowed to be stored in
* the cache.
*
* @param aSourceUrl String
* The url of the script's source that will be stored.
* @returns true, if the script can be added, false otherwise.
*/
_allowSource: function (aSource) {
return !isHiddenSource(aSource);
},
/**
* Restore any pre-existing breakpoints to the scripts that we have access to.
*/
@ -2046,7 +2031,7 @@ ThreadActor.prototype = {
* @returns true, if the script was added; false otherwise.
*/
_addScript: function (aScript) {
if (!this._allowSource(aScript.source)) {
if (!this.sources.allowSource(aScript.source)) {
return false;
}
@ -2054,7 +2039,12 @@ ThreadActor.prototype = {
let promises = [];
let sourceActor = this.sources.createNonSourceMappedActor(aScript.source);
let endLine = aScript.startLine + aScript.lineCount - 1;
for (let actor of this.breakpointActorMap.findActors()) {
for (let _actor of this.breakpointActorMap.findActors()) {
// XXX bug 1142115: We do async work in here, so we need to
// create a fresh binding because for/of does not yet do that in
// SpiderMonkey
let actor = _actor;
if (actor.isPending) {
promises.push(sourceActor._setBreakpointForActor(actor));
} else {
@ -3270,6 +3260,7 @@ SourceActor.prototype.requestTypes = {
"setBreakpoint": SourceActor.prototype.onSetBreakpoint
};
exports.SourceActor = SourceActor;
/**
* Determine if a given value is non-primitive.
@ -4926,30 +4917,43 @@ BreakpointActor.prototype = {
/**
* Check if this breakpoint has a condition that doesn't error and
* evaluates to true in aFrame
* evaluates to true in aFrame.
*
* @param aFrame Debugger.Frame
* The frame to evaluate the condition in
* @returns Boolean
* Indicates whether to pause or not, returns undefined when
* evaluation was killed
* @returns Object
* - result: boolean|undefined
* True when the conditional breakpoint should trigger a pause, false otherwise.
* If the condition evaluation failed/killed, `result` will be `undefined`.
* - message: string
* The thrown message converted to a string, when the condition throws.
*/
checkCondition: function(aFrame) {
let completion = aFrame.eval(this.condition);
if (completion) {
if (completion.throw) {
// The evaluation failed and threw an error, currently
// we will only return true to break on the error
return true;
// The evaluation failed and threw
let message = "Unknown exception";
try {
if (completion.throw.getOwnPropertyDescriptor) {
message = completion.throw.getOwnPropertyDescriptor("message").value;
} else if (completion.toString) {
message = completion.toString();
}
} catch (ex) {}
return {
result: true,
message: message
};
} else if (completion.yield) {
dbg_assert(false,
"Shouldn't ever get yield completions from an eval");
} else {
return completion.return ? true : false;
return { result: completion.return ? true : false };
}
} else {
// The evaluation was killed (possibly by the slow script dialog)
return undefined;
return { result: undefined };
}
},
@ -4976,12 +4980,24 @@ BreakpointActor.prototype = {
if (this.threadActor._hiddenBreakpoints.has(this.actorID)) {
reason.type = "pauseOnDOMEvents";
} else if (!this.condition || this.checkCondition(aFrame)) {
} else if (!this.condition) {
reason.type = "breakpoint";
// TODO: add the rest of the breakpoints on that line (bug 676602).
reason.actors = [ this.actorID ];
} else {
return undefined;
let { result, message } = this.checkCondition(aFrame)
if (result) {
if (!message) {
reason.type = "breakpoint";
} else {
reason.type = "breakpointConditionThrown";
reason.message = message;
}
reason.actors = [ this.actorID ];
} else {
return undefined;
}
}
return this.threadActor._pauseAndRespond(aFrame, reason);
},
@ -5302,738 +5318,13 @@ update(AddonThreadActor.prototype, {
constructor: AddonThreadActor,
// A constant prefix that will be used to form the actor ID by the server.
actorPrefix: "addonThread",
/**
* Override the eligibility check for scripts and sources to make
* sure every script and source with a URL is stored when debugging
* add-ons.
*/
_allowSource: function(aSource) {
let url = aSource.url;
if (isHiddenSource(aSource)) {
return false;
}
// XPIProvider.jsm evals some code in every add-on's bootstrap.js. Hide it.
if (url === "resource://gre/modules/addons/XPIProvider.jsm") {
return false;
}
return true;
},
actorPrefix: "addonThread"
});
exports.AddonThreadActor = AddonThreadActor;
/**
* Manages the sources for a thread. Handles source maps, locations in the
* sources, etc for ThreadActors.
*/
function ThreadSources(aThreadActor, aOptions, aAllowPredicate,
aOnNewSource) {
this._thread = aThreadActor;
this._useSourceMaps = aOptions.useSourceMaps;
this._autoBlackBox = aOptions.autoBlackBox;
this._allow = aAllowPredicate;
this._onNewSource = DevToolsUtils.makeInfallible(
aOnNewSource,
"ThreadSources.prototype._onNewSource"
);
this._anonSourceMapId = 1;
// generated Debugger.Source -> promise of SourceMapConsumer
this._sourceMaps = new Map();
// sourceMapURL -> promise of SourceMapConsumer
this._sourceMapCache = Object.create(null);
// Debugger.Source -> SourceActor
this._sourceActors = new Map();
// url -> SourceActor
this._sourceMappedSourceActors = Object.create(null);
}
/**
* Matches strings of the form "foo.min.js" or "foo-min.js", etc. If the regular
* expression matches, we can be fairly sure that the source is minified, and
* treat it as such.
*/
const MINIFIED_SOURCE_REGEXP = /\bmin\.js$/;
ThreadSources.prototype = {
/**
* Return the source actor representing the `source` (or
* `originalUrl`), creating one if none exists already. May return
* null if the source is disallowed.
*
* @param Debugger.Source source
* The source to make an actor for
* @param String originalUrl
* The original source URL of a sourcemapped source
* @param optional Debguger.Source generatedSource
* The generated source that introduced this source via source map,
* if any.
* @param optional String contentType
* The content type of the source, if immediately available.
* @returns a SourceActor representing the source or null.
*/
source: function ({ source, originalUrl, generatedSource,
isInlineSource, contentType }) {
dbg_assert(source || (originalUrl && generatedSource),
"ThreadSources.prototype.source needs an originalUrl or a source");
if (source) {
// If a source is passed, we are creating an actor for a real
// source, which may or may not be sourcemapped.
if (!this._allow(source)) {
return null;
}
// It's a hack, but inline HTML scripts each have real sources,
// but we want to represent all of them as one source as the
// HTML page. The actor representing this fake HTML source is
// stored in this array, which always has a URL, so check it
// first.
if (source.url in this._sourceMappedSourceActors) {
return this._sourceMappedSourceActors[source.url];
}
if (isInlineSource) {
// If it's an inline source, the fake HTML source hasn't been
// created yet (would have returned above), so flip this source
// into a sourcemapped state by giving it an `originalUrl` which
// is the HTML url.
originalUrl = source.url;
source = null;
}
else if (this._sourceActors.has(source)) {
return this._sourceActors.get(source);
}
}
else if (originalUrl) {
// Not all "original" scripts are distinctly separate from the
// generated script. Pretty-printed sources have a sourcemap for
// themselves, so we need to make sure there a real source
// doesn't already exist with this URL.
for (let [source, actor] of this._sourceActors) {
if (source.url === originalUrl) {
return actor;
}
}
if (originalUrl in this._sourceMappedSourceActors) {
return this._sourceMappedSourceActors[originalUrl];
}
}
let actor = new SourceActor({
thread: this._thread,
source: source,
originalUrl: originalUrl,
generatedSource: generatedSource,
contentType: contentType
});
let sourceActorStore = this._thread.sourceActorStore;
var id = sourceActorStore.getReusableActorId(source, originalUrl);
if (id) {
actor.actorID = id;
}
this._thread.threadLifetimePool.addActor(actor);
sourceActorStore.setReusableActorId(source, originalUrl, actor.actorID);
if (this._autoBlackBox && this._isMinifiedURL(actor.url)) {
this.blackBox(actor.url);
}
if (source) {
this._sourceActors.set(source, actor);
}
else {
this._sourceMappedSourceActors[originalUrl] = actor;
}
this._emitNewSource(actor);
return actor;
},
_emitNewSource: function(actor) {
if(!actor.source) {
// Always notify if we don't have a source because that means
// it's something that has been sourcemapped, or it represents
// the HTML file that contains inline sources.
this._onNewSource(actor);
}
else {
// If sourcemapping is enabled and a source has sourcemaps, we
// create `SourceActor` instances for both the original and
// generated sources. The source actors for the generated
// sources are only for internal use, however; breakpoints are
// managed by these internal actors. We only want to notify the
// user of the original sources though, so if the actor has a
// `Debugger.Source` instance and a valid source map (meaning
// it's a generated source), don't send the notification.
this.fetchSourceMap(actor.source).then(map => {
if(!map) {
this._onNewSource(actor);
}
});
}
},
getSourceActor: function(source) {
if (source.url in this._sourceMappedSourceActors) {
return this._sourceMappedSourceActors[source.url];
}
if (this._sourceActors.has(source)) {
return this._sourceActors.get(source);
}
throw new Error('getSource: could not find source actor for ' +
(source.url || 'source'));
},
getSourceActorByURL: function(url) {
if (url) {
for (let [source, actor] of this._sourceActors) {
if (source.url === url) {
return actor;
}
}
if (url in this._sourceMappedSourceActors) {
return this._sourceMappedSourceActors[url];
}
}
throw new Error('getSourceByURL: could not find source for ' + url);
},
/**
* Returns true if the URL likely points to a minified resource, false
* otherwise.
*
* @param String aURL
* The URL to test.
* @returns Boolean
*/
_isMinifiedURL: function (aURL) {
try {
let url = Services.io.newURI(aURL, null, null)
.QueryInterface(Ci.nsIURL);
return MINIFIED_SOURCE_REGEXP.test(url.fileName);
} catch (e) {
// Not a valid URL so don't try to parse out the filename, just test the
// whole thing with the minified source regexp.
return MINIFIED_SOURCE_REGEXP.test(aURL);
}
},
/**
* Create a source actor representing this source. This ignores
* source mapping and always returns an actor representing this real
* source. Use `createSourceActors` if you want to respect source maps.
*
* @param Debugger.Source aSource
* The source instance to create an actor for.
* @returns SourceActor
*/
createNonSourceMappedActor: function (aSource) {
// Don't use getSourceURL because we don't want to consider the
// displayURL property if it's an eval source. We only want to
// consider real URLs, otherwise if there is a URL but it's
// invalid the code below will not set the content type, and we
// will later try to fetch the contents of the URL to figure out
// the content type, but it's a made up URL for eval sources.
let url = isEvalSource(aSource) ? null : aSource.url;
let spec = { source: aSource };
// XXX bug 915433: We can't rely on Debugger.Source.prototype.text
// if the source is an HTML-embedded <script> tag. Since we don't
// have an API implemented to detect whether this is the case, we
// need to be conservative and only treat valid js files as real
// sources. Otherwise, use the `originalUrl` property to treat it
// as an HTML source that manages multiple inline sources.
if (url) {
try {
let urlInfo = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL);
if (urlInfo.fileExtension === "html") {
spec.isInlineSource = true;
}
else if (urlInfo.fileExtension === "js") {
spec.contentType = "text/javascript";
}
} catch(ex) {
// Not a valid URI.
// bug 1124536: fix getSourceText on scripts associated "javascript:SOURCE" urls
// (e.g. 'evaluate(sandbox, sourcecode, "javascript:"+sourcecode)' )
if (url.indexOf("javascript:") === 0) {
spec.contentType = "text/javascript";
}
}
}
else {
// Assume the content is javascript if there's no URL
spec.contentType = "text/javascript";
}
return this.source(spec);
},
/**
* This is an internal function that returns a promise of an array
* of source actors representing all the source mapped sources of
* `aSource`, or `null` if the source is not sourcemapped or
* sourcemapping is disabled. Users should call `createSourceActors`
* instead of this.
*
* @param Debugger.Source aSource
* The source instance to create actors for.
* @return Promise of an array of source actors
*/
_createSourceMappedActors: function (aSource) {
if (!this._useSourceMaps || !aSource.sourceMapURL) {
return resolve(null);
}
return this.fetchSourceMap(aSource)
.then(map => {
if (map) {
return [
this.source({ originalUrl: s, generatedSource: aSource })
for (s of map.sources)
].filter(isNotNull);
}
return null;
});
},
/**
* Creates the source actors representing the appropriate sources
* of `aSource`. If sourcemapped, returns actors for all of the original
* sources, otherwise returns a 1-element array with the actor for
* `aSource`.
*
* @param Debugger.Source aSource
* The source instance to create actors for.
* @param Promise of an array of source actors
*/
createSourceActors: function(aSource) {
return this._createSourceMappedActors(aSource).then(actors => {
let actor = this.createNonSourceMappedActor(aSource);
return (actors || [actor]).filter(isNotNull);
});
},
/**
* Return a promise of a SourceMapConsumer for the source map for
* `aSource`; if we already have such a promise extant, return that.
* This will fetch the source map if we don't have a cached object
* and source maps are enabled (see `_fetchSourceMap`).
*
* @param Debugger.Source aSource
* The source instance to get sourcemaps for.
* @return Promise of a SourceMapConsumer
*/
fetchSourceMap: function (aSource) {
if (this._sourceMaps.has(aSource)) {
return this._sourceMaps.get(aSource);
}
else if (!aSource || !aSource.sourceMapURL) {
return resolve(null);
}
let sourceMapURL = aSource.sourceMapURL;
if (aSource.url) {
sourceMapURL = this._normalize(sourceMapURL, aSource.url);
}
let result = this._fetchSourceMap(sourceMapURL, aSource.url);
// The promises in `_sourceMaps` must be the exact same instances
// as returned by `_fetchSourceMap` for `clearSourceMapCache` to work.
this._sourceMaps.set(aSource, result);
return result;
},
/**
* Return a promise of a SourceMapConsumer for the source map for
* `aSource`. The resolved result may be null if the source does not
* have a source map or source maps are disabled.
*/
getSourceMap: function(aSource) {
return resolve(this._sourceMaps.get(aSource));
},
/**
* Set a SourceMapConsumer for the source map for
* |aSource|.
*/
setSourceMap: function(aSource, aMap) {
this._sourceMaps.set(aSource, resolve(aMap));
},
/**
* Return a promise of a SourceMapConsumer for the source map located at
* |aAbsSourceMapURL|, which must be absolute. If there is already such a
* promise extant, return it. This will not fetch if source maps are
* disabled.
*
* @param string aAbsSourceMapURL
* The source map URL, in absolute form, not relative.
* @param string aScriptURL
* When the source map URL is a data URI, there is no sourceRoot on the
* source map, and the source map's sources are relative, we resolve
* them from aScriptURL.
*/
_fetchSourceMap: function (aAbsSourceMapURL, aSourceURL) {
if (this._sourceMapCache[aAbsSourceMapURL]) {
return this._sourceMapCache[aAbsSourceMapURL];
}
else if (!this._useSourceMaps) {
return resolve(null);
}
let fetching = fetch(aAbsSourceMapURL, { loadFromCache: false })
.then(({ content }) => {
let map = new SourceMapConsumer(content);
this._setSourceMapRoot(map, aAbsSourceMapURL, aSourceURL);
return map;
})
.then(null, error => {
if (!DevToolsUtils.reportingDisabled) {
DevToolsUtils.reportException("ThreadSources.prototype._fetchSourceMap", error);
}
return null;
});
this._sourceMapCache[aAbsSourceMapURL] = fetching;
return fetching;
},
/**
* Sets the source map's sourceRoot to be relative to the source map url.
*/
_setSourceMapRoot: function (aSourceMap, aAbsSourceMapURL, aScriptURL) {
const base = this._dirname(
aAbsSourceMapURL.indexOf("data:") === 0
? aScriptURL
: aAbsSourceMapURL);
aSourceMap.sourceRoot = aSourceMap.sourceRoot
? this._normalize(aSourceMap.sourceRoot, base)
: base;
},
_dirname: function (aPath) {
return Services.io.newURI(
".", null, Services.io.newURI(aPath, null, null)).spec;
},
/**
* Clears the source map cache. Source maps are cached by URL so
* they can be reused across separate Debugger instances (once in
* this cache, they will never be reparsed again). They are
* also cached by Debugger.Source objects for usefulness. By default
* this just removes the Debugger.Source cache, but you can remove
* the lower-level URL cache with the `hard` option.
*
* @param aSourceMapURL string
* The source map URL to uncache
* @param opts object
* An object with the following properties:
* - hard: Also remove the lower-level URL cache, which will
* make us completely forget about the source map.
*/
clearSourceMapCache: function(aSourceMapURL, opts = { hard: false }) {
let oldSm = this._sourceMapCache[aSourceMapURL];
if (opts.hard) {
delete this._sourceMapCache[aSourceMapURL];
}
if (oldSm) {
// Clear out the current cache so all sources will get the new one
for (let [source, sm] of this._sourceMaps.entries()) {
if (sm === oldSm) {
this._sourceMaps.delete(source);
}
}
}
},
/*
* Forcefully change the source map of a source, changing the
* sourceMapURL and installing the source map in the cache. This is
* necessary to expose changes across Debugger instances
* (pretty-printing is the use case). Generate a random url if one
* isn't specified, allowing you to set "anonymous" source maps.
*
* @param aSource Debugger.Source
* The source to change the sourceMapURL property
* @param aUrl string
* The source map URL (optional)
* @param aMap SourceMapConsumer
* The source map instance
*/
setSourceMapHard: function(aSource, aUrl, aMap) {
let url = aUrl;
if (!url) {
// This is a littly hacky, but we want to forcefully set a
// sourcemap regardless of sourcemap settings. We want to
// literally change the sourceMapURL so that all debuggers will
// get this and pretty-printing will Just Work (Debugger.Source
// instances are per-debugger, so we can't key off that). To
// avoid tons of work serializing the sourcemap into a data url,
// just make a fake URL and stick the sourcemap there.
url = "internal://sourcemap" + (this._anonSourceMapId++) + '/';
}
aSource.sourceMapURL = url;
// Forcefully set the sourcemap cache. This will be used even if
// sourcemaps are disabled.
this._sourceMapCache[url] = resolve(aMap);
},
/**
* Return the non-source-mapped location of the given Debugger.Frame. If the
* frame does not have a script, the location's properties are all null.
*
* @param Debugger.Frame aFrame
* The frame whose location we are getting.
* @returns Object
* Returns an object of the form { source, line, column }
*/
getFrameLocation: function (aFrame) {
if (!aFrame || !aFrame.script) {
return new GeneratedLocation();
}
return new GeneratedLocation(
this.createNonSourceMappedActor(aFrame.script.source),
aFrame.script.getOffsetLine(aFrame.offset),
getOffsetColumn(aFrame.offset, aFrame.script)
);
},
/**
* Returns a promise of the location in the original source if the source is
* source mapped, otherwise a promise of the same location. This can
* be called with a source from *any* Debugger instance and we make
* sure to that it works properly, reusing source maps if already
* fetched. Use this from any actor that needs sourcemapping.
*/
getOriginalLocation: function (generatedLocation) {
let {
generatedSourceActor,
generatedLine,
generatedColumn
} = generatedLocation;
let source = generatedSourceActor.source;
let url = source ? source.url : generatedSourceActor._originalUrl;
// In certain scenarios the source map may have not been fetched
// yet (or at least tied to this Debugger.Source instance), so use
// `fetchSourceMap` instead of `getSourceMap`. This allows this
// function to be called from anywere (across debuggers) and it
// should just automatically work.
return this.fetchSourceMap(source).then(map => {
if (map) {
let {
source: originalUrl,
line: originalLine,
column: originalColumn,
name: originalName
} = map.originalPositionFor({
line: generatedLine,
column: generatedColumn == null ? Infinity : generatedColumn
});
// Since the `Debugger.Source` instance may come from a
// different `Debugger` instance (any actor can call this
// method), we can't rely on any of the source discovery
// setup (`_discoverSources`, etc) to have been run yet. So
// we have to assume that the actor may not already exist,
// and we might need to create it, so use `source` and give
// it the required parameters for a sourcemapped source.
return new OriginalLocation(
originalUrl ? this.source({
originalUrl: originalUrl,
generatedSource: source
}) : null,
originalLine,
originalColumn,
originalName
);
}
// No source map
return OriginalLocation.fromGeneratedLocation(generatedLocation);
});
},
/**
* Returns a promise of the location in the generated source corresponding to
* the original source and line given.
*
* When we pass a script S representing generated code to `sourceMap`,
* above, that returns a promise P. The process of resolving P populates
* the tables this function uses; thus, it won't know that S's original
* source URLs map to S until P is resolved.
*/
getGeneratedLocation: function (originalLocation) {
let { originalSourceActor } = originalLocation;
// Both original sources and normal sources could have sourcemaps,
// because normal sources can be pretty-printed which generates a
// sourcemap for itself. Check both of the source properties to make it work
// for both kinds of sources.
let source = originalSourceActor.source || originalSourceActor.generatedSource;
// See comment about `fetchSourceMap` in `getOriginalLocation`.
return this.fetchSourceMap(source).then((map) => {
if (map) {
let {
originalLine,
originalColumn
} = originalLocation;
let {
line: generatedLine,
column: generatedColumn
} = map.generatedPositionFor({
source: originalSourceActor.url,
line: originalLine,
column: originalColumn == null ? 0 : originalColumn,
bias: SourceMapConsumer.LEAST_UPPER_BOUND
});
return new GeneratedLocation(
this.createNonSourceMappedActor(source),
generatedLine,
generatedColumn
);
}
return GeneratedLocation.fromOriginalLocation(originalLocation);
});
},
/**
* Returns true if URL for the given source is black boxed.
*
* @param aURL String
* The URL of the source which we are checking whether it is black
* boxed or not.
*/
isBlackBoxed: function (aURL) {
return this._thread.blackBoxedSources.has(aURL);
},
/**
* Add the given source URL to the set of sources that are black boxed.
*
* @param aURL String
* The URL of the source which we are black boxing.
*/
blackBox: function (aURL) {
this._thread.blackBoxedSources.add(aURL);
},
/**
* Remove the given source URL to the set of sources that are black boxed.
*
* @param aURL String
* The URL of the source which we are no longer black boxing.
*/
unblackBox: function (aURL) {
this._thread.blackBoxedSources.delete(aURL);
},
/**
* Returns true if the given URL is pretty printed.
*
* @param aURL String
* The URL of the source that might be pretty printed.
*/
isPrettyPrinted: function (aURL) {
return this._thread.prettyPrintedSources.has(aURL);
},
/**
* Add the given URL to the set of sources that are pretty printed.
*
* @param aURL String
* The URL of the source to be pretty printed.
*/
prettyPrint: function (aURL, aIndent) {
this._thread.prettyPrintedSources.set(aURL, aIndent);
},
/**
* Return the indent the given URL was pretty printed by.
*/
prettyPrintIndent: function (aURL) {
return this._thread.prettyPrintedSources.get(aURL);
},
/**
* Remove the given URL from the set of sources that are pretty printed.
*
* @param aURL String
* The URL of the source that is no longer pretty printed.
*/
disablePrettyPrint: function (aURL) {
this._thread.prettyPrintedSources.delete(aURL);
},
/**
* Normalize multiple relative paths towards the base paths on the right.
*/
_normalize: function (...aURLs) {
dbg_assert(aURLs.length > 1, "Should have more than 1 URL");
let base = Services.io.newURI(aURLs.pop(), null, null);
let url;
while ((url = aURLs.pop())) {
base = Services.io.newURI(url, null, base);
}
return base.spec;
},
iter: function () {
let actors = Object.keys(this._sourceMappedSourceActors).map(k => {
return this._sourceMappedSourceActors[k];
});
for (let actor of this._sourceActors.values()) {
if (!this._sourceMaps.has(actor.source)) {
actors.push(actor);
}
}
return actors;
}
};
exports.ThreadSources = ThreadSources;
// Utility functions.
/**
* Checks if a source should never be displayed to the user because
* it's either internal or we don't support in the UI yet.
*/
function isHiddenSource(aSource) {
// Ignore the internal Function.prototype script
return aSource.text === '() {\n}';
}
/**
* Returns true if its argument is not null.
*/
function isNotNull(aThing) {
return aThing !== null;
}
/**
* Report the given error in the error console and to stdout.
*
@ -6087,6 +5378,7 @@ function isEvalSource(source) {
introType === 'setTimeout' ||
introType === 'setInterval');
}
exports.isEvalSource = isEvalSource;
function getSourceURL(source) {
if(isEvalSource(source)) {
@ -6105,6 +5397,7 @@ function getSourceURL(source) {
}
return source.url;
}
exports.getSourceURL = getSourceURL;
/**
* Find the scripts which contain offsets that are an entry point to the given

View File

@ -991,6 +991,17 @@ var StyleRuleActor = protocol.ActorClass({
if (this.rawRule.parentRule) {
form.parentRule = this.pageStyle._styleRef(this.rawRule.parentRule).actorID;
// CSS rules that we call media rules are STYLE_RULES that are children
// of MEDIA_RULEs. We need to check the parentRule to check if a rule is
// a media rule so we do this here instead of in the switch statement
// below.
if (this.rawRule.parentRule.type === Ci.nsIDOMCSSRule.MEDIA_RULE) {
form.media = [];
for (let i = 0, n = this.rawRule.parentRule.media.length; i < n; i++) {
form.media.push(this.rawRule.parentRule.media.item(i));
}
}
}
if (this.rawRule.parentStyleSheet) {
form.parentStyleSheet = this.pageStyle._sheetRef(this.rawRule.parentStyleSheet).actorID;
@ -1014,12 +1025,6 @@ var StyleRuleActor = protocol.ActorClass({
case Ci.nsIDOMCSSRule.IMPORT_RULE:
form.href = this.rawRule.href;
break;
case Ci.nsIDOMCSSRule.MEDIA_RULE:
form.media = [];
for (let i = 0, n = this.rawRule.media.length; i < n; i++) {
form.media.push(this.rawRule.media.item(i));
}
break;
case Ci.nsIDOMCSSRule.KEYFRAMES_RULE:
form.cssText = this.rawRule.cssText;
form.name = this.rawRule.name;
@ -1245,9 +1250,10 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
if (this._originalLocation) {
return promise.resolve(this._originalLocation);
}
let parentSheet = this.parentStyleSheet;
if (!parentSheet) {
// This rule doesn't belong to a stylesheet so it is an inline style.
// Inline styles do not have any mediaText so we can return early.
return promise.resolve(this.location);
}
return parentSheet.getOriginalLocation(this.line, this.column)
@ -1255,8 +1261,9 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
let location = {
href: source,
line: line,
column: column
}
column: column,
mediaText: this.mediaText
};
if (fromSourceMap === false) {
location.source = this.parentStyleSheet;
}
@ -1265,7 +1272,7 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
}
this._originalLocation = location;
return location;
})
});
}
});

View File

@ -744,7 +744,7 @@ let StyleSheetActor = protocol.ActorClass({
source: this.href,
line: line,
column: column
}
};
});
}, {
request: {

View File

@ -0,0 +1,751 @@
const Services = require("Services");
const { Ci, Cu } = require("chrome");
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
const EventEmitter = require("devtools/toolkit/event-emitter");
const { dbg_assert, fetch } = require("devtools/toolkit/DevToolsUtils");
const { OriginalLocation, GeneratedLocation, getOffsetColumn } = require("devtools/server/actors/common");
const { resolve } = require("promise");
loader.lazyRequireGetter(this, "SourceActor", "devtools/server/actors/script", true);
loader.lazyRequireGetter(this, "isEvalSource", "devtools/server/actors/script", true);
loader.lazyRequireGetter(this, "SourceMapConsumer", "source-map", true);
loader.lazyRequireGetter(this, "SourceMapGenerator", "source-map", true);
/**
* Manages the sources for a thread. Handles source maps, locations in the
* sources, etc for ThreadActors.
*/
function TabSources(threadActor, allowSourceFn=() => true) {
EventEmitter.decorate(this);
this._thread = threadActor;
this._useSourceMaps = true;
this._autoBlackBox = true;
this._anonSourceMapId = 1;
this.allowSource = source => {
return !isHiddenSource(source) && allowSourceFn(source);
}
this.blackBoxedSources = new Set();
this.prettyPrintedSources = new Map();
// generated Debugger.Source -> promise of SourceMapConsumer
this._sourceMaps = new Map();
// sourceMapURL -> promise of SourceMapConsumer
this._sourceMapCache = Object.create(null);
// Debugger.Source -> SourceActor
this._sourceActors = new Map();
// url -> SourceActor
this._sourceMappedSourceActors = Object.create(null);
}
/**
* Matches strings of the form "foo.min.js" or "foo-min.js", etc. If the regular
* expression matches, we can be fairly sure that the source is minified, and
* treat it as such.
*/
const MINIFIED_SOURCE_REGEXP = /\bmin\.js$/;
TabSources.prototype = {
/**
* Update preferences and clear out existing sources
*/
reconfigure: function(options) {
if('useSourceMaps' in options) {
this._useSourceMaps = options.useSourceMaps;
}
if('autoBlackBox' in options) {
this._autoBlackBox = options.autoBlackBox;
}
this.reset();
},
/**
* Clear existing sources so they are recreated on the next access.
*
* @param Object opts
* Specify { sourceMaps: true } if you also want to clear
* the source map cache (usually done on reload).
*/
reset: function(opts={}) {
this._sourceActors = new Map();
this._sourceMaps = new Map();
this._sourceMappedSourceActors = Object.create(null);
if(opts.sourceMaps) {
this._sourceMapCache = Object.create(null);
}
},
/**
* Return the source actor representing the `source` (or
* `originalUrl`), creating one if none exists already. May return
* null if the source is disallowed.
*
* @param Debugger.Source source
* The source to make an actor for
* @param String originalUrl
* The original source URL of a sourcemapped source
* @param optional Debguger.Source generatedSource
* The generated source that introduced this source via source map,
* if any.
* @param optional String contentType
* The content type of the source, if immediately available.
* @returns a SourceActor representing the source or null.
*/
source: function ({ source, originalUrl, generatedSource,
isInlineSource, contentType }) {
dbg_assert(source || (originalUrl && generatedSource),
"TabSources.prototype.source needs an originalUrl or a source");
if (source) {
// If a source is passed, we are creating an actor for a real
// source, which may or may not be sourcemapped.
if (!this.allowSource(source)) {
return null;
}
// It's a hack, but inline HTML scripts each have real sources,
// but we want to represent all of them as one source as the
// HTML page. The actor representing this fake HTML source is
// stored in this array, which always has a URL, so check it
// first.
if (source.url in this._sourceMappedSourceActors) {
return this._sourceMappedSourceActors[source.url];
}
if (isInlineSource) {
// If it's an inline source, the fake HTML source hasn't been
// created yet (would have returned above), so flip this source
// into a sourcemapped state by giving it an `originalUrl` which
// is the HTML url.
originalUrl = source.url;
source = null;
}
else if (this._sourceActors.has(source)) {
return this._sourceActors.get(source);
}
}
else if (originalUrl) {
// Not all "original" scripts are distinctly separate from the
// generated script. Pretty-printed sources have a sourcemap for
// themselves, so we need to make sure there a real source
// doesn't already exist with this URL.
for (let [source, actor] of this._sourceActors) {
if (source.url === originalUrl) {
return actor;
}
}
if (originalUrl in this._sourceMappedSourceActors) {
return this._sourceMappedSourceActors[originalUrl];
}
}
let actor = new SourceActor({
thread: this._thread,
source: source,
originalUrl: originalUrl,
generatedSource: generatedSource,
contentType: contentType
});
let sourceActorStore = this._thread.sourceActorStore;
var id = sourceActorStore.getReusableActorId(source, originalUrl);
if (id) {
actor.actorID = id;
}
this._thread.threadLifetimePool.addActor(actor);
sourceActorStore.setReusableActorId(source, originalUrl, actor.actorID);
if (this._autoBlackBox && this._isMinifiedURL(actor.url)) {
this.blackBox(actor.url);
}
if (source) {
this._sourceActors.set(source, actor);
}
else {
this._sourceMappedSourceActors[originalUrl] = actor;
}
this._emitNewSource(actor);
return actor;
},
_emitNewSource: function(actor) {
if(!actor.source) {
// Always notify if we don't have a source because that means
// it's something that has been sourcemapped, or it represents
// the HTML file that contains inline sources.
this.emit('newSource', actor);
}
else {
// If sourcemapping is enabled and a source has sourcemaps, we
// create `SourceActor` instances for both the original and
// generated sources. The source actors for the generated
// sources are only for internal use, however; breakpoints are
// managed by these internal actors. We only want to notify the
// user of the original sources though, so if the actor has a
// `Debugger.Source` instance and a valid source map (meaning
// it's a generated source), don't send the notification.
this.fetchSourceMap(actor.source).then(map => {
if(!map) {
this.emit('newSource', actor);
}
});
}
},
getSourceActor: function(source) {
if (source.url in this._sourceMappedSourceActors) {
return this._sourceMappedSourceActors[source.url];
}
if (this._sourceActors.has(source)) {
return this._sourceActors.get(source);
}
throw new Error('getSource: could not find source actor for ' +
(source.url || 'source'));
},
getSourceActorByURL: function(url) {
if (url) {
for (let [source, actor] of this._sourceActors) {
if (source.url === url) {
return actor;
}
}
if (url in this._sourceMappedSourceActors) {
return this._sourceMappedSourceActors[url];
}
}
throw new Error('getSourceByURL: could not find source for ' + url);
},
/**
* Returns true if the URL likely points to a minified resource, false
* otherwise.
*
* @param String aURL
* The URL to test.
* @returns Boolean
*/
_isMinifiedURL: function (aURL) {
try {
let url = Services.io.newURI(aURL, null, null)
.QueryInterface(Ci.nsIURL);
return MINIFIED_SOURCE_REGEXP.test(url.fileName);
} catch (e) {
// Not a valid URL so don't try to parse out the filename, just test the
// whole thing with the minified source regexp.
return MINIFIED_SOURCE_REGEXP.test(aURL);
}
},
/**
* Create a source actor representing this source. This ignores
* source mapping and always returns an actor representing this real
* source. Use `createSourceActors` if you want to respect source maps.
*
* @param Debugger.Source aSource
* The source instance to create an actor for.
* @returns SourceActor
*/
createNonSourceMappedActor: function (aSource) {
// Don't use getSourceURL because we don't want to consider the
// displayURL property if it's an eval source. We only want to
// consider real URLs, otherwise if there is a URL but it's
// invalid the code below will not set the content type, and we
// will later try to fetch the contents of the URL to figure out
// the content type, but it's a made up URL for eval sources.
let url = isEvalSource(aSource) ? null : aSource.url;
let spec = { source: aSource };
// XXX bug 915433: We can't rely on Debugger.Source.prototype.text
// if the source is an HTML-embedded <script> tag. Since we don't
// have an API implemented to detect whether this is the case, we
// need to be conservative and only treat valid js files as real
// sources. Otherwise, use the `originalUrl` property to treat it
// as an HTML source that manages multiple inline sources.
if (url) {
try {
let urlInfo = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL);
if (urlInfo.fileExtension === "html" || urlInfo.fileExtension === "xml") {
spec.isInlineSource = true;
}
else if (urlInfo.fileExtension === "js") {
spec.contentType = "text/javascript";
}
} catch(ex) {
// Not a valid URI.
// bug 1124536: fix getSourceText on scripts associated "javascript:SOURCE" urls
// (e.g. 'evaluate(sandbox, sourcecode, "javascript:"+sourcecode)' )
if (url.indexOf("javascript:") === 0) {
spec.contentType = "text/javascript";
}
}
}
else {
// Assume the content is javascript if there's no URL
spec.contentType = "text/javascript";
}
return this.source(spec);
},
/**
* This is an internal function that returns a promise of an array
* of source actors representing all the source mapped sources of
* `aSource`, or `null` if the source is not sourcemapped or
* sourcemapping is disabled. Users should call `createSourceActors`
* instead of this.
*
* @param Debugger.Source aSource
* The source instance to create actors for.
* @return Promise of an array of source actors
*/
_createSourceMappedActors: function (aSource) {
if (!this._useSourceMaps || !aSource.sourceMapURL) {
return resolve(null);
}
return this.fetchSourceMap(aSource)
.then(map => {
if (map) {
return [
this.source({ originalUrl: s, generatedSource: aSource })
for (s of map.sources)
].filter(isNotNull);
}
return null;
});
},
/**
* Creates the source actors representing the appropriate sources
* of `aSource`. If sourcemapped, returns actors for all of the original
* sources, otherwise returns a 1-element array with the actor for
* `aSource`.
*
* @param Debugger.Source aSource
* The source instance to create actors for.
* @param Promise of an array of source actors
*/
createSourceActors: function(aSource) {
return this._createSourceMappedActors(aSource).then(actors => {
let actor = this.createNonSourceMappedActor(aSource);
return (actors || [actor]).filter(isNotNull);
});
},
/**
* Return a promise of a SourceMapConsumer for the source map for
* `aSource`; if we already have such a promise extant, return that.
* This will fetch the source map if we don't have a cached object
* and source maps are enabled (see `_fetchSourceMap`).
*
* @param Debugger.Source aSource
* The source instance to get sourcemaps for.
* @return Promise of a SourceMapConsumer
*/
fetchSourceMap: function (aSource) {
if (this._sourceMaps.has(aSource)) {
return this._sourceMaps.get(aSource);
}
else if (!aSource || !aSource.sourceMapURL) {
return resolve(null);
}
let sourceMapURL = aSource.sourceMapURL;
if (aSource.url) {
sourceMapURL = this._normalize(sourceMapURL, aSource.url);
}
let result = this._fetchSourceMap(sourceMapURL, aSource.url);
// The promises in `_sourceMaps` must be the exact same instances
// as returned by `_fetchSourceMap` for `clearSourceMapCache` to work.
this._sourceMaps.set(aSource, result);
return result;
},
/**
* Return a promise of a SourceMapConsumer for the source map for
* `aSource`. The resolved result may be null if the source does not
* have a source map or source maps are disabled.
*/
getSourceMap: function(aSource) {
return resolve(this._sourceMaps.get(aSource));
},
/**
* Set a SourceMapConsumer for the source map for
* |aSource|.
*/
setSourceMap: function(aSource, aMap) {
this._sourceMaps.set(aSource, resolve(aMap));
},
/**
* Return a promise of a SourceMapConsumer for the source map located at
* |aAbsSourceMapURL|, which must be absolute. If there is already such a
* promise extant, return it. This will not fetch if source maps are
* disabled.
*
* @param string aAbsSourceMapURL
* The source map URL, in absolute form, not relative.
* @param string aScriptURL
* When the source map URL is a data URI, there is no sourceRoot on the
* source map, and the source map's sources are relative, we resolve
* them from aScriptURL.
*/
_fetchSourceMap: function (aAbsSourceMapURL, aSourceURL) {
if (!this._useSourceMaps) {
return resolve(null);
}
else if (this._sourceMapCache[aAbsSourceMapURL]) {
return this._sourceMapCache[aAbsSourceMapURL];
}
let fetching = fetch(aAbsSourceMapURL, { loadFromCache: false })
.then(({ content }) => {
let map = new SourceMapConsumer(content);
this._setSourceMapRoot(map, aAbsSourceMapURL, aSourceURL);
return map;
})
.then(null, error => {
if (!DevToolsUtils.reportingDisabled) {
DevToolsUtils.reportException("TabSources.prototype._fetchSourceMap", error);
}
return null;
});
this._sourceMapCache[aAbsSourceMapURL] = fetching;
return fetching;
},
/**
* Sets the source map's sourceRoot to be relative to the source map url.
*/
_setSourceMapRoot: function (aSourceMap, aAbsSourceMapURL, aScriptURL) {
const base = this._dirname(
aAbsSourceMapURL.indexOf("data:") === 0
? aScriptURL
: aAbsSourceMapURL);
aSourceMap.sourceRoot = aSourceMap.sourceRoot
? this._normalize(aSourceMap.sourceRoot, base)
: base;
},
_dirname: function (aPath) {
return Services.io.newURI(
".", null, Services.io.newURI(aPath, null, null)).spec;
},
/**
* Clears the source map cache. Source maps are cached by URL so
* they can be reused across separate Debugger instances (once in
* this cache, they will never be reparsed again). They are
* also cached by Debugger.Source objects for usefulness. By default
* this just removes the Debugger.Source cache, but you can remove
* the lower-level URL cache with the `hard` option.
*
* @param aSourceMapURL string
* The source map URL to uncache
* @param opts object
* An object with the following properties:
* - hard: Also remove the lower-level URL cache, which will
* make us completely forget about the source map.
*/
clearSourceMapCache: function(aSourceMapURL, opts = { hard: false }) {
let oldSm = this._sourceMapCache[aSourceMapURL];
if (opts.hard) {
delete this._sourceMapCache[aSourceMapURL];
}
if (oldSm) {
// Clear out the current cache so all sources will get the new one
for (let [source, sm] of this._sourceMaps.entries()) {
if (sm === oldSm) {
this._sourceMaps.delete(source);
}
}
}
},
/*
* Forcefully change the source map of a source, changing the
* sourceMapURL and installing the source map in the cache. This is
* necessary to expose changes across Debugger instances
* (pretty-printing is the use case). Generate a random url if one
* isn't specified, allowing you to set "anonymous" source maps.
*
* @param aSource Debugger.Source
* The source to change the sourceMapURL property
* @param aUrl string
* The source map URL (optional)
* @param aMap SourceMapConsumer
* The source map instance
*/
setSourceMapHard: function(aSource, aUrl, aMap) {
let url = aUrl;
if (!url) {
// This is a littly hacky, but we want to forcefully set a
// sourcemap regardless of sourcemap settings. We want to
// literally change the sourceMapURL so that all debuggers will
// get this and pretty-printing will Just Work (Debugger.Source
// instances are per-debugger, so we can't key off that). To
// avoid tons of work serializing the sourcemap into a data url,
// just make a fake URL and stick the sourcemap there.
url = "internal://sourcemap" + (this._anonSourceMapId++) + '/';
}
aSource.sourceMapURL = url;
// Forcefully set the sourcemap cache. This will be used even if
// sourcemaps are disabled.
this._sourceMapCache[url] = resolve(aMap);
},
/**
* Return the non-source-mapped location of the given Debugger.Frame. If the
* frame does not have a script, the location's properties are all null.
*
* @param Debugger.Frame aFrame
* The frame whose location we are getting.
* @returns Object
* Returns an object of the form { source, line, column }
*/
getFrameLocation: function (aFrame) {
if (!aFrame || !aFrame.script) {
return new GeneratedLocation();
}
return new GeneratedLocation(
this.createNonSourceMappedActor(aFrame.script.source),
aFrame.script.getOffsetLine(aFrame.offset),
getOffsetColumn(aFrame.offset, aFrame.script)
);
},
/**
* Returns a promise of the location in the original source if the source is
* source mapped, otherwise a promise of the same location. This can
* be called with a source from *any* Debugger instance and we make
* sure to that it works properly, reusing source maps if already
* fetched. Use this from any actor that needs sourcemapping.
*/
getOriginalLocation: function (generatedLocation) {
let {
generatedSourceActor,
generatedLine,
generatedColumn
} = generatedLocation;
let source = generatedSourceActor.source;
let url = source ? source.url : generatedSourceActor._originalUrl;
// In certain scenarios the source map may have not been fetched
// yet (or at least tied to this Debugger.Source instance), so use
// `fetchSourceMap` instead of `getSourceMap`. This allows this
// function to be called from anywere (across debuggers) and it
// should just automatically work.
return this.fetchSourceMap(source).then(map => {
if (map) {
let {
source: originalUrl,
line: originalLine,
column: originalColumn,
name: originalName
} = map.originalPositionFor({
line: generatedLine,
column: generatedColumn == null ? Infinity : generatedColumn
});
// Since the `Debugger.Source` instance may come from a
// different `Debugger` instance (any actor can call this
// method), we can't rely on any of the source discovery
// setup (`_discoverSources`, etc) to have been run yet. So
// we have to assume that the actor may not already exist,
// and we might need to create it, so use `source` and give
// it the required parameters for a sourcemapped source.
return new OriginalLocation(
originalUrl ? this.source({
originalUrl: originalUrl,
generatedSource: source
}) : null,
originalLine,
originalColumn,
originalName
);
}
// No source map
return OriginalLocation.fromGeneratedLocation(generatedLocation);
});
},
/**
* Returns a promise of the location in the generated source corresponding to
* the original source and line given.
*
* When we pass a script S representing generated code to `sourceMap`,
* above, that returns a promise P. The process of resolving P populates
* the tables this function uses; thus, it won't know that S's original
* source URLs map to S until P is resolved.
*/
getGeneratedLocation: function (originalLocation) {
let { originalSourceActor } = originalLocation;
// Both original sources and normal sources could have sourcemaps,
// because normal sources can be pretty-printed which generates a
// sourcemap for itself. Check both of the source properties to make it work
// for both kinds of sources.
let source = originalSourceActor.source || originalSourceActor.generatedSource;
// See comment about `fetchSourceMap` in `getOriginalLocation`.
return this.fetchSourceMap(source).then((map) => {
if (map) {
let {
originalLine,
originalColumn
} = originalLocation;
let {
line: generatedLine,
column: generatedColumn
} = map.generatedPositionFor({
source: originalSourceActor.url,
line: originalLine,
column: originalColumn == null ? 0 : originalColumn,
bias: SourceMapConsumer.LEAST_UPPER_BOUND
});
return new GeneratedLocation(
this.createNonSourceMappedActor(source),
generatedLine,
generatedColumn
);
}
return GeneratedLocation.fromOriginalLocation(originalLocation);
});
},
/**
* Returns true if URL for the given source is black boxed.
*
* @param aURL String
* The URL of the source which we are checking whether it is black
* boxed or not.
*/
isBlackBoxed: function (aURL) {
return this.blackBoxedSources.has(aURL);
},
/**
* Add the given source URL to the set of sources that are black boxed.
*
* @param aURL String
* The URL of the source which we are black boxing.
*/
blackBox: function (aURL) {
this.blackBoxedSources.add(aURL);
},
/**
* Remove the given source URL to the set of sources that are black boxed.
*
* @param aURL String
* The URL of the source which we are no longer black boxing.
*/
unblackBox: function (aURL) {
this.blackBoxedSources.delete(aURL);
},
/**
* Returns true if the given URL is pretty printed.
*
* @param aURL String
* The URL of the source that might be pretty printed.
*/
isPrettyPrinted: function (aURL) {
return this.prettyPrintedSources.has(aURL);
},
/**
* Add the given URL to the set of sources that are pretty printed.
*
* @param aURL String
* The URL of the source to be pretty printed.
*/
prettyPrint: function (aURL, aIndent) {
this.prettyPrintedSources.set(aURL, aIndent);
},
/**
* Return the indent the given URL was pretty printed by.
*/
prettyPrintIndent: function (aURL) {
return this.prettyPrintedSources.get(aURL);
},
/**
* Remove the given URL from the set of sources that are pretty printed.
*
* @param aURL String
* The URL of the source that is no longer pretty printed.
*/
disablePrettyPrint: function (aURL) {
this.prettyPrintedSources.delete(aURL);
},
/**
* Normalize multiple relative paths towards the base paths on the right.
*/
_normalize: function (...aURLs) {
dbg_assert(aURLs.length > 1, "Should have more than 1 URL");
let base = Services.io.newURI(aURLs.pop(), null, null);
let url;
while ((url = aURLs.pop())) {
base = Services.io.newURI(url, null, base);
}
return base.spec;
},
iter: function () {
let actors = Object.keys(this._sourceMappedSourceActors).map(k => {
return this._sourceMappedSourceActors[k];
});
for (let actor of this._sourceActors.values()) {
if (!this._sourceMaps.has(actor.source)) {
actors.push(actor);
}
}
return actors;
}
};
/*
* Checks if a source should never be displayed to the user because
* it's either internal or we don't support in the UI yet.
*/
function isHiddenSource(aSource) {
// Ignore the internal Function.prototype script
return aSource.text === '() {\n}';
}
/**
* Returns true if its argument is not null.
*/
function isNotNull(aThing) {
return aThing !== null;
}
exports.TabSources = TabSources;
exports.isHiddenSource = isHiddenSource;

View File

@ -13,6 +13,7 @@ let { RootActor } = require("devtools/server/actors/root");
let { DebuggerServer } = require("devtools/server/main");
let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
let { dbg_assert } = DevToolsUtils;
let { TabSources, isHiddenSource } = require("./utils/TabSources");
let makeDebugger = require("./utils/make-debugger");
let mapURIToAddonID = require("./utils/map-uri-to-addon-id");
@ -598,6 +599,7 @@ function TabActor(aConnection)
// A map of actor names to actor instances provided by extensions.
this._extraActors = {};
this._exited = false;
this._sources = null;
// Map of DOM stylesheets to StyleSheetActors
this._styleSheetActors = new Map();
@ -767,6 +769,14 @@ TabActor.prototype = {
return null;
},
get sources() {
if (!this._sources) {
dbg_assert(this.threadActor, "threadActor should exist when creating sources.");
this._sources = new TabSources(this.threadActor);
}
return this._sources;
},
/**
* This is called by BrowserTabList.getList for existing tab actors prior to
* calling |form| below. It can be used to do any async work that may be
@ -1118,6 +1128,7 @@ TabActor.prototype = {
this._contextPool = null;
this.threadActor.exit();
this.threadActor = null;
this._sources = null;
},
/**
@ -1402,12 +1413,11 @@ TabActor.prototype = {
// TODO bug 997119: move that code to ThreadActor by listening to window-ready
let threadActor = this.threadActor;
if (isTopLevel) {
if (isTopLevel && threadActor.state != "detached") {
this.sources.reset({ sourceMaps: true });
threadActor.clearDebuggees();
if (threadActor.dbg) {
threadActor.dbg.enabled = true;
threadActor.maybePauseOnExceptions();
}
threadActor.dbg.enabled = true;
threadActor.maybePauseOnExceptions();
// Update the global no matter if the debugger is on or off,
// otherwise the global will be wrong when enabled later.
threadActor.global = window;
@ -1851,6 +1861,15 @@ BrowserAddonActor.prototype = {
return this._global;
},
get sources() {
if (!this._sources) {
dbg_assert(this.threadActor, "threadActor should exist when creating sources.");
this._sources = new TabSources(this._threadActor, this._allowSource);
}
return this._sources;
},
form: function BAA_form() {
dbg_assert(this.actorID, "addon should have an actorID.");
if (!this._consoleActor) {
@ -1931,6 +1950,7 @@ BrowserAddonActor.prototype = {
this._contextPool.removeActor(this._threadActor);
this._threadActor = null;
this._sources = null;
return { type: "detached" };
},
@ -2004,6 +2024,20 @@ BrowserAddonActor.prototype = {
return false;
},
/**
* Override the eligibility check for scripts and sources to make
* sure every script and source with a URL is stored when debugging
* add-ons.
*/
_allowSource: function(aSource) {
// XPIProvider.jsm evals some code in every add-on's bootstrap.js. Hide it.
if (aSource.url === "resource://gre/modules/addons/XPIProvider.jsm") {
return false;
}
return true;
},
/**
* Yield the current set of globals associated with this addon that should be
* added as debuggees.

View File

@ -81,6 +81,7 @@ EXTRA_JS_MODULES.devtools.server.actors.utils += [
'actors/utils/map-uri-to-addon-id.js',
'actors/utils/ScriptStore.js',
'actors/utils/stack.js',
'actors/utils/TabSources.js'
]
FAIL_ON_WARNINGS = True

View File

@ -122,7 +122,7 @@ addTest(function testMediaQuery() {
}).then(applied => {
is(applied[1].rule.type, 1, "Entry 1 is a rule style");
is(applied[1].rule.parentRule.type, 4, "Entry 1's parent rule is a media rule");
is(applied[1].rule.parentRule.media[0], "screen", "Entry 1's parent rule has the expected medium");
is(applied[1].rule.media[0], "screen", "Entry 1's rule has the expected medium");
}).then(runNextTest));
});

View File

@ -33,7 +33,7 @@ function test_simple_breakpoint()
}, function (aResponse, bpClient) {
gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
// Check the return value.
do_check_eq(aPacket.why.type, "breakpoint");
do_check_eq(aPacket.why.type, "breakpointConditionThrown");
do_check_eq(aPacket.frame.where.line, 3);
// Remove the breakpoint.

View File

@ -5,6 +5,7 @@ const { ActorPool, appendExtraActors, createExtraActors } = require("devtools/se
const { RootActor } = require("devtools/server/actors/root");
const { ThreadActor } = require("devtools/server/actors/script");
const { DebuggerServer } = require("devtools/server/main");
const { TabSources } = require("devtools/server/actors/utils/TabSources");
const promise = require("promise");
const makeDebugger = require("devtools/server/actors/utils/make-debugger");
@ -91,6 +92,13 @@ TestTabActor.prototype = {
return this._global.__name;
},
get sources() {
if (!this._sources) {
this._sources = new TabSources(this.threadActor);
}
return this._sources;
},
form: function() {
let response = { actor: this.actorID, title: this._global.__name };
@ -124,6 +132,7 @@ TestTabActor.prototype = {
},
onReload: function(aRequest) {
this.sources.reset({ sourceMaps: true });
this.threadActor.clearDebuggees();
this.threadActor.dbg.addDebuggees();
return {};

View File

@ -338,14 +338,7 @@ GMPWrapper.prototype = {
AddonManagerPrivate.callAddonListeners("onPropertyChanged", this,
["appDisabled"]);
if (this.appDisabled) {
AddonManagerPrivate.callAddonListeners("onUninstalling", this, false);
if (this._gmpPath) {
this._log.info("onPrefEMEGlobalEnabledChanged() - unregistering gmp " +
"directory " + this._gmpPath);
gmpService.removeAndDeletePluginDirectory(this._gmpPath);
}
GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_VERSION, this.id);
AddonManagerPrivate.callAddonListeners("onUninstalled", this);
this.uninstallPlugin();
} else {
AddonManagerPrivate.callInstallListeners("onExternalInstall", null, this,
null, false);
@ -405,6 +398,17 @@ GMPWrapper.prototype = {
AddonManagerPrivate.callAddonListeners("onInstalled", this);
},
uninstallPlugin: function() {
AddonManagerPrivate.callAddonListeners("onUninstalling", this, false);
if (this.gmpPath) {
this._log.info("uninstallPlugin() - unregistering gmp directory " +
this.gmpPath);
gmpService.removeAndDeletePluginDirectory(this.gmpPath);
}
GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_VERSION, this.id);
AddonManagerPrivate.callAddonListeners("onUninstalled", this);
},
shutdown: function() {
Preferences.ignore(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_ENABLED,
this._plugin.id),
@ -431,6 +435,7 @@ let GMPProvider = {
"GMPProvider.");
let telemetry = {};
this.buildPluginList();
this.ensureProperCDMInstallState();
Preferences.observe(GMPPrefs.KEY_LOG_BASE, configureLogging);
@ -563,6 +568,17 @@ let GMPProvider = {
this._plugins.set(plugin.id, plugin);
}
},
ensureProperCDMInstallState: function() {
if (!GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true)) {
for (let [id, plugin] of this._plugins) {
if (plugin.isEME && plugin.wrapper.isInstalled) {
gmpService.addPluginDirectory(plugin.wrapper.gmpPath);
plugin.wrapper.uninstallPlugin();
}
}
}
},
};
AddonManagerPrivate.registerProvider(GMPProvider, [