mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Merge fx-team to m-c a=merge CLOSED TREE
This commit is contained in:
commit
3ea14aaaa3
1
addon-sdk/source/app-extension/bootstrap.js
vendored
1
addon-sdk/source/app-extension/bootstrap.js
vendored
@ -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.
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
23
addon-sdk/source/gulpfile.js
Normal file
23
addon-sdk/source/gulpfile.js
Normal 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);
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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]
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
24
addon-sdk/source/test/tabs/utils.js
Normal file
24
addon-sdk/source/test/tabs/utils.js
Normal 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;
|
92
addon-sdk/source/test/test-event-dom.js
Normal file
92
addon-sdk/source/test/test-event-dom.js
Normal 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);
|
@ -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 });
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -14,6 +14,6 @@ function startMiddleClickTestCase(aTestNumber) {
|
||||
}
|
||||
|
||||
function test() {
|
||||
requestLongerTimeout(5); // slowwww shutdown on e10s
|
||||
requestLongerTimeout(10); // slowwww shutdown on e10s
|
||||
startReferrerTest(startMiddleClickTestCase);
|
||||
}
|
||||
|
@ -17,6 +17,6 @@ function startNewPrivateWindowTestCase(aTestNumber) {
|
||||
}
|
||||
|
||||
function test() {
|
||||
requestLongerTimeout(5); // slowwww shutdown on e10s
|
||||
requestLongerTimeout(10); // slowwww shutdown on e10s
|
||||
startReferrerTest(startNewPrivateWindowTestCase);
|
||||
}
|
||||
|
@ -16,6 +16,6 @@ function startNewTabTestCase(aTestNumber) {
|
||||
}
|
||||
|
||||
function test() {
|
||||
requestLongerTimeout(5); // slowwww shutdown on e10s
|
||||
requestLongerTimeout(10); // slowwww shutdown on e10s
|
||||
startReferrerTest(startNewTabTestCase);
|
||||
}
|
||||
|
@ -17,6 +17,6 @@ function startNewWindowTestCase(aTestNumber) {
|
||||
}
|
||||
|
||||
function test() {
|
||||
requestLongerTimeout(5); // slowwww shutdown on e10s
|
||||
requestLongerTimeout(10); // slowwww shutdown on e10s
|
||||
startReferrerTest(startNewWindowTestCase);
|
||||
}
|
||||
|
@ -13,6 +13,6 @@ function startSimpleClickTestCase(aTestNumber) {
|
||||
};
|
||||
|
||||
function test() {
|
||||
requestLongerTimeout(5); // slowwww shutdown on e10s
|
||||
requestLongerTimeout(10); // slowwww shutdown on e10s
|
||||
startReferrerTest(startSimpleClickTestCase);
|
||||
}
|
||||
|
@ -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"/>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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"/>
|
||||
|
@ -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]
|
||||
|
@ -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.")
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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.");
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
||||
});
|
||||
|
@ -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");
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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";
|
||||
|
BIN
toolkit/components/places/tests/migration/places_v28.sqlite
Normal file
BIN
toolkit/components/places/tests/migration/places_v28.sqlite
Normal file
Binary file not shown.
@ -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
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
@ -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]
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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));
|
||||
},
|
||||
};
|
||||
|
@ -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.
|
||||
|
@ -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(() => {}, () => {});
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -744,7 +744,7 @@ let StyleSheetActor = protocol.ActorClass({
|
||||
source: this.href,
|
||||
line: line,
|
||||
column: column
|
||||
}
|
||||
};
|
||||
});
|
||||
}, {
|
||||
request: {
|
||||
|
751
toolkit/devtools/server/actors/utils/TabSources.js
Normal file
751
toolkit/devtools/server/actors/utils/TabSources.js
Normal 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;
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
});
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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 {};
|
||||
|
@ -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, [
|
||||
|
Loading…
Reference in New Issue
Block a user