Merge mozilla-central to mozilla-inbound

This commit is contained in:
Carsten "Tomcat" Book 2016-02-03 12:27:02 +01:00
commit 968ab88a55
75 changed files with 1060 additions and 484 deletions

View File

@ -484,7 +484,7 @@ const CustomizableWidgets = [
// First sort and filter the list of tabs for each client. Note that the
// SyncedTabs module promises that the objects it returns are never
// shared, so we are free to mutate those objects directly.
const maxTabs = 15;
const maxTabs = 50;
for (let client of clients) {
let tabs = client.tabs;
tabs.sort((a, b) => b.lastUsed - a.lastUsed);

View File

@ -686,7 +686,9 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton {
.PanelUI-remotetabs-prefs-button {
-moz-appearance: none;
background-color: #0096dd;
color: white;
/* !important for the color as an OSX specific rule when a lightweight theme
is used for buttons in the toolbox overrides. See bug 1238531 for details */
color: white !important;
border-radius: 2px;
margin-top: 10px;
margin-bottom: 10px;

View File

@ -4,8 +4,6 @@
"use strict";
const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
add_task(function* capture() {
if (!shouldCapture()) {
return;

View File

@ -5,6 +5,7 @@
"use strict";
const {AddonWatcher} = Cu.import("resource://gre/modules/AddonWatcher.jsm", {});
const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
function setup() {
requestLongerTimeout(10);
@ -23,7 +24,8 @@ function shouldCapture() {
// Automation isn't able to schedule test jobs to only run on nightlies so we handle it here
// (see also: bug 1116275). Try pushes and local builds should also capture.
let capture = AppConstants.MOZ_UPDATE_CHANNEL == "nightly" ||
AppConstants.SOURCE_REVISION_URL.includes("/try/rev/") ||
(AppConstants.SOURCE_REVISION_URL.includes("/try/rev/") &&
env.get("MOZSCREENSHOTS_SETS")) ||
AppConstants.SOURCE_REVISION_URL == "";
if (!capture) {
ok(true, "Capturing is disabled for this MOZ_UPDATE_CHANNEL or REPO");

View File

@ -15,6 +15,7 @@ const DEFAULT_FAVICON_TAB = `data:text/html,<meta charset="utf-8">
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
this.Tabs = {
init(libDir) {},
@ -25,6 +26,9 @@ this.Tabs = {
fiveTabsHelper();
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
hoverTab(browserWindow.gBrowser.tabs[3]);
yield new Promise((resolve, reject) => {
setTimeout(resolve, 3000);
});
}),
},
@ -48,6 +52,9 @@ this.Tabs = {
hoverTab(newTabButton);
browserWindow.gBrowser.tabs[browserWindow.gBrowser.tabs.length - 1].
setAttribute("beforehovered", true);
yield new Promise((resolve, reject) => {
setTimeout(resolve, 3000);
});
}),
},
@ -92,6 +99,9 @@ this.Tabs = {
browserWindow.gBrowser.pinTab(tab);
browserWindow.gBrowser.selectTabAtIndex(4);
hoverTab(browserWindow.gBrowser.tabs[6]);
yield new Promise((resolve, reject) => {
setTimeout(resolve, 3000);
});
}),
},
},

View File

@ -30,16 +30,19 @@ this.WindowSize = {
setTimeout(function waitToLeaveFS() {
browserWindow.maximize();
resolve();
}, Services.appinfo.OS == "Darwin" ? 1500 : 0);
}, 5000);
});
}),
},
normal: {
applyConfig: Task.async(() => {
applyConfig: Task.async(function*() {
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
browserWindow.fullScreen = false;
browserWindow.restore();
yield new Promise((resolve, reject) => {
setTimeout(resolve, 5000);
});
}),
},
@ -49,9 +52,7 @@ this.WindowSize = {
browserWindow.fullScreen = true;
// OS X Lion fullscreen transition takes a while
yield new Promise((resolve, reject) => {
setTimeout(function waitAfterEnteringFS() {
resolve();
}, Services.appinfo.OS == "Darwin" ? 1500 : 0);
setTimeout(resolve, 5000);
});
}),
},

View File

@ -33,6 +33,9 @@ const Strings = Services.strings.createBundle(
var AboutDebugging = {
_prefListeners: [],
// Pointer to the current React component.
_component: null,
_categories: null,
get categories() {
// If needed, initialize the list of available categories.
@ -66,11 +69,11 @@ var AboutDebugging = {
location.hash = "#" + category;
if (category == "addons") {
React.render(React.createElement(AddonsComponent, { client: this.client }),
document.querySelector("#addons"));
this._component = React.render(React.createElement(AddonsComponent,
{client: this.client}), document.querySelector("#addons"));
} else if (category == "workers") {
React.render(React.createElement(WorkersComponent, { client: this.client }),
document.querySelector("#workers"));
this._component = React.render(React.createElement(WorkersComponent,
{client: this.client}), document.querySelector("#workers"));
}
},
@ -82,16 +85,22 @@ var AboutDebugging = {
let elements = document.querySelectorAll("input[type=checkbox][data-pref]");
Array.map(elements, element => {
let pref = element.dataset.pref;
let updatePref = () => {
Services.prefs.setBoolPref(pref, element.checked);
};
element.addEventListener("change", updatePref, false);
let updateCheckbox = () => {
let onPreferenceChanged = () => {
element.checked = Services.prefs.getBoolPref(pref);
this.update();
};
Services.prefs.addObserver(pref, updateCheckbox, false);
this._prefListeners.push([pref, updateCheckbox]);
updateCheckbox();
Services.prefs.addObserver(pref, onPreferenceChanged, false);
this._prefListeners.push([pref, onPreferenceChanged]);
// Initialize the current checkbox element.
element.checked = Services.prefs.getBoolPref(pref);
});
// Link buttons to their associated actions.
@ -112,6 +121,12 @@ var AboutDebugging = {
});
},
update() {
if (this._component) {
this._component.setState({});
}
},
loadAddonFromFile() {
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(window,

View File

@ -41,8 +41,10 @@ exports.AddonsComponent = React.createClass({
let client = this.props.client;
let targets = this.state.extensions;
let name = Strings.GetStringFromName("extensions");
let debugDisabled = !Services.prefs.getBoolPref("devtools.chrome.enabled");
return React.createElement("div", null,
React.createElement(TargetListComponent, { name, targets, client })
React.createElement(TargetListComponent,
{ name, targets, client, debugDisabled })
);
},

View File

@ -23,8 +23,10 @@ exports.TargetListComponent = React.createClass({
render() {
let client = this.props.client;
let debugDisabled = this.props.debugDisabled;
let targets = this.props.targets.sort(LocaleCompare).map(target => {
return React.createElement(TargetComponent, { client, target });
return React.createElement(TargetComponent,
{ client, target, debugDisabled });
});
return (
React.createElement("div", { id: this.props.id, className: "targets" },

View File

@ -54,6 +54,8 @@ exports.TargetComponent = React.createClass({
render() {
let target = this.props.target;
let debugDisabled = this.props.debugDisabled;
return React.createElement("div", { className: "target" },
React.createElement("img", {
className: "target-icon",
@ -62,8 +64,11 @@ exports.TargetComponent = React.createClass({
React.createElement("div", { className: "target-name" }, target.name),
React.createElement("div", { className: "target-url" }, target.url)
),
React.createElement("button", { onClick: this.debug },
Strings.GetStringFromName("debug"))
React.createElement("button", {
className: "debug-button",
onClick: this.debug,
disabled: debugDisabled,
}, Strings.GetStringFromName("debug"))
);
},
});

View File

@ -9,5 +9,6 @@ support-files =
service-workers/empty-sw.js
[browser_addons_install.js]
[browser_addons_toggle_debug.js]
[browser_service_workers.js]
[browser_service_workers_timeout.js]

View File

@ -2,33 +2,13 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {AddonManager} = Cu.import("resource://gre/modules/AddonManager.jsm", {});
const ADDON_ID = "test-devtools@mozilla.org";
const ADDON_NAME = "test-devtools";
add_task(function *() {
let { tab, document } = yield openAboutDebugging("addons");
// Mock the file picker to select a test addon
let MockFilePicker = SpecialPowers.MockFilePicker;
MockFilePicker.init(null);
let file = get_supports_file("addons/unpacked/install.rdf");
MockFilePicker.returnFiles = [file.file];
// Wait for a message sent by the addon's bootstrap.js file
let promise = new Promise(done => {
Services.obs.addObserver(function listener() {
Services.obs.removeObserver(listener, "test-devtools", false);
ok(true, "Addon installed and running its bootstrap.js file");
done();
}, "test-devtools", false);
});
// Trigger the file picker by clicking on the button
document.getElementById("load-addon-from-file").click();
// Wait for the addon execution
yield promise;
yield installAddon(document, "addons/unpacked/install.rdf", "test-devtools");
// Check that the addon appears in the UI
let names = [...document.querySelectorAll("#addons .target-name")];
@ -36,21 +16,7 @@ add_task(function *() {
ok(names.includes(ADDON_NAME), "The addon name appears in the list of addons: " + names);
// Now uninstall this addon
yield new Promise(done => {
AddonManager.getAddonByID(ADDON_ID, addon => {
let listener = {
onUninstalled: function(aUninstalledAddon) {
if (aUninstalledAddon != addon) {
return;
}
AddonManager.removeAddonListener(listener);
done();
}
};
AddonManager.addAddonListener(listener);
addon.uninstall();
});
});
yield uninstallAddon(ADDON_ID);
// Ensure that the UI removes the addon from the list
names = [...document.querySelectorAll("#addons .target-name")];

View File

@ -0,0 +1,58 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that individual Debug buttons are disabled when "Addons debugging"
// is disabled.
// Test that the buttons are updated dynamically if the preference changes.
const ADDON_ID = "test-devtools@mozilla.org";
add_task(function* () {
info("Turn off addon debugging.");
yield new Promise(resolve => {
let options = {"set": [
["devtools.chrome.enabled", false],
]};
SpecialPowers.pushPrefEnv(options, resolve);
});
let { tab, document } = yield openAboutDebugging("addons");
info("Install a test addon.");
yield installAddon(document, "addons/unpacked/install.rdf", "test-devtools");
let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
ok(!addonDebugCheckbox.checked, "Addons debugging should be disabled.");
info("Check all debug buttons are disabled.");
let debugButtons = [...document.querySelectorAll("#addons .debug-button")];
ok(debugButtons.every(b => b.disabled), "Debug buttons should be disabled");
info("Click on 'Enable addons debugging' checkbox.");
let addonsContainer = document.getElementById("addons");
let onAddonsMutation = waitForMutation(addonsContainer,
{ subtree: true, attributes: true });
addonDebugCheckbox.click();
yield onAddonsMutation;
info("Check all debug buttons are enabled.");
ok(addonDebugCheckbox.checked, "Addons debugging should be enabled.");
debugButtons = [...document.querySelectorAll("#addons .debug-button")];
ok(debugButtons.every(b => !b.disabled), "Debug buttons should be enabled");
info("Click again on 'Enable addons debugging' checkbox.");
onAddonsMutation = waitForMutation(addonsContainer,
{ subtree: true, attributes: true });
addonDebugCheckbox.click();
yield onAddonsMutation;
info("Check all debug buttons are disabled again.");
debugButtons = [...document.querySelectorAll("#addons .debug-button")];
ok(debugButtons.every(b => b.disabled), "Debug buttons should be disabled");
info("Uninstall addon installed earlier.");
yield uninstallAddon(ADDON_ID);
yield closeAboutDebugging(tab);
});

View File

@ -9,17 +9,6 @@ const HTTP_ROOT = CHROME_ROOT.replace("chrome://mochitests/content/",
const SERVICE_WORKER = HTTP_ROOT + "service-workers/empty-sw.js";
const TAB_URL = HTTP_ROOT + "service-workers/empty-sw.html";
function waitForWorkersUpdate(document) {
return new Promise(done => {
var observer = new MutationObserver(function(mutations) {
observer.disconnect();
done();
});
var target = document.getElementById("service-workers");
observer.observe(target, { childList: true });
});
}
add_task(function *() {
yield new Promise(done => {
let options = {"set": [
@ -32,7 +21,8 @@ add_task(function *() {
let swTab = yield addTab(TAB_URL);
yield waitForWorkersUpdate(document);
let serviceWorkersElement = document.getElementById("service-workers");
yield waitForMutation(serviceWorkersElement, { childList: true });
// Check that the service worker appears in the UI
let names = [...document.querySelectorAll("#service-workers .target-name")];
@ -40,7 +30,8 @@ add_task(function *() {
ok(names.includes(SERVICE_WORKER), "The service worker url appears in the list: " + names);
// Finally, unregister the service worker itself
let aboutDebuggingUpdate = waitForWorkersUpdate(document);
let aboutDebuggingUpdate = waitForMutation(serviceWorkersElement,
{ childList: true });
// Use message manager to work with e10s
let frameScript = function () {

View File

@ -11,17 +11,6 @@ const TAB_URL = HTTP_ROOT + "service-workers/empty-sw.html";
const SW_TIMEOUT = 1000;
function waitForWorkersUpdate(document) {
return new Promise(done => {
var observer = new MutationObserver(function(mutations) {
observer.disconnect();
done();
});
var target = document.getElementById("service-workers");
observer.observe(target, { childList: true });
});
}
function assertHasWorker(expected, document, type, name) {
let names = [...document.querySelectorAll("#" + type + " .target-name")];
names = names.map(element => element.textContent);
@ -45,7 +34,8 @@ add_task(function *() {
let swTab = yield addTab(TAB_URL);
yield waitForWorkersUpdate(document);
let serviceWorkersElement = document.getElementById("service-workers");
yield waitForMutation(serviceWorkersElement, { childList: true });
assertHasWorker(true, document, "service-workers", SERVICE_WORKER);
@ -100,7 +90,7 @@ add_task(function *() {
// Now ensure that the worker is correctly destroyed
// after we destroy the toolbox.
// The list should update once it get destroyed.
yield waitForWorkersUpdate(document);
yield waitForMutation(serviceWorkersElement, { childList: true });
assertHasWorker(false, document, "service-workers", SERVICE_WORKER);

View File

@ -6,6 +6,7 @@
var {utils: Cu, classes: Cc, interfaces: Ci} = Components;
const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
const {AddonManager} = Cu.import("resource://gre/modules/AddonManager.jsm", {});
const Services = require("Services");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
DevToolsUtils.testing = true;
@ -27,7 +28,6 @@ function openAboutDebugging(page) {
return {
tab,
document: browser.contentDocument,
window: browser.contentWindow
};
});
}
@ -80,3 +80,61 @@ function get_supports_file(path) {
let fileurl = cr.convertChromeURL(Services.io.newURI(CHROME_ROOT + path, null, null));
return fileurl.QueryInterface(Ci.nsIFileURL);
}
function installAddon(document, path, evt) {
// Mock the file picker to select a test addon
let MockFilePicker = SpecialPowers.MockFilePicker;
MockFilePicker.init(null);
let file = get_supports_file(path);
MockFilePicker.returnFiles = [file.file];
// Wait for a message sent by the addon's bootstrap.js file
let onAddonInstalled = new Promise(done => {
Services.obs.addObserver(function listener() {
Services.obs.removeObserver(listener, evt, false);
ok(true, "Addon installed and running its bootstrap.js file");
done();
}, evt, false);
});
// Trigger the file picker by clicking on the button
document.getElementById("load-addon-from-file").click();
// Wait for the addon execution
return onAddonInstalled;
}
function uninstallAddon(addonId) {
// Now uninstall this addon
return new Promise(done => {
AddonManager.getAddonByID(addonId, addon => {
let listener = {
onUninstalled: function(uninstalledAddon) {
if (uninstalledAddon != addon) {
return;
}
AddonManager.removeAddonListener(listener);
done();
}
};
AddonManager.addAddonListener(listener);
addon.uninstall();
});
});
}
/**
* Returns a promise that will resolve after receiving a mutation matching the
* provided mutation options on the provided target.
* @param {Node} target
* @param {Object} mutationOptions
* @return {Promise}
*/
function waitForMutation(target, mutationOptions) {
return new Promise(resolve => {
let observer = new MutationObserver(() => {
observer.disconnect();
resolve();
});
observer.observe(target, mutationOptions);
});
}

View File

@ -8,7 +8,6 @@ support-files =
mockCommands.js
[browser_cmd_addon.js]
skip-if = buildapp == 'mulet'
[browser_cmd_calllog.js]
skip-if = true || e10s # Bug 845831
[browser_cmd_calllog_chrome.js]

View File

@ -533,7 +533,7 @@ skip-if = e10s && debug
[browser_dbg_variables-view-frame-with.js]
skip-if = e10s && debug
[browser_dbg_variables-view-frozen-sealed-nonext.js]
skip-if = e10s && debug || buildapp == 'mulet'
skip-if = e10s && debug
[browser_dbg_variables-view-hide-non-enums.js]
skip-if = e10s && debug
[browser_dbg_variables-view-large-array-buffer.js]

View File

@ -14,7 +14,6 @@ var gFirstSourceLabel = "code_ugly-5.js";
var gSecondSourceLabel = "code_ugly-6.js";
var gOriginalPref = Services.prefs.getBoolPref("devtools.debugger.auto-pretty-print");
Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", true);
function test(){
initDebugger(TAB_URL).then(([aTab,, aPanel]) => {
@ -27,28 +26,31 @@ function test(){
gOptions = gDebugger.DebuggerView.Options;
gView = gDebugger.DebuggerView;
// Should be on by default.
Task.spawn(function*() {
yield waitForSourceShown(gPanel, gFirstSourceLabel);
testSourceIsUgly();
enableAutoPrettyPrint();
testAutoPrettyPrintOn();
waitForSourceShown(gPanel, gFirstSourceLabel)
.then(testSourceIsUgly)
.then(() => waitForSourceShown(gPanel, gFirstSourceLabel))
.then(testSourceIsPretty)
.then(disableAutoPrettyPrint)
.then(testAutoPrettyPrintOff)
.then(() => {
reload(gPanel);
yield waitForSourceShown(gPanel, gFirstSourceLabel);
testSourceIsUgly();
yield waitForSourceShown(gPanel, gFirstSourceLabel);
testSourceIsPretty();
disableAutoPrettyPrint();
testAutoPrettyPrintOff();
let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
gSources.selectedIndex = 1;
return finished;
})
.then(testSecondSourceLabel)
.then(testSourceIsUgly)
// Re-enable auto pretty printing for browser_dbg_auto-pretty-print-02.js
.then(enableAutoPrettyPrint)
.then(() => closeDebuggerAndFinish(gPanel))
.then(null, aError => {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError));
})
yield finished;
testSecondSourceLabel();
testSourceIsUgly();
enableAutoPrettyPrint();
yield closeDebuggerAndFinish(gPanel);
});
});
}

View File

@ -41,7 +41,7 @@ function test() {
const textbox = gDebugger.document.getElementById("conditional-breakpoint-panel-textbox");
is(textbox.value, "hello", "The expression is correct (2).")
resumeDebuggerThenCloseAndFinish(gPanel);
yield resumeDebuggerThenCloseAndFinish(gPanel);
});
callInTab(gTab, "ermahgerd");

View File

@ -10,5 +10,5 @@
// etc...
// etc...
function foo(){var a=1;var b=2;bar(a,b);}
function bar(c,d){debugger;}
function bar(c,d){return 3;}
foo();

View File

@ -588,9 +588,9 @@ AddonDebugger.prototype = {
info("Addon debugger panel shown successfully.");
this.debuggerPanel = toolbox.getCurrentPanel();
yield waitForSourceShown(this.debuggerPanel, '');
// Wait for the initial resume...
yield prepareDebugger(this.debuggerPanel);
prepareDebugger(this.debuggerPanel);
yield this._attachConsole();
}),

View File

@ -116,7 +116,7 @@ var SourceUtils = {
clearCache: function() {
this._labelsCache.clear();
this._groupsCache.clear();
this._minifiedCache = new WeakMap();
this._minifiedCache.clear();
},
/**

View File

@ -29,7 +29,7 @@ support-files =
[browser_target_remote.js]
[browser_target_support.js]
[browser_two_tabs.js]
skip-if = e10s && debug && os == 'win'
skip-if = e10s && debug && os == 'win' # Bug 1231869
[browser_toolbox_dynamic_registration.js]
[browser_toolbox_getpanelwhenready.js]
[browser_toolbox_highlight.js]
@ -40,11 +40,8 @@ skip-if = true # Bug 1177463 - Temporarily hide the minimize button
[browser_toolbox_options.js]
[browser_toolbox_options_disable_buttons.js]
[browser_toolbox_options_disable_cache-01.js]
skip-if = e10s # Bug 1030318
[browser_toolbox_options_disable_cache-02.js]
skip-if = e10s # Bug 1030318
[browser_toolbox_options_disable_js.js]
skip-if = e10s # Bug 1030318
# [browser_toolbox_raise.js] # Bug 962258
# skip-if = os == "win"
[browser_toolbox_ready.js]

View File

@ -57,29 +57,21 @@ function testJSEnabledIframe(secondPass) {
}
}
function toggleJS() {
let deferred = promise.defer();
let toggleJS = Task.async(function*() {
let panel = toolbox.getCurrentPanel();
let cbx = panel.panelDoc.getElementById("devtools-disable-javascript");
cbx.scrollIntoView();
if (cbx.checked) {
info("Clearing checkbox to re-enable JS");
} else {
info("Checking checkbox to disable JS");
}
gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
doc = content.document;
deferred.resolve();
}, true);
let browserLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
cbx.click();
return deferred.promise;
}
yield browserLoaded;
doc = content.document;
});
function testJSDisabled() {
info("Testing that JS is disabled");

View File

@ -48,15 +48,19 @@ function* checkCacheEnabled(tabX, expected) {
yield reloadTab(tabX);
let oldGuid = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
let doc = content.document;
let h1 = doc.querySelector("h1");
let oldGuid = h1.textContent;
return h1.textContent;
});
yield reloadTab(tabX);
doc = content.document;
h1 = doc.querySelector("h1");
let guid = h1.textContent;
let guid = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
let doc = content.document;
let h1 = doc.querySelector("h1");
return h1.textContent;
});
if (expected) {
is(guid, oldGuid, tabX.title + " cache is enabled");
@ -71,14 +75,9 @@ function* setDisableCacheCheckboxChecked(tabX, state) {
let panel = tabX.toolbox.getCurrentPanel();
let cbx = panel.panelDoc.getElementById("devtools-disable-cache");
cbx.scrollIntoView();
// After uising scrollIntoView() we need to wait for the browser to scroll.
yield waitForTick();
if (cbx.checked !== state) {
info("Setting disable cache checkbox to " + state + " for " + tabX.title);
EventUtils.synthesizeMouseAtCenter(cbx, {}, panel.panelWin);
cbx.click();
// We need to wait for all checkboxes to be updated and the docshells to
// apply the new cache settings.

View File

@ -962,12 +962,13 @@ CssRuleView.prototype = {
}
this._clearRules();
this._createEditors();
let onEditorsReady = this._createEditors();
this.refreshPseudoClassPanel();
// Notify anyone that cares that we refreshed.
return onEditorsReady.then(() => {
this.emit("ruleview-refreshed");
}, e => console.error(e));
}).then(null, promiseWarn);
},
@ -1147,9 +1148,10 @@ CssRuleView.prototype = {
let container = null;
if (!this._elementStyle.rules) {
return;
return promise.resolve();
}
let editorReadyPromises = [];
for (let rule of this._elementStyle.rules) {
if (rule.domRule.system) {
continue;
@ -1158,6 +1160,7 @@ CssRuleView.prototype = {
// Initialize rule editor
if (!rule.editor) {
rule.editor = new RuleEditor(this, rule);
editorReadyPromises.push(rule.editor.once("source-link-updated"));
}
// Filter the rules and highlight any matches if there is a search input
@ -1211,6 +1214,8 @@ CssRuleView.prototype = {
} else {
this.searchField.classList.remove("devtools-style-searchbox-no-match");
}
return promise.all(editorReadyPromises);
},
/**

View File

@ -110,7 +110,7 @@ skip-if = e10s # Bug 1039528: "inspect element" contextual-menu doesn't work wit
[browser_rules_filtereditor-appears-on-swatch-click.js]
[browser_rules_filtereditor-commit-on-ENTER.js]
[browser_rules_filtereditor-revert-on-ESC.js]
skip-if = (os == "win" && debug) || e10s # bug 963492: win. bug 1040653: e10s.
skip-if = (os == "win" && debug) # bug 963492: win.
[browser_rules_guessIndentation.js]
[browser_rules_inherited-properties_01.js]
[browser_rules_inherited-properties_02.js]
@ -139,7 +139,6 @@ skip-if = (os == "win" && debug) || e10s # bug 963492: win. bug 1040653: e10s.
[browser_rules_original-source-link.js]
[browser_rules_pseudo-element_01.js]
[browser_rules_pseudo-element_02.js]
skip-if = e10s # Bug 1090340
[browser_rules_pseudo_lock_options.js]
[browser_rules_refresh-no-flicker.js]
[browser_rules_refresh-on-attribute-change_01.js]
@ -173,7 +172,6 @@ skip-if = e10s # Bug 1090340
[browser_rules_strict-search-filter_02.js]
[browser_rules_strict-search-filter_03.js]
[browser_rules_style-editor-link.js]
skip-if = e10s # bug 1040670 Cannot open inline styles in viewSourceUtils
[browser_rules_urls-clickable.js]
[browser_rules_user-agent-styles.js]
[browser_rules_user-agent-styles-uneditable.js]

View File

@ -19,23 +19,15 @@ function* testPressingEscapeRevertsChanges(view) {
let ruleEditor = getRuleViewRuleEditor(view, 1);
let propEditor = ruleEditor.rule.textProps[0].editor;
let swatch = propEditor.valueSpan.querySelector(".ruleview-filterswatch");
let filterTooltip = view.tooltips.filterEditor;
let onShow = filterTooltip.tooltip.once("shown");
swatch.click();
yield onShow;
let widget = yield filterTooltip.widget;
widget.setCssValue("blur(2px)");
yield ruleEditor.rule._applyingModifications;
yield clickOnFilterSwatch(swatch, view);
yield setValueInFilterWidget("blur(2px)", view);
yield waitForComputedStyleProperty("body", null, "filter", "blur(2px)");
is(propEditor.valueSpan.textContent, "blur(2px)",
"Got expected property value.");
info("Pressing ESCAPE to close the tooltip");
EventUtils.sendKey("ESCAPE", widget.styleWindow);
yield ruleEditor.rule._applyingModifications;
yield pressEscapeToCloseTooltip(view);
yield waitForComputedStyleProperty("body", null, "filter",
"blur(2px) contrast(2)");
@ -47,11 +39,11 @@ function* testPressingEscapeRevertsChangesAndDisables(view) {
let ruleEditor = getRuleViewRuleEditor(view, 1);
let propEditor = ruleEditor.rule.textProps[0].editor;
let swatch = propEditor.valueSpan.querySelector(".ruleview-filterswatch");
let filterTooltip = view.tooltips.filterEditor;
info("Disabling filter property");
let onRuleViewChanged = view.once("ruleview-changed");
propEditor.enable.click();
yield ruleEditor.rule._applyingModifications;
yield onRuleViewChanged;
ok(propEditor.element.classList.contains("ruleview-overridden"),
"property is overridden.");
@ -64,22 +56,15 @@ function* testPressingEscapeRevertsChangesAndDisables(view) {
let newValue = yield getRulePropertyValue("filter");
is(newValue, "", "filter should have been unset.");
let onShow = filterTooltip.tooltip.once("shown");
swatch.click();
yield onShow;
yield clickOnFilterSwatch(swatch, view);
ok(!propEditor.element.classList.contains("ruleview-overridden"),
"property overridden is not displayed.");
is(propEditor.enable.style.visibility, "hidden",
"property enable checkbox is hidden.");
let widget = yield filterTooltip.widget;
widget.setCssValue("blur(2px)");
yield ruleEditor.rule._applyingModifications;
info("Pressing ESCAPE to close the tooltip");
EventUtils.sendKey("ESCAPE", widget.styleWindow);
yield ruleEditor.rule._applyingModifications;
yield setValueInFilterWidget("blur(2px)", view);
yield pressEscapeToCloseTooltip(view);
ok(propEditor.element.classList.contains("ruleview-overridden"),
"property is overridden.");
@ -102,3 +87,34 @@ function* getRulePropertyValue(name) {
});
return propValue;
}
function* clickOnFilterSwatch(swatch, view) {
info("Clicking on a css filter swatch to open the tooltip");
// Clicking on a cssfilter swatch sets the current filter value in the tooltip
// which, in turn, makes the FilterWidget emit an "updated" event that causes
// the rule-view to refresh. So we must wait for the ruleview-changed event.
let onRuleViewChanged = view.once("ruleview-changed");
swatch.click();
yield onRuleViewChanged;
}
function* setValueInFilterWidget(value, view) {
info("Setting the CSS filter value in the tooltip");
let filterTooltip = view.tooltips.filterEditor;
let widget = yield filterTooltip.widget;
let onRuleViewChanged = view.once("ruleview-changed");
widget.setCssValue(value);
yield onRuleViewChanged;
}
function* pressEscapeToCloseTooltip(view) {
info("Pressing ESCAPE to close the tooltip");
let filterTooltip = view.tooltips.filterEditor;
let widget = yield filterTooltip.widget;
let onRuleViewChanged = view.once("ruleview-changed");
EventUtils.sendKey("ESCAPE", widget.styleWindow);
yield onRuleViewChanged;
}

View File

@ -11,11 +11,8 @@ const TEST_URI = URL_ROOT + "doc_pseudoelement.html";
add_task(function*() {
yield addTab(TEST_URI);
let {inspector} = yield openRuleView();
yield testTopLeft(inspector);
});
function* testTopLeft(inspector) {
let node = inspector.markup.walker.frontForRawNode(getNode("#topleft"));
let node = yield getNodeFront("#topleft", inspector);
let children = yield inspector.markup.walker.children(node);
is(children.nodes.length, 3, "Element has correct number of children");
@ -29,5 +26,4 @@ function* testTopLeft(inspector) {
is(afterElement.tagName, "_moz_generated_content_after",
"tag name is correct");
yield selectNode(afterElement, inspector);
}
});

View File

@ -25,6 +25,8 @@ const {
SELECTOR_ELEMENT,
SELECTOR_PSEUDO_CLASS
} = require("devtools/client/shared/css-parsing-utils");
const promise = require("promise");
const EventEmitter = require("devtools/shared/event-emitter");
XPCOMUtils.defineLazyGetter(this, "_strings", function() {
return Services.strings.createBundle(
@ -40,12 +42,20 @@ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
* for its TextProperties.
* Manages creation of new text properties.
*
* One step of a RuleEditor's instantiation is figuring out what's the original
* source link to the parent stylesheet (in case of source maps). This step is
* asynchronous and is triggered as soon as the RuleEditor is instantiated (see
* updateSourceLink). If you need to know when the RuleEditor is done with this,
* you need to listen to the source-link-updated event.
*
* @param {CssRuleView} ruleView
* The CssRuleView containg the document holding this rule editor.
* @param {Rule} rule
* The Rule object we're editing.
*/
function RuleEditor(ruleView, rule) {
EventEmitter.decorate(this);
this.ruleView = ruleView;
this.doc = this.ruleView.styleDocument;
this.rule = rule;
@ -235,10 +245,21 @@ RuleEditor.prototype = {
let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
if (showOrig && !this.rule.isSystem &&
this.rule.domRule.type !== ELEMENT_STYLE) {
// Only get the original source link if the right pref is set, if the rule
// isn't a system rule and if it isn't an inline rule.
this.rule.getOriginalSourceStrings().then((strings) => {
sourceLabel.setAttribute("value", strings.short);
sourceLabel.setAttribute("tooltiptext", strings.full);
}, console.error);
}, e => console.error(e)).then(() => {
this.emit("source-link-updated");
});
} else {
// If we're not getting the original source link, then we can emit the
// event immediately (but still asynchronously to give consumers a chance
// to register it after having instantiated the RuleEditor).
promise.resolve().then(() => {
this.emit("source-link-updated");
});
}
},

View File

@ -54,6 +54,7 @@ tree.labels.cookies=Cookies
tree.labels.localStorage=Local Storage
tree.labels.sessionStorage=Session Storage
tree.labels.indexedDB=Indexed DB
tree.labels.Cache=Cache Storage
# LOCALIZATION NOTE (table.headers.*.*):
# These strings are the header names of the columns in the Storage Table for
@ -84,6 +85,9 @@ table.headers.localStorage.value=Value
table.headers.sessionStorage.name=Key
table.headers.sessionStorage.value=Value
table.headers.Cache.url=URL
table.headers.Cache.status=Status
table.headers.indexedDB.name=Key
table.headers.indexedDB.db=Database Name
table.headers.indexedDB.objectStore=Object Store Name

View File

@ -49,7 +49,6 @@ support-files =
[browser_net_charts-01.js]
[browser_net_charts-02.js]
[browser_net_charts-03.js]
skip-if= buildapp == 'mulet'
[browser_net_charts-04.js]
[browser_net_charts-05.js]
[browser_net_charts-06.js]

View File

@ -7,7 +7,6 @@ support-files =
helper_edits.js
[browser_projecteditor_app_options.js]
skip-if = buildapp == 'mulet'
[browser_projecteditor_confirm_unsaved.js]
[browser_projecteditor_contextmenu_01.js]
[browser_projecteditor_contextmenu_02.js]
@ -15,9 +14,7 @@ skip-if = true # Bug 1173950
[browser_projecteditor_delete_file.js]
skip-if = e10s # Frequent failures in e10s - Bug 1020027
[browser_projecteditor_rename_file.js]
skip-if = buildapp == 'mulet'
[browser_projecteditor_editing_01.js]
skip-if = buildapp == 'mulet'
[browser_projecteditor_editors_image.js]
[browser_projecteditor_external_change.js]
[browser_projecteditor_immediate_destroy.js]
@ -27,7 +24,6 @@ skip-if = buildapp == 'mulet'
skip-if = true # Bug 1173950
[browser_projecteditor_new_file.js]
[browser_projecteditor_saveall.js]
skip-if = buildapp == 'mulet'
[browser_projecteditor_stores.js]
[browser_projecteditor_tree_selection_01.js]
[browser_projecteditor_tree_selection_02.js]

View File

@ -8,7 +8,6 @@ support-files = head.js
[browser_scratchpad_reset_undo.js]
[browser_scratchpad_display_outputs_errors.js]
[browser_scratchpad_eval_func.js]
skip-if = buildapp == 'mulet'
[browser_scratchpad_goto_line_ui.js]
[browser_scratchpad_reload_and_run.js]
[browser_scratchpad_display_non_error_exceptions.js]

View File

@ -129,7 +129,6 @@ skip-if = e10s # Test intermittently fails with e10s. Bug 1124162.
[browser_tableWidget_basic.js]
[browser_tableWidget_keyboard_interaction.js]
[browser_tableWidget_mouse_interaction.js]
skip-if = buildapp == 'mulet'
[browser_telemetry_button_eyedropper.js]
[browser_telemetry_button_paintflashing.js]
skip-if = e10s # Bug 937167 - e10s paintflashing
@ -156,7 +155,7 @@ skip-if = e10s # Bug 1086492 - Disable tilt for e10s
[browser_toolbar_basic.js]
[browser_toolbar_tooltip.js]
[browser_toolbar_webconsole_errors_count.js]
skip-if = buildapp == 'mulet' || e10s # The developertoolbar error count isn't correct with e10s
skip-if = e10s # The developertoolbar error count isn't correct with e10s
[browser_treeWidget_basic.js]
[browser_treeWidget_keyboard_interaction.js]
[browser_treeWidget_mouse_interaction.js]

View File

@ -57,7 +57,10 @@ StoragePanel.prototype = {
this.emit("ready");
return this;
}).catch(this.destroy);
}).catch(e => {
console.log("error while opening storage panel", e);
this.destroy();
});
},
/**

View File

@ -61,6 +61,9 @@ const testCases = [
[6, 7]],
[["indexedDB", "https://sectest1.example.org", "idb-s2", "obj-s2"],
[16]],
[["Cache", "http://test1.example.org", "plop"],
[MAIN_DOMAIN + "404_cached_file.js", MAIN_DOMAIN + "browser_storage_basic.js"]],
];
/**

View File

@ -14,6 +14,8 @@ const SPLIT_CONSOLE_PREF = "devtools.toolbox.splitconsoleEnabled";
const STORAGE_PREF = "devtools.storage.enabled";
const DUMPEMIT_PREF = "devtools.dump.emit";
const DEBUGGERLOG_PREF = "devtools.debugger.log";
// Allows Cache API to be working on usage `http` test page
const CACHES_ON_HTTP_PREF = "dom.caches.testing.enabled";
const PATH = "browser/devtools/client/storage/test/";
const MAIN_DOMAIN = "http://test1.example.org/" + PATH;
const ALT_DOMAIN = "http://sectest1.example.org/" + PATH;
@ -27,6 +29,7 @@ var gToolbox, gPanelWindow, gWindow, gUI;
// Services.prefs.setBoolPref(DEBUGGERLOG_PREF, true);
Services.prefs.setBoolPref(STORAGE_PREF, true);
Services.prefs.setBoolPref(CACHES_ON_HTTP_PREF, true);
DevToolsUtils.testing = true;
registerCleanupFunction(() => {
gToolbox = gPanelWindow = gWindow = gUI = null;
@ -34,6 +37,7 @@ registerCleanupFunction(() => {
Services.prefs.clearUserPref(SPLIT_CONSOLE_PREF);
Services.prefs.clearUserPref(DUMPEMIT_PREF);
Services.prefs.clearUserPref(DEBUGGERLOG_PREF);
Services.prefs.clearUserPref(CACHES_ON_HTTP_PREF);
DevToolsUtils.testing = false;
while (gBrowser.tabs.length > 1) {
gBrowser.removeCurrentTab();
@ -82,35 +86,20 @@ function addTab(url) {
* @return {Promise} A promise that resolves after storage inspector is ready
*/
function* openTabAndSetupStorage(url) {
/**
* This method iterates over iframes in a window and setups the indexed db
* required for this test.
*/
let setupIDBInFrames = (w, i, c) => {
if (w[i] && w[i].idbGenerator) {
w[i].setupIDB = w[i].idbGenerator(() => setupIDBInFrames(w, i + 1, c));
w[i].setupIDB.next();
} else if (w[i] && w[i + 1]) {
setupIDBInFrames(w, i + 1, c);
} else {
c();
}
};
let content = yield addTab(url);
let def = promise.defer();
// Setup the indexed db in main window.
gWindow = content.wrappedJSObject;
if (gWindow.idbGenerator) {
gWindow.setupIDB = gWindow.idbGenerator(() => {
setupIDBInFrames(gWindow, 0, () => {
def.resolve();
});
});
gWindow.setupIDB.next();
yield def.promise;
// Setup the async storages in main window and for all its iframes
let callSetup = function*(win) {
if (typeof(win.setup) == "function") {
yield win.setup();
}
for(var i = 0; i < win.frames.length; i++) {
yield callSetup(win.frames[i]);
}
}
yield callSetup(gWindow);
// open storage inspector
return yield openStoragePanel();
@ -501,6 +490,7 @@ function* selectTreeItem(ids) {
let selector = "[data-id='" + JSON.stringify(ids) + "'] > .tree-widget-item";
let target = gPanelWindow.document.querySelector(selector);
ok(target, "tree item found with ids " + JSON.stringify(ids));
let updated = gUI.once("store-objects-updated");
@ -517,6 +507,7 @@ function* selectTreeItem(ids) {
function* selectTableItem(id) {
let selector = ".table-widget-cell[data-id='" + id + "']";
let target = gPanelWindow.document.querySelector(selector);
ok(target, "table item found with ids " + id);
yield click(target);
yield gUI.once("sidebar-updated");

View File

@ -34,53 +34,63 @@ sessionStorage.setItem("ss2", "This~is~another~array");
sessionStorage.setItem("ss3", "this#is~an#object~foo#bar");
console.log("added cookies and stuff from main page");
function success(event) {
setupIDB.next(event);
}
window.idbGenerator = function*(callback) {
let idbGenerator = function*() {
let request = indexedDB.open("idb1", 1);
request.onupgradeneeded = success;
request.onsuccess = success;
request.onerror = function() {
throw new Error("error opening db connection");
};
let event = yield undefined;
let db = yield new Promise(done => {
request.onupgradeneeded = event => {
let db = event.target.result;
let store1 = db.createObjectStore("obj1", { keyPath: "id" });
store1.createIndex("name", "name", { unique: false });
store1.createIndex("email", "email", { unique: true });
let store2 = db.createObjectStore("obj2", { keyPath: "id2" });
db.createObjectStore("obj2", { keyPath: "id2" });
store1.transaction.oncomplete = () => {
done(db);
};
};
});
store1.add({id: 1, name: "foo", email: "foo@bar.com"}).onsuccess = success;
yield undefined;
store1.add({id: 2, name: "foo2", email: "foo2@bar.com"}).onsuccess = success;
yield undefined;
store1.add({id: 3, name: "foo2", email: "foo3@bar.com"}).onsuccess = success;
yield undefined;
// Prevents AbortError
yield new Promise(done => {
request.onsuccess = done;
});
let transaction = db.transaction(["obj1", "obj2"], "readwrite");
let store1 = transaction.objectStore("obj1");
let store2 = transaction.objectStore("obj2");
store1.add({id: 1, name: "foo", email: "foo@bar.com"});
store1.add({id: 2, name: "foo2", email: "foo2@bar.com"});
store1.add({id: 3, name: "foo2", email: "foo3@bar.com"});
store2.add({
id2: 1,
name: "foo",
email: "foo@bar.com",
extra: "baz"}).onsuccess = success;
yield undefined;
extra: "baz"});
yield undefined;
db.close();
request = indexedDB.open("idb2", 1);
request.onupgradeneeded = success;
request.onsuccess = success;
event = yield undefined;
let db2 = yield new Promise(done => {
request.onupgradeneeded = event => {
let db2 = event.target.result;
let store3 = db2.createObjectStore("obj3", { keyPath: "id3" });
store3.createIndex("name2", "name2", { unique: true });
store3.transaction.oncomplete = () => {
done(db2);
};
};
});
// Prevents AbortError during close()
yield new Promise(done => {
request.onsuccess = done;
});
yield undefined;
db2.close();
console.log("added cookies and stuff from main page");
callback();
};
function deleteDB(dbName) {
@ -90,6 +100,10 @@ function deleteDB(dbName) {
});
}
window.setup = function*() {
yield idbGenerator();
};
window.clear = function*() {
document.cookie = "c1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/browser";
document.cookie = "cs2=; expires=Thu, 01 Jan 1970 00:00:00 GMT";

View File

@ -28,53 +28,66 @@ localStorage.setItem("ls2", "foobar-2");
sessionStorage.setItem("ss1", "foobar-3");
dump("added cookies and stuff from main page\n");
function success(event) {
setupIDB.next(event);
}
window.idbGenerator = function*(callback) {
let idbGenerator = function*() {
let request = indexedDB.open("idb1", 1);
request.onupgradeneeded = success;
request.onsuccess = success;
request.onerror = function() {
throw new Error("error opening db connection");
};
let event = yield undefined;
let db = yield new Promise(done => {
request.onupgradeneeded = event => {
let db = event.target.result;
let store1 = db.createObjectStore("obj1", { keyPath: "id" });
store1.createIndex("name", "name", { unique: false });
store1.createIndex("email", "email", { unique: true });
let store2 = db.createObjectStore("obj2", { keyPath: "id2" });
store1.transaction.oncomplete = () => {
done(db);
};
};
});
store1.add({id: 1, name: "foo", email: "foo@bar.com"}).onsuccess = success;
yield undefined;
store1.add({id: 2, name: "foo2", email: "foo2@bar.com"}).onsuccess = success;
yield undefined;
store1.add({id: 3, name: "foo2", email: "foo3@bar.com"}).onsuccess = success;
yield undefined;
// Prevents AbortError
yield new Promise(done => {
request.onsuccess = done;
});
let transaction = db.transaction(["obj1", "obj2"], "readwrite");
let store1 = transaction.objectStore("obj1");
let store2 = transaction.objectStore("obj2");
store1.add({id: 1, name: "foo", email: "foo@bar.com"});
store1.add({id: 2, name: "foo2", email: "foo2@bar.com"});
store1.add({id: 3, name: "foo2", email: "foo3@bar.com"});
store2.add({
id2: 1,
name: "foo",
email: "foo@bar.com",
extra: "baz"}).onsuccess = success;
yield undefined;
extra: "baz"
});
// Prevents AbortError during close()
yield new Promise(success => {
transaction.oncomplete = success;
});
yield undefined;
db.close();
request = indexedDB.open("idb2", 1);
request.onupgradeneeded = success;
request.onsuccess = success;
event = yield undefined;
let db2 = yield new Promise(done => {
request.onupgradeneeded = event => {
let db2 = event.target.result;
let store3 = db2.createObjectStore("obj3", { keyPath: "id3" });
store3.createIndex("name2", "name2", { unique: true });
yield undefined;
store3.transaction.oncomplete = () => {
done(db2);
}
};
});
// Prevents AbortError during close()
yield new Promise(done => {
request.onsuccess = done;
});
db2.close();
dump("added cookies and stuff from main page\n");
callback();
};
function deleteDB(dbName) {
@ -84,6 +97,17 @@ function deleteDB(dbName) {
});
}
let cacheGenerator = function*() {
let cache = yield caches.open("plop");
yield cache.add("404_cached_file.js");
yield cache.add("browser_storage_basic.js");
};
window.setup = function*() {
yield idbGenerator();
yield cacheGenerator();
};
window.clear = function*() {
document.cookie = "c1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/browser";
document.cookie =
@ -98,6 +122,8 @@ window.clear = function*() {
yield deleteDB("idb1");
yield deleteDB("idb2");
yield caches.delete("plop");
dump("removed cookies, localStorage, sessionStorage and indexedDB data " +
"from " + document.location + "\n");
};

View File

@ -17,41 +17,59 @@ function success(event) {
setupIDB.next(event);
}
window.idbGenerator = function*(callback) {
let idbGenerator = function*() {
let request = indexedDB.open("idb-s1", 1);
request.onupgradeneeded = success;
request.onsuccess = success;
request.onerror = function() {
throw new Error("error opening db connection");
};
let event = yield undefined;
let db = yield new Promise(done => {
request.onupgradeneeded = event => {
let db = event.target.result;
let store1 = db.createObjectStore("obj-s1", { keyPath: "id" });
store1.transaction.oncomplete = () => {
done(db);
};
};
});
yield new Promise(done => {
request.onsuccess = done;
});
store1.add({id: 6, name: "foo", email: "foo@bar.com"}).onsuccess = success;
yield undefined;
store1.add({id: 7, name: "foo2", email: "foo2@bar.com"}).onsuccess = success;
yield undefined;
let transaction = db.transaction(["obj-s1"], "readwrite");
let store1 = transaction.objectStore("obj-s1");
store1.add({id: 6, name: "foo", email: "foo@bar.com"});
store1.add({id: 7, name: "foo2", email: "foo2@bar.com"});
yield new Promise(success => {
transaction.oncomplete = success;
});
yield undefined;
db.close();
request = indexedDB.open("idb-s2", 1);
request.onupgradeneeded = success;
request.onsuccess = success;
event = yield undefined;
let db2 = yield new Promise(done => {
request.onupgradeneeded = event => {
let db2 = event.target.result;
let store3 =
db2.createObjectStore("obj-s2", { keyPath: "id3", autoIncrement: true });
store3.createIndex("name2", "name2", { unique: true });
store3.add({id3: 16, name2: "foo", email: "foo@bar.com"}).onsuccess = success;
yield undefined;
store3.transaction.oncomplete = () => {
done(db2);
};
};
});
yield new Promise(done => {
request.onsuccess = done;
});
transaction = db2.transaction(["obj-s2"], "readwrite");
let store3 = transaction.objectStore("obj-s2");
store3.add({id3: 16, name2: "foo", email: "foo@bar.com"});
yield new Promise(success => {
transaction.oncomplete = success;
});
yield undefined;
db2.close();
dump("added cookies and stuff from secured iframe\n");
callback();
};
function deleteDB(dbName) {
@ -61,6 +79,10 @@ function deleteDB(dbName) {
});
}
window.setup = function*() {
yield idbGenerator();
};
window.clear = function*() {
document.cookie = "sc1=; expires=Thu, 01 Jan 1970 00:00:00 GMT";

View File

@ -341,11 +341,16 @@ StorageUI.prototype = {
populateStorageTree: function(storageTypes) {
this.storageTypes = {};
for (let type in storageTypes) {
// Ignore `from` field, which is just a protocol.js implementation artifact
if (type === "from") {
continue;
}
let typeLabel = L10N.getStr("tree.labels." + type);
let typeLabel = type;
try {
typeLabel = L10N.getStr("tree.labels." + type);
} catch(e) {
console.error("Unable to localize tree label type:" + type);
}
this.tree.add([{id: type, label: typeLabel, type: "store"}]);
if (!storageTypes[type].hosts) {
continue;
@ -568,7 +573,12 @@ StorageUI.prototype = {
if (!uniqueKey) {
this.table.uniqueId = uniqueKey = key;
}
columns[key] = key;
try {
columns[key] = L10N.getStr("table.headers." + type + "." + key);
} catch(e) {
console.error("Unable to localize table header type:" + type + " key:" + key);
}
}
this.table.setColumns(columns, null, HIDDEN_COLUMNS);
this.shouldResetColumns = false;

View File

@ -115,6 +115,7 @@ body {
#global-toolbar > *,
#timeline-toolbar > * {
min-height: var(--toolbar-height);
border-color: var(--theme-splitter-color);
border-width: 0 0 0 1px;
display: flex;
justify-content: center;

View File

@ -1089,7 +1089,7 @@ Messages.Extended = function(messagePieces, options = {})
}
this._repeatID.quoteStrings = this._quoteStrings;
this._repeatID.messagePieces = messagePieces + "";
this._repeatID.messagePieces = JSON.stringify(messagePieces);
this._repeatID.actors = new Set(); // using a set to avoid duplicates
};

View File

@ -163,28 +163,26 @@ skip-if = e10s # Bug 1042253 - webconsole e10s tests (intermittent Linux debug)
skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
[browser_console_copy_entire_message_context_menu.js]
[browser_console_error_source_click.js]
skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests
skip-if = e10s # Bug 1042253 - webconsole e10s tests
[browser_console_filters.js]
[browser_console_iframe_messages.js]
skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests
skip-if = e10s # Bug 1042253 - webconsole e10s tests
[browser_console_keyboard_accessibility.js]
[browser_console_log_inspectable_object.js]
[browser_console_native_getters.js]
[browser_console_navigation_marker.js]
[browser_console_netlogging.js]
[browser_console_nsiconsolemessage.js]
skip-if = buildapp == 'mulet'
[browser_console_optimized_out_vars.js]
skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
[browser_console_private_browsing.js]
skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests
skip-if = e10s # Bug 1042253 - webconsole e10s tests
[browser_console_server_logging.js]
[browser_console_variables_view.js]
skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
[browser_console_variables_view_filter.js]
[browser_console_variables_view_dom_nodes.js]
[browser_console_variables_view_dont_sort_non_sortable_classes_properties.js]
skip-if = buildapp == 'mulet'
[browser_console_variables_view_special_names.js]
[browser_console_variables_view_while_debugging.js]
skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
@ -199,17 +197,14 @@ skip-if = e10s && debug && os == 'win'
[browser_output_breaks_after_console_dir_uninspectable.js]
[browser_output_longstring_expand.js]
[browser_repeated_messages_accuracy.js]
skip-if = buildapp == 'mulet'
[browser_result_format_as_string.js]
[browser_warn_user_about_replaced_api.js]
[browser_webconsole_abbreviate_source_url.js]
[browser_webconsole_allow_mixedcontent_securityerrors.js]
tags = mcb
skip-if = buildapp == 'mulet'
[browser_webconsole_assert.js]
[browser_webconsole_block_mixedcontent_securityerrors.js]
tags = mcb
skip-if = buildapp == 'mulet'
[browser_webconsole_bug_579412_input_focus.js]
[browser_webconsole_bug_580001_closing_after_completion.js]
[browser_webconsole_bug_580030_errors_after_page_reload.js]
@ -285,7 +280,6 @@ tags = mcb
[browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js]
skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout)
[browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js]
skip-if = buildapp == 'mulet'
[browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js]
skip-if = true # Bug 1110500 - mouse event failure in test
[browser_webconsole_bug_764572_output_open_url.js]
@ -294,12 +288,10 @@ skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
[browser_webconsole_bug_770099_violation.js]
skip-if = e10s && os == 'win'
[browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js]
skip-if = buildapp == 'mulet'
[browser_webconsole_bug_804845_ctrl_key_nav.js]
skip-if = os != "mac"
[browser_webconsole_bug_817834_add_edited_input_to_history.js]
[browser_webconsole_bug_837351_securityerrors.js]
skip-if = buildapp == 'mulet'
[browser_webconsole_filter_buttons_contextmenu.js]
[browser_webconsole_bug_1006027_message_timestamps_incorrect.js]
skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug intermittent)
@ -328,7 +320,7 @@ skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
[browser_webconsole_history.js]
[browser_webconsole_hpkp_invalid-headers.js]
[browser_webconsole_hsts_invalid-headers.js]
skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests
skip-if = e10s # Bug 1042253 - webconsole e10s tests
[browser_webconsole_input_field_focus_on_panel_select.js]
[browser_webconsole_inspect-parsed-documents.js]
[browser_webconsole_js_input_expansion.js]

View File

@ -29,6 +29,7 @@ add_task(function* () {
yield testCSSRepeats(hud);
yield testCSSRepeatsAfterReload(hud);
yield testConsoleRepeats(hud);
yield testConsoleFalsyValues(hud);
Services.prefs.clearUserPref(PREF);
});
@ -124,3 +125,52 @@ function testConsoleRepeats(hud) {
],
});
}
function testConsoleFalsyValues(hud) {
hud.jsterm.clearOutput(true);
hud.jsterm.execute("testConsoleFalsyValues()");
info("wait for repeats of falsy values with the console API");
return waitForMessages({
webconsole: hud,
messages: [
{
name: "console.log 'NaN' repeated once",
category: CATEGORY_WEBDEV,
severity: SEVERITY_LOG,
repeats: 1,
},
{
name: "console.log 'undefined' repeated once",
category: CATEGORY_WEBDEV,
severity: SEVERITY_LOG,
repeats: 1,
},
{
name: "console.log 'null' repeated once",
category: CATEGORY_WEBDEV,
severity: SEVERITY_LOG,
repeats: 1,
},
{
name: "console.log 'NaN' repeated twice",
category: CATEGORY_WEBDEV,
severity: SEVERITY_LOG,
repeats: 2,
},
{
name: "console.log 'undefined' repeated twice",
category: CATEGORY_WEBDEV,
severity: SEVERITY_LOG,
repeats: 2,
},
{
name: "console.log 'null' repeated twice",
category: CATEGORY_WEBDEV,
severity: SEVERITY_LOG,
repeats: 2,
},
],
});
}

View File

@ -2,14 +2,15 @@
<html dir="ltr" xml:lang="en-US" lang="en-US">
<head>
<meta charset="utf8">
<title>Test for bugs 720180, 800510 and 865288</title>
<title>Test for bugs 720180, 800510, 865288 and 1218089</title>
<script>
function testConsole() {
// same line and column number
for(var i = 0; i < 2; i++) {
console.log("foo repeat");
}
console.log("foo repeat"); console.error("foo repeat");
console.log("foo repeat");
console.error("foo repeat");
}
function testConsoleObjects() {
for (var i = 0; i < 3; i++) {
@ -17,6 +18,20 @@
console.log("abba", o);
}
}
function testConsoleFalsyValues(){
[NaN, undefined, null].forEach(function(item, index){
console.log(item);
});
[NaN, NaN].forEach(function(item, index){
console.log(item);
});
[undefined, undefined].forEach(function(item, index){
console.log(item);
});
[null, null].forEach(function(item, index){
console.log(item);
});
}
</script>
<style>
body {

View File

@ -138,13 +138,15 @@ var AnimationPlayerActor = ActorClass({
},
/**
* Get the name associated with the player. This is used to match
* up the player with values in the computed animation-name or
* transition-property property.
* Get the name of this animation. This can be either the animation.id
* property if it was set, or the keyframe rule name or the transition
* property.
* @return {String}
*/
getName: function() {
if (this.isAnimation()) {
if (this.player.id) {
return this.player.id;
} else if (this.isAnimation()) {
return this.player.animationName;
} else if (this.isTransition()) {
return this.player.transitionProperty;

View File

@ -922,6 +922,131 @@ StorageActors.createActor({
storeObjectType: "storagestoreobject"
}, getObjectForLocalOrSessionStorage("sessionStorage"));
let CacheAttributes = [
"url",
"status",
];
types.addDictType("cacheobject", {
"url": "string",
"status": "string"
});
// Array of Cache store objects
types.addDictType("cachestoreobject", {
total: "number",
offset: "number",
data: "array:nullable:cacheobject"
});
StorageActors.createActor({
typeName: "Cache",
storeObjectType: "cachestoreobject"
}, {
getCachesForHost: Task.async(function*(host) {
let uri = Services.io.newURI(host, null, null);
let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);
// The first argument tells if you want to get |content| cache or |chrome| cache.
// The |content| cache is the cache explicitely named by the web content
// (service worker or web page).
// The |chrome| cache is the cache implicitely cached by the platform, hosting the
// source file of the service worker.
let { CacheStorage } = this.storageActor.window;
let cache = new CacheStorage("content", principal);
return cache;
}),
preListStores: Task.async(function*() {
for (let host of this.hosts) {
yield this.populateStoresForHost(host);
}
}),
form: function(form, detail) {
if (detail === "actorid") {
return this.actorID;
}
let hosts = {};
for (let host of this.hosts) {
hosts[host] = this.getNamesForHost(host);
}
return {
actor: this.actorID,
hosts: hosts
};
},
getNamesForHost: function(host) {
// UI code expect each name to be a JSON string of an array :/
return [...this.hostVsStores.get(host).keys()].map(a => JSON.stringify([a]));
},
getValuesForHost: Task.async(function*(host, name) {
if (!name) return [];
// UI is weird and expect a JSON stringified array... and pass it back :/
name = JSON.parse(name)[0];
let cache = this.hostVsStores.get(host).get(name);
let requests = yield cache.keys();
let results = [];
for(let request of requests) {
let response = yield cache.match(request);
// Unwrap the response to get access to all its properties if the
// response happen to be 'opaque', when it is a Cross Origin Request.
response = response.cloneUnfiltered();
results.push(yield this.processEntry(request, response));
}
return results;
}),
processEntry: Task.async(function*(request, response) {
return {
url: String(request.url),
status: String(response.statusText),
};
}),
getHostName: function(location) {
if (!location.host) {
return location.href;
}
return location.protocol + "//" + location.host;
},
populateStoresForHost: Task.async(function*(host, window) {
let storeMap = new Map();
let caches = yield this.getCachesForHost(host);
for (let name of (yield caches.keys())) {
storeMap.set(name, (yield caches.open(name)));
}
this.hostVsStores.set(host, storeMap);
}),
/**
* This method is overriden and left blank as for Cache Storage, this
* operation cannot be performed synchronously. Thus, the preListStores
* method exists to do the same task asynchronously.
*/
populateStoresForHosts: function() {
this.hostVsStores = new Map();
},
/**
* Given a url, correctly determine its protocol + hostname part.
*/
getSchemaAndHost: function(url) {
let uri = Services.io.newURI(url, null, null);
return uri.scheme + "://" + uri.hostPort;
},
toStoreObject: function(item) {
return item;
},
});
/**
* Code related to the Indexed DB actor and front
*/

View File

@ -145,6 +145,19 @@
width: 100px;
}
}
.script-generated {
display: inline-block;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: black;
background-image:
repeating-linear-gradient(45deg, transparent 0, transparent 5px, #f06 5px, #f06 10px);
border: 5px solid #f06;
box-sizing: border-box;
}
</style>
<div class="not-animated"></div>
<div class="simple-animation"></div>
@ -157,11 +170,26 @@
<div class="delayed-multiple-animations"></div>
<div class="multiple-animations-2"></div>
<div class="all-transitions"></div>
<div class="script-generated"></div>
<script type="text/javascript">
// Get the transitions started when the page loads
var players;
addEventListener("load", function() {
document.querySelector(".transition").classList.add("get-round");
document.querySelector(".delayed-transition").classList.add("get-round");
// Create a script-generated animation.
var animation = document.querySelector(".script-generated").animate({
backgroundColor: ["black", "gold"]
}, {
duration: 500,
iterations: Infinity,
direction: "alternate"
});
animation.id = "custom-animation-name";
// Add a custom animation id to an existing css animation.
document.querySelector(".delayed-animation")
.getAnimations()[0].id = "cssanimation-custom-name";
});
</script>

View File

@ -28,6 +28,7 @@ support-files =
[browser_animation_getStateAfterFinished.js]
[browser_animation_getSubTreeAnimations.js]
[browser_animation_keepFinished.js]
[browser_animation_name.js]
[browser_animation_playerState.js]
[browser_animation_playPauseIframe.js]
[browser_animation_playPauseSeveral.js]

View File

@ -0,0 +1,51 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the AnimationPlayerActor provides the correct name for an
// animation. Whether this animation is a CSSAnimation, CSSTransition or a
// script-based animation that has been given an id, or even a CSSAnimation that
// has been given an id.
const TEST_DATA = [{
selector: ".simple-animation",
animationIndex: 0,
expectedName: "move"
}, {
selector: ".transition",
animationIndex: 0,
expectedName: "width"
}, {
selector: ".script-generated",
animationIndex: 0,
expectedName: "custom-animation-name"
}, {
selector: ".delayed-animation",
animationIndex: 0,
expectedName: "cssanimation-custom-name"
}];
add_task(function*() {
let {client, walker, animations} =
yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
for (let {selector, animationIndex, expectedName} of TEST_DATA) {
let {name} = yield getAnimationStateForNode(walker, animations, selector,
animationIndex);
is(name, expectedName, "The animation has the expected name");
}
yield closeDebuggerClient(client);
gBrowser.removeCurrentTab();
});
function* getAnimationStateForNode(walker, animations, nodeSelector, index) {
let node = yield walker.querySelector(walker.rootNode, nodeSelector);
let players = yield animations.getAnimationPlayersForNode(node);
let player = players[index];
yield player.ready();
let state = yield player.getCurrentState();
return state;
}

View File

@ -265,10 +265,7 @@ AccessibleCaretManager::UpdateCaretsForCursorMode(UpdateCaretsHint aHint)
case PositionChangedResult::Changed:
switch (aHint) {
case UpdateCaretsHint::Default:
// On Fennec, always show accessiblecaret even if the input is empty
// to make ActionBar visible.
if (sCaretsExtendedVisibility ||
HasNonEmptyTextContent(GetEditingHostForFrame(frame))) {
if (HasNonEmptyTextContent(GetEditingHostForFrame(frame))) {
mFirstCaret->SetAppearance(Appearance::Normal);
} else {
mFirstCaret->SetAppearance(Appearance::NormalNotShown);

View File

@ -252,8 +252,7 @@ protected:
static bool sSelectionBarEnabled;
// Android specific visibility extensions correct compatibility issues
// with caret-drag, tapping into empty inputs, and ActionBar visiblity
// during page scroll.
// with caret-drag and ActionBar visibility during page scroll.
static bool sCaretsExtendedVisibility;
// By default, javascript content selection changes closes AccessibleCarets and

View File

@ -926,6 +926,10 @@ pref("layout.accessiblecaret.enabled", true);
#else
pref("layout.accessiblecaret.enabled", false);
#endif
// Android need persistent carets and actionbar. Turn off the caret timeout.
pref("layout.accessiblecaret.timeout_ms", 0);
// Android generates long tap (mouse) events.
pref("layout.accessiblecaret.use_long_tap_injector", false);

View File

@ -7,6 +7,7 @@ package org.mozilla.gecko;
import android.Manifest;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import org.mozilla.gecko.adjust.AdjustHelperInterface;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.AppConstants.Versions;
@ -1727,9 +1728,7 @@ public class BrowserApp extends GeckoApp
final BrowserDB db = getProfile().getDB();
final ContentResolver cr = getContentResolver();
Telemetry.addToHistogram("PLACES_PAGES_COUNT", db.getCount(cr, "history"));
Telemetry.addToHistogram("PLACES_BOOKMARKS_COUNT", db.getCount(cr, "bookmarks"));
Telemetry.addToHistogram("FENNEC_FAVICONS_COUNT", db.getCount(cr, "favicons"));
Telemetry.addToHistogram("FENNEC_THUMBNAILS_COUNT", db.getCount(cr, "thumbnails"));
Telemetry.addToHistogram("FENNEC_BOOKMARKS_COUNT", db.getCount(cr, "bookmarks"));
Telemetry.addToHistogram("FENNEC_READING_LIST_COUNT", db.getReadingListAccessor().getCount(cr));
Telemetry.addToHistogram("BROWSER_IS_USER_DEFAULT", (isDefaultBrowser(Intent.ACTION_VIEW) ? 1 : 0));
Telemetry.addToHistogram("FENNEC_TABQUEUE_ENABLED", (TabQueueHelper.isTabQueueEnabled(BrowserApp.this) ? 1 : 0));
@ -2222,36 +2221,33 @@ public class BrowserApp extends GeckoApp
}
String url = "";
String telemetryMsg = "urlbar-empty";
final Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
final String userSearchTerm = tab.getUserRequested();
final String tabURL = tab.getURL();
// Check to see if there's a user-entered search term,
// which we save whenever the user performs a search.
final String telemetryMsg;
if (!TextUtils.isEmpty(userSearchTerm)) {
url = userSearchTerm;
telemetryMsg = "urlbar-userentered";
} else {
url = tab.getURL();
telemetryMsg = url.isEmpty() ? "urlbar-empty" : "urlbar-url";
} else if (!TextUtils.isEmpty(tabURL)) {
url = tabURL;
telemetryMsg = "urlbar-url";
}
}
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.ACTIONBAR, telemetryMsg);
}
enterEditingMode(url);
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.ACTIONBAR, telemetryMsg);
}
/**
* Enters editing mode with the specified URL. If a null
* url is given, the empty String will be used instead.
*/
private void enterEditingMode(String url) {
if (url == null) {
url = "";
}
private void enterEditingMode(@NonNull String url) {
if (mBrowserToolbar.isEditing() || mBrowserToolbar.isAnimating()) {
return;
}

View File

@ -181,8 +181,6 @@ public class LocalURLMetadata implements URLMetadata {
}
}
Telemetry.addToHistogram("FENNEC_TILES_CACHE_HIT", data.size());
// If everything was in the cache, we're done!
if (urlsToQuery.size() == 0) {
return Collections.unmodifiableMap(data);

View File

@ -26,16 +26,11 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.zip.GZIPInputStream;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
import ch.boye.httpclientandroidlib.client.HttpClient;
import ch.boye.httpclientandroidlib.client.methods.HttpGet;
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpRequestRetryHandler;
import ch.boye.httpclientandroidlib.impl.client.HttpClientBuilder;
/**
* Download content that has been scheduled during "study" or "verify".
*/
@ -43,8 +38,12 @@ public class DownloadAction extends BaseAction {
private static final String LOGTAG = "DLCDownloadAction";
private static final String CACHE_DIRECTORY = "downloadContent";
private static final String CDN_BASE_URL = "https://mobile.cdn.mozilla.net/";
private static final int STATUS_OK = 200;
private static final int STATUS_PARTIAL_CONTENT = 206;
public interface Callback {
void onContentDownloaded(DownloadContent content);
}
@ -70,8 +69,6 @@ public class DownloadAction extends BaseAction {
return;
}
final HttpClient client = buildHttpClient();
for (DownloadContent content : catalog.getScheduledDownloads()) {
Log.d(LOGTAG, "Downloading: " + content);
@ -96,7 +93,7 @@ public class DownloadAction extends BaseAction {
final String url = createDownloadURL(content);
if (!temporaryFile.exists() || temporaryFile.length() < content.getSize()) {
download(client, url, temporaryFile);
download(url, temporaryFile);
}
if (!verify(temporaryFile, content.getDownloadChecksum())) {
@ -146,22 +143,23 @@ public class DownloadAction extends BaseAction {
Log.v(LOGTAG, "Done");
}
protected void download(HttpClient client, String source, File temporaryFile)
protected void download(String source, File temporaryFile)
throws RecoverableDownloadContentException, UnrecoverableDownloadContentException {
InputStream inputStream = null;
OutputStream outputStream = null;
final HttpGet request = new HttpGet(source);
HttpURLConnection connection = null;
try {
connection = buildHttpURLConnection(source);
final long offset = temporaryFile.exists() ? temporaryFile.length() : 0;
if (offset > 0) {
request.setHeader("Range", "bytes=" + offset + "-");
connection.setRequestProperty("Range", "bytes=" + offset + "-");
}
try {
final HttpResponse response = client.execute(request);
final int status = response.getStatusLine().getStatusCode();
if (status != HttpStatus.SC_OK && status != HttpStatus.SC_PARTIAL_CONTENT) {
final int status = connection.getResponseCode();
if (status != STATUS_OK && status != STATUS_PARTIAL_CONTENT) {
// We are trying to be smart and only retry if this is an error that might resolve in the future.
// TODO: This is guesstimating at best. We want to implement failure counters (Bug 1215106).
if (status >= 500) {
@ -172,6 +170,7 @@ public class DownloadAction extends BaseAction {
// Unrecoverable: Client errors 4xx - Unlikely that this version of the client will ever succeed.
throw new UnrecoverableDownloadContentException("(Unrecoverable) Download failed. Status code: " + status);
} else {
// HttpsUrlConnection: -1 (No valid response code)
// Informational 1xx: They have no meaning to us.
// Successful 2xx: We don't know how to handle anything but 200.
// Redirection 3xx: HttpClient should have followed redirects if possible. We should not see those errors here.
@ -179,14 +178,8 @@ public class DownloadAction extends BaseAction {
}
}
final HttpEntity entity = response.getEntity();
if (entity == null) {
// Recoverable: Should not happen for a valid asset
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.SERVER, "Null entity");
}
inputStream = new BufferedInputStream(entity.getContent());
outputStream = openFile(temporaryFile, status == HttpStatus.SC_PARTIAL_CONTENT);
inputStream = new BufferedInputStream(connection.getInputStream());
outputStream = openFile(temporaryFile, status == STATUS_PARTIAL_CONTENT);
IOUtils.copy(inputStream, outputStream);
@ -198,6 +191,10 @@ public class DownloadAction extends BaseAction {
} finally {
IOUtils.safeStreamClose(inputStream);
IOUtils.safeStreamClose(outputStream);
if (connection != null) {
connection.disconnect();
}
}
}
@ -258,14 +255,21 @@ public class DownloadAction extends BaseAction {
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
}
protected HttpClient buildHttpClient() {
protected HttpURLConnection buildHttpURLConnection(String url)
throws UnrecoverableDownloadContentException, IOException {
// TODO: Implement proxy support (Bug 1209496)
return HttpClientBuilder.create()
.setUserAgent(HardwareUtils.isTablet() ?
try {
System.setProperty("http.keepAlive", "true");
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestProperty("User-Agent", HardwareUtils.isTablet() ?
AppConstants.USER_AGENT_FENNEC_TABLET :
AppConstants.USER_AGENT_FENNEC_MOBILE)
.setRetryHandler(new DefaultHttpRequestRetryHandler())
.build();
AppConstants.USER_AGENT_FENNEC_MOBILE);
connection.setRequestMethod("GET");
return connection;
} catch (MalformedURLException e) {
throw new UnrecoverableDownloadContentException(e);
}
}
protected String createDownloadURL(DownloadContent content) {

View File

@ -80,6 +80,8 @@ class SearchEngineRow extends AnimatedHeightLayout {
private int mMaxSavedSuggestions;
private int mMaxSearchSuggestions;
private final List<Integer> mOccurrences = new ArrayList<Integer>();
public SearchEngineRow(Context context) {
this(context, null);
}
@ -164,19 +166,19 @@ class SearchEngineRow extends AnimatedHeightLayout {
* @param pattern The pattern that is searched for
* @param string The string where we search for the pattern
*/
private List<Integer> findAllOccurrencesOf(String pattern, String string) {
List<Integer> occurrences = new ArrayList<>();
private void refreshOccurrencesWith(String pattern, String string) {
mOccurrences.clear();
final int patternLength = pattern.length();
int indexOfMatch = 0;
int lastIndexOfMatch = 0;
while(indexOfMatch != -1) {
indexOfMatch = string.indexOf(pattern, lastIndexOfMatch);
lastIndexOfMatch = indexOfMatch + patternLength;
// Crash here?
if(indexOfMatch != -1) {
occurrences.add(indexOfMatch);
mOccurrences.add(indexOfMatch);
}
}
return occurrences;
}
/**
@ -201,18 +203,19 @@ class SearchEngineRow extends AnimatedHeightLayout {
final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text);
final String searchTerm = getSuggestionTextFromView(mUserEnteredView);
final int searchTermLength = searchTerm.length();
final List<Integer> occurrences = findAllOccurrencesOf(searchTerm, suggestion);
if (occurrences.size() > 0) {
refreshOccurrencesWith(searchTerm, suggestion);
if (mOccurrences.size() > 0) {
final SpannableStringBuilder sb = new SpannableStringBuilder(suggestion);
int nextStartSpanIndex = 0;
// Done to make sure that the stretch of text after the last occurrence, till the end of the suggestion, is made bold
occurrences.add(suggestion.length());
for(int occurrence : occurrences) {
mOccurrences.add(suggestion.length());
for(int occurrence : mOccurrences) {
// Even though they're the same style, SpannableStringBuilder will interpret there as being only one Span present if we re-use a StyleSpan
StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
sb.setSpan(boldSpan, nextStartSpanIndex, occurrence, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
nextStartSpanIndex = occurrence + searchTermLength;
}
mOccurrences.clear();
suggestionText.setText(sb);
} else {
suggestionText.setText(suggestion);

View File

@ -51,7 +51,7 @@ public class TelemetryPingGenerator {
appName + '/' +
appVersion + '/' +
appUpdateChannel + '/' +
appBuildId + '/';
appBuildId;
}
/**

View File

@ -31,7 +31,6 @@ final class UnusedResourcesUtil {
R.drawable.close,
R.drawable.homepage_banner_firstrun,
R.drawable.icon_openinapp,
R.drawable.network_offline,
R.drawable.pause,
R.drawable.phone,
R.drawable.play,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -35,6 +35,27 @@ var ActionBarHandler = {
return;
}
if (!this._selectionID && e.collapsed) {
switch (e.reason) {
case 'longpressonemptycontent':
// case 'taponcaret':
// Show ActionBar when long pressing on an empty input.
// XXX: If we ever want to show ActionBar when single tapping on the
// caret, uncomment the above case 'taponcaret'.
this._init();
break;
case 'updateposition':
// Do not show ActionBar when single tapping on an non-empty editable
// input.
break;
default:
break;
}
return;
}
// Open a closed ActionBar if carets actually visible.
if (!this._selectionID && e.caretVisuallyVisible) {
this._init();

View File

@ -4123,7 +4123,8 @@ Tab.prototype = {
let list = this.sanitizeRelString(target.rel);
if (list.indexOf("[icon]") != -1) {
jsonMessage = this.makeFaviconMessage(target);
} else if (list.indexOf("[apple-touch-icon]") != -1) {
} else if (list.indexOf("[apple-touch-icon]") != -1 ||
list.indexOf("[apple-touch-icon-precomposed]") != -1) {
let message = this.makeFaviconMessage(target);
this.addMetadata("touchIconList", message.href, message.size);
} else if (list.indexOf("[alternate]") != -1 && aEvent.type == "DOMLinkAdded") {

View File

@ -5,33 +5,24 @@
package org.mozilla.gecko.dlc;
import android.content.Context;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mozilla.gecko.background.testhelpers.TestRunner;
import org.mozilla.gecko.dlc.catalog.DownloadContent;
import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
import android.content.Context;
import org.robolectric.RuntimeEnvironment;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.Arrays;
import java.util.Collections;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
import ch.boye.httpclientandroidlib.StatusLine;
import ch.boye.httpclientandroidlib.client.HttpClient;
import ch.boye.httpclientandroidlib.client.methods.HttpGet;
import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
@ -42,6 +33,9 @@ import static org.mockito.Mockito.*;
public class TestDownloadAction {
private static final String TEST_URL = "http://example.org";
private static final int STATUS_OK = 200;
private static final int STATUS_PARTIAL_CONTENT = 206;
/**
* Scenario: The current network is metered.
*
@ -55,8 +49,8 @@ public class TestDownloadAction {
action.perform(RuntimeEnvironment.application, null);
verify(action, never()).buildHttpClient();
verify(action, never()).download(any(HttpClient.class), anyString(), any(File.class));
verify(action, never()).buildHttpURLConnection(anyString());
verify(action, never()).download(anyString(), any(File.class));
}
/**
@ -73,8 +67,8 @@ public class TestDownloadAction {
action.perform(RuntimeEnvironment.application, null);
verify(action, never()).isActiveNetworkMetered(any(Context.class));
verify(action, never()).buildHttpClient();
verify(action, never()).download(any(HttpClient.class), anyString(), any(File.class));
verify(action, never()).buildHttpURLConnection(anyString());
verify(action, never()).download(anyString(), any(File.class));
}
/**
@ -102,7 +96,7 @@ public class TestDownloadAction {
action.perform(RuntimeEnvironment.application, catalog);
verify(action, never()).download(any(HttpClient.class), anyString(), any(File.class));
verify(action, never()).download(anyString(), any(File.class));
verify(catalog).markAsDownloaded(content);
}
@ -114,15 +108,16 @@ public class TestDownloadAction {
*/
@Test(expected=BaseAction.RecoverableDownloadContentException.class)
public void testServerErrorsAreRecoverable() throws Exception {
HttpClient client = mockHttpClient(500, "");
HttpURLConnection connection = mockHttpURLConnection(500, "");
File temporaryFile = mock(File.class);
doReturn(false).when(temporaryFile).exists();
DownloadAction action = spy(new DownloadAction(null));
action.download(client, TEST_URL, temporaryFile);
doReturn(connection).when(action).buildHttpURLConnection(anyString());
action.download(TEST_URL, temporaryFile);
verify(client).execute(any(HttpUriRequest.class));
verify(connection).getInputStream();
}
/**
@ -133,15 +128,16 @@ public class TestDownloadAction {
*/
@Test(expected=BaseAction.UnrecoverableDownloadContentException.class)
public void testClientErrorsAreUnrecoverable() throws Exception {
HttpClient client = mockHttpClient(404, "");
HttpURLConnection connection = mockHttpURLConnection(404, "");
File temporaryFile = mock(File.class);
doReturn(false).when(temporaryFile).exists();
DownloadAction action = spy(new DownloadAction(null));
action.download(client, TEST_URL, temporaryFile);
doReturn(connection).when(action).buildHttpURLConnection(anyString());
action.download(TEST_URL, temporaryFile);
verify(client).execute(any(HttpUriRequest.class));
verify(connection).getInputStream();
}
/**
@ -169,14 +165,13 @@ public class TestDownloadAction {
doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content);
doReturn(false).when(action).verify(eq(file), anyString());
doNothing().when(action).download(any(HttpClient.class), anyString(), eq(file));
doNothing().when(action).download(anyString(), eq(file));
doReturn(true).when(action).verify(eq(file), anyString());
doNothing().when(action).extract(eq(file), eq(file), anyString());
action.perform(RuntimeEnvironment.application, catalog);
verify(action).buildHttpClient();
verify(action).download(any(HttpClient.class), anyString(), eq(file));
verify(action).download(anyString(), eq(file));
verify(action).extract(eq(file), eq(file), anyString());
verify(catalog).markAsDownloaded(content);
}
@ -209,8 +204,8 @@ public class TestDownloadAction {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
doReturn(outputStream).when(action).openFile(eq(temporaryFile), anyBoolean());
HttpClient client = mockHttpClient(HttpStatus.SC_PARTIAL_CONTENT, "HelloWorld");
doReturn(client).when(action).buildHttpClient();
HttpURLConnection connection = mockHttpURLConnection(STATUS_PARTIAL_CONTENT, "HelloWorld");
doReturn(connection).when(action).buildHttpURLConnection(anyString());
File destinationFile = mockNotExistingFile();
doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
@ -220,12 +215,9 @@ public class TestDownloadAction {
action.perform(RuntimeEnvironment.application, catalog);
ArgumentCaptor<HttpGet> argument = ArgumentCaptor.forClass(HttpGet.class);
verify(client).execute(argument.capture());
verify(connection).getInputStream();
verify(connection).setRequestProperty("Range", "bytes=1337-");
HttpGet request = argument.getValue();
Assert.assertTrue(request.containsHeader("Range"));
Assert.assertEquals("bytes=1337-", request.getFirstHeader("Range").getValue());
Assert.assertEquals("HelloWorld", new String(outputStream.toByteArray(), "UTF-8"));
verify(action).openFile(eq(temporaryFile), eq(true));
@ -261,8 +253,8 @@ public class TestDownloadAction {
doReturn(outputStream).when(action).openFile(eq(temporaryFile), anyBoolean());
doThrow(IOException.class).when(outputStream).write(any(byte[].class), anyInt(), anyInt());
HttpClient client = mockHttpClient(HttpStatus.SC_PARTIAL_CONTENT, "HelloWorld");
doReturn(client).when(action).buildHttpClient();
HttpURLConnection connection = mockHttpURLConnection(STATUS_PARTIAL_CONTENT, "HelloWorld");
doReturn(connection).when(action).buildHttpURLConnection(anyString());
doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content);
@ -306,7 +298,7 @@ public class TestDownloadAction {
action.perform(RuntimeEnvironment.application, catalog);
verify(action, never()).download(any(HttpClient.class), anyString(), eq(temporaryFile));
verify(action, never()).download(anyString(), eq(temporaryFile));
verify(action).verify(eq(temporaryFile), anyString());
verify(action).extract(eq(temporaryFile), eq(destinationFile), anyString());
verify(catalog).markAsDownloaded(content);
@ -333,7 +325,7 @@ public class TestDownloadAction {
DownloadAction action = spy(new DownloadAction(null));
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
doNothing().when(action).download(any(HttpClient.class), anyString(), any(File.class));
doNothing().when(action).download(anyString(), any(File.class));
doReturn(false).when(action).verify(any(File.class), anyString());
File temporaryFile = mockNotExistingFile();
@ -372,8 +364,8 @@ public class TestDownloadAction {
doReturn(true).when(action).hasEnoughDiskSpace(content, destinationFile, temporaryFile);
verify(action, never()).buildHttpClient();
verify(action, never()).download(any(HttpClient.class), anyString(), any(File.class));
verify(action, never()).buildHttpURLConnection(anyString());
verify(action, never()).download(anyString(), any(File.class));
verify(action, never()).verify(any(File.class), anyString());
verify(catalog, never()).markAsDownloaded(content);
}
@ -444,9 +436,9 @@ public class TestDownloadAction {
doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content);
doReturn(true).when(action).hasEnoughDiskSpace(eq(content), any(File.class), any(File.class));
HttpClient client = mock(HttpClient.class);
doThrow(IOException.class).when(client).execute(any(HttpUriRequest.class));
doReturn(client).when(action).buildHttpClient();
HttpURLConnection connection = mockHttpURLConnection(STATUS_OK, "");
doThrow(IOException.class).when(connection).getInputStream();
doReturn(connection).when(action).buildHttpURLConnection(anyString());
action.perform(RuntimeEnvironment.application, catalog);
@ -476,7 +468,7 @@ public class TestDownloadAction {
doReturn(mockNotExistingFile()).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content);
doReturn(true).when(action).hasEnoughDiskSpace(eq(content), any(File.class), any(File.class));
doNothing().when(action).download(any(HttpClient.class), anyString(), any(File.class));
doNothing().when(action).download(anyString(), any(File.class));
doReturn(true).when(action).verify(any(File.class), anyString());
File destinationFile = mock(File.class);
@ -541,20 +533,12 @@ public class TestDownloadAction {
return file;
}
private static HttpClient mockHttpClient(int statusCode, String content) throws Exception {
StatusLine status = mock(StatusLine.class);
doReturn(statusCode).when(status).getStatusCode();
private static HttpURLConnection mockHttpURLConnection(int statusCode, String content) throws Exception {
HttpURLConnection connection = mock(HttpURLConnection.class);
HttpEntity entity = mock(HttpEntity.class);
doReturn(new ByteArrayInputStream(content.getBytes("UTF-8"))).when(entity).getContent();
doReturn(statusCode).when(connection).getResponseCode();
doReturn(new ByteArrayInputStream(content.getBytes("UTF-8"))).when(connection).getInputStream();
HttpResponse response = mock(HttpResponse.class);
doReturn(status).when(response).getStatusLine();
doReturn(entity).when(response).getEntity();
HttpClient client = mock(HttpClient.class);
doReturn(response).when(client).execute(any(HttpUriRequest.class));
return client;
return connection;
}
}

View File

@ -14,6 +14,7 @@ Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-common/logmanager.js");
Cu.import("resource://services-common/async.js");
XPCOMUtils.defineLazyModuleGetter(this, "Status",
"resource://services-sync/status.js");
@ -577,18 +578,22 @@ ErrorHandler.prototype = {
this._log.debug(data + " failed to apply some records.");
}
break;
case "weave:engine:sync:error":
case "weave:engine:sync:error": {
let exception = subject; // exception thrown by engine's sync() method
let engine_name = data; // engine name that threw the exception
this.checkServerError(exception);
Status.engines = [engine_name, exception.failureCode || ENGINE_UNKNOWN_FAIL];
if (Async.isShutdownException(exception)) {
this._log.debug(engine_name + " was interrupted due to the application shutting down");
} else {
this._log.debug(engine_name + " failed", exception);
Services.telemetry.getKeyedHistogramById("WEAVE_ENGINE_SYNC_ERRORS")
.add(engine_name);
}
break;
}
case "weave:service:login:error":
this._log.error("Sync encountered a login error");
this.resetFileLog();
@ -601,12 +606,22 @@ ErrorHandler.prototype = {
this.dontIgnoreErrors = false;
break;
case "weave:service:sync:error":
case "weave:service:sync:error": {
if (Status.sync == CREDENTIALS_CHANGED) {
this.service.logout();
}
this._log.error("Sync encountered an error");
let exception = subject;
if (Async.isShutdownException(exception)) {
// If we are shutting down we just log the fact, attempt to flush
// the log file and get out of here!
this._log.error("Sync was interrupted due to the application shutting down");
this.resetFileLog();
break;
}
// Not a shutdown related exception...
this._log.error("Sync encountered an error", exception);
this.resetFileLog();
if (this.shouldReportError()) {
@ -617,6 +632,7 @@ ErrorHandler.prototype = {
this.dontIgnoreErrors = false;
break;
}
case "weave:service:sync:finish":
this._log.trace("Status.service is " + Status.service);

View File

@ -161,7 +161,7 @@ var HistoryEntry = {
}
let all_items_found = true;
for (let itemvisit in item.visits) {
for (let itemvisit of item.visits) {
all_items_found = all_items_found && "found" in itemvisit;
Logger.logInfo("History entry for " + item.uri + ", type:" +
itemvisit.type + ", date:" + itemvisit.date +

View File

@ -888,7 +888,6 @@ var TPS = {
// that complete.
if (this.fxaccounts_enabled) {
this._triggeredSync = true;
this.waitForEvent("weave:service:sync:start");
this.waitForSyncFinished();
}
},
@ -919,6 +918,7 @@ var TPS = {
this._triggeredSync = true;
this.StartAsyncOperation();
Weave.Service.sync();
Logger.logInfo("Sync is complete");
},
WipeServer: function TPS__WipeServer() {

View File

@ -342,11 +342,11 @@ class TPSTestRunner(object):
if self.mobile:
self.preferences.update({'services.sync.client.type' : 'mobile'})
# Set a dummy username to force the correct authentication type. For the
# old sync, the username is not allowed to contain a '@'.
dummy = {'fx_account': 'dummy@somewhere', 'sync_account': 'dummy'}
auth_type = self.config.get('auth_type', 'fx_account')
self.preferences.update({'services.sync.username': dummy[auth_type]})
# If we are using legacy Sync, then set a dummy username to force the
# correct authentication type. Without this pref set to a value
# without an '@' character, Sync will initialize for FxA.
if self.config.get('auth_type', 'fx_account') != "fx_account":
self.preferences.update({'services.sync.username': "dummy"})
if self.debug:
self.preferences.update(self.debug_preferences)

View File

@ -2410,7 +2410,7 @@ var Microformats; // jshint ignore:line
* @return {Object || undefined}
*/
getDOMParser: function () {
if (typeof DOMParser === undefined) {
if (typeof DOMParser === "undefined") {
try {
return Components.classes["@mozilla.org/xmlextras/domparser;1"]
.createInstance(Components.interfaces.nsIDOMParser);

View File

@ -3386,21 +3386,14 @@
"description": "Time taken to download a specified distribution file (msec)",
"cpp_guard": "ANDROID"
},
"FENNEC_FAVICONS_COUNT": {
"expires_in_version": "40",
"FENNEC_BOOKMARKS_COUNT": {
"expires_in_version": "60",
"kind": "exponential",
"high": "2000",
"n_buckets": 10,
"cpp_guard": "ANDROID",
"description": "Number of favicons stored in the browser DB *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
},
"FENNEC_THUMBNAILS_COUNT": {
"expires_in_version": "40",
"kind": "exponential",
"high": "2000",
"n_buckets": 10,
"cpp_guard": "ANDROID",
"description": "Number of thumbnails stored in the browser DB *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
"high": "8000",
"n_buckets": 20,
"description": "Number of bookmarks stored in the browser DB",
"alert_emails": ["mobile-frontend@mozilla.com"],
"bug_numbers": [1244704]
},
"FENNEC_READING_LIST_COUNT": {
"expires_in_version": "50",
@ -8148,13 +8141,6 @@
"kind": "flag",
"description": "If a master-password is enabled for this profile"
},
"FENNEC_TILES_CACHE_HIT": {
"expires_in_version": "40",
"kind": "linear",
"high": "13",
"n_buckets": 12,
"description": "Cache hits on the tile-info metadata database *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
},
"DISPLAY_SCALING_OSX" : {
"expires_in_version": "never",
"kind": "linear",

View File

@ -58,6 +58,8 @@ const PREF_UNIFIED = PREF_BRANCH + "unified";
const MESSAGE_TELEMETRY_PAYLOAD = "Telemetry:Payload";
const MESSAGE_TELEMETRY_GET_CHILD_PAYLOAD = "Telemetry:GetChildPayload";
const MESSAGE_TELEMETRY_THREAD_HANGS = "Telemetry:ChildThreadHangs";
const MESSAGE_TELEMETRY_GET_CHILD_THREAD_HANGS = "Telemetry:GetChildThreadHangs";
const MESSAGE_TELEMETRY_USS = "Telemetry:USS";
const MESSAGE_TELEMETRY_GET_CHILD_USS = "Telemetry:GetChildUSS";
@ -541,6 +543,17 @@ this.TelemetrySession = Object.freeze({
requestChildPayloads: function() {
return Impl.requestChildPayloads();
},
/**
* Returns a promise that resolves to an array of thread hang stats from content processes, one entry per process.
* The structure of each entry is identical to that of "threadHangStats" in nsITelemetry.
* While thread hang stats are also part of the child payloads, this function is useful for cheaply getting this information,
* which is useful for realtime hang monitoring.
* Child processes that do not respond, or spawn/die during execution of this function are excluded from the result.
* @returns Promise
*/
getChildThreadHangs: function() {
return Impl.getChildThreadHangs();
},
/**
* Save the session state to a pending file.
* Used only for testing purposes.
@ -646,6 +659,15 @@ var Impl = {
// where source is a weak reference to the child process,
// and payload is the telemetry payload from that child process.
_childTelemetry: [],
// Thread hangs from child processes.
// Each element is in the format {source: <weak-ref>, payload: <object>},
// where source is a weak reference to the child process,
// and payload contains the thread hang stats from that child process.
_childThreadHangs: [],
// Array of the resolve functions of all the promises that are waiting for the child thread hang stats to arrive, used to resolve all those promises at once
_childThreadHangsResolveFunctions: [],
// Timeout function for child thread hang stats retrieval
_childThreadHangsTimeout: null,
// Unique id that identifies this session so the server can cope with duplicate
// submissions, orphaning and other oddities. The id is shared across subsessions.
_sessionId: null,
@ -1372,6 +1394,7 @@ var Impl = {
this._hasXulWindowVisibleObserver = true;
ppml.addMessageListener(MESSAGE_TELEMETRY_PAYLOAD, this);
ppml.addMessageListener(MESSAGE_TELEMETRY_THREAD_HANGS, this);
ppml.addMessageListener(MESSAGE_TELEMETRY_USS, this);
// Delay full telemetry initialization to give the browser time to
@ -1438,6 +1461,7 @@ var Impl = {
Services.obs.addObserver(this, "content-child-shutdown", false);
cpml.addMessageListener(MESSAGE_TELEMETRY_GET_CHILD_PAYLOAD, this);
cpml.addMessageListener(MESSAGE_TELEMETRY_GET_CHILD_THREAD_HANGS, this);
cpml.addMessageListener(MESSAGE_TELEMETRY_GET_CHILD_USS, this);
this.gatherStartupHistograms();
@ -1470,6 +1494,7 @@ var Impl = {
switch (message.name) {
case MESSAGE_TELEMETRY_PAYLOAD:
{
// In parent process, receive Telemetry payload from child
let source = message.data.childUUID;
delete message.data.childUUID;
@ -1495,11 +1520,42 @@ var Impl = {
}
case MESSAGE_TELEMETRY_GET_CHILD_PAYLOAD:
{
// In child process, send the requested Telemetry payload
this.sendContentProcessPing("saved-session");
break;
}
case MESSAGE_TELEMETRY_THREAD_HANGS:
{
// Accumulate child thread hang stats from this child
this._childThreadHangs.push(message.data);
// Check if we've got data from all the children, accounting for child processes dying
// if it happens before the last response is received and no new child processes are spawned at the exact same time
// If that happens, we can resolve the promise earlier rather than having to wait for the timeout to expire
// Basically, the number of replies is at most the number of messages sent out, this._childCount,
// and also at most the number of child processes that currently exist
if (this._childThreadHangs.length === Math.min(this._childCount, ppmm.childCount)) {
clearTimeout(this._childThreadHangsTimeout);
// Resolve all the promises that are waiting on these thread hang stats
// We resolve here instead of rejecting because
for (let resolve of this._childThreadHangsResolveFunctions) {
resolve(this._childThreadHangs);
}
this._childThreadHangsResolveFunctions = [];
}
break;
}
case MESSAGE_TELEMETRY_GET_CHILD_THREAD_HANGS:
{
// In child process, send the requested child thread hangs
this.sendContentProcessThreadHangs();
break;
}
case MESSAGE_TELEMETRY_USS:
{
// In parent process, receive the USS report from the child
if (this._totalMemoryTimeout && this._childrenToHearFrom.delete(message.data.id)) {
this._totalMemory += message.data.bytes;
if (this._childrenToHearFrom.size == 0) {
@ -1517,6 +1573,7 @@ var Impl = {
}
case MESSAGE_TELEMETRY_GET_CHILD_USS:
{
// In child process, send the requested USS report
this.sendContentProcessUSS(message.data.id);
break
}
@ -1553,6 +1610,15 @@ var Impl = {
cpmm.sendAsyncMessage(MESSAGE_TELEMETRY_PAYLOAD, payload);
},
sendContentProcessThreadHangs: function sendContentProcessThreadHangs() {
this._log.trace("sendContentProcessThreadHangs");
let payload = {
childUUID: this._processUUID,
hangs: Telemetry.threadHangStats,
};
cpmm.sendAsyncMessage(MESSAGE_TELEMETRY_THREAD_HANGS, payload);
},
/**
* Save both the "saved-session" and the "shutdown" pings to disk.
* This needs to be called after TelemetrySend shuts down otherwise pings
@ -1642,6 +1708,50 @@ var Impl = {
ppmm.broadcastAsyncMessage(MESSAGE_TELEMETRY_GET_CHILD_PAYLOAD, {});
},
getChildThreadHangs: function getChildThreadHangs() {
return new Promise((resolve) => {
// Return immediately if there are no child processes to get stats from
if (ppmm.childCount === 0) {
resolve([]);
return;
}
// Register our promise so it will be resolved when we receive the child thread hang stats on the parent process
// The resolve functions will all be called from "receiveMessage" when a MESSAGE_TELEMETRY_THREAD_HANGS message comes in
this._childThreadHangsResolveFunctions.push((threadHangStats) => {
let hangs = threadHangStats.map(child => child.hangs);
return resolve(hangs);
});
// If we (the parent) are not currently in the process of requesting child thread hangs, request them
// If we are, then the resolve function we registered above will receive the results without needing to request them again
if (this._childThreadHangsResolveFunctions.length === 1) {
// We have to cache the number of children we send messages to, in case the child count changes while waiting for messages to arrive
// This handles the case where the child count increases later on, in which case the new processes won't respond since we never sent messages to them
this._childCount = ppmm.childCount;
this._childThreadHangs = []; // Clear the child hangs
for (let i = 0; i < this._childCount; i++) {
// If a child dies at exactly while we're running this loop, the message sending will fail but we won't get an exception
// In this case, since we won't know this has happened, we will simply rely on the timeout to handle it
ppmm.getChildAt(i).sendAsyncMessage(MESSAGE_TELEMETRY_GET_CHILD_THREAD_HANGS);
}
// Set up a timeout in case one or more of the content processes never responds
this._childThreadHangsTimeout = setTimeout(() => {
// Resolve all the promises that are waiting on these thread hang stats
// We resolve here instead of rejecting because the purpose of this function is
// to retrieve the BHR stats from all processes that will give us stats
// As a result, one process failing simply means it doesn't get included in the result.
for (let resolve of this._childThreadHangsResolveFunctions) {
resolve(this._childThreadHangs);
}
this._childThreadHangsResolveFunctions = [];
}, 200);
}
});
},
gatherStartup: function gatherStartup() {
this._log.trace("gatherStartup");
let counters = processInfo.getCounters();

View File

@ -8,6 +8,7 @@
.main-content {
padding-top: 0;
-moz-padding-end: 0;
padding-bottom: 0;
}
#nav-header {