зеркало из https://github.com/mozilla/gecko-dev.git
Merge mozilla-central to mozilla-inbound
This commit is contained in:
Коммит
26b5712f9f
|
@ -212,12 +212,15 @@ class BasePopup {
|
|||
break;
|
||||
|
||||
case "DOMWindowCreated":
|
||||
let winUtils = this.browser.contentWindow
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
|
||||
for (let stylesheet of global.stylesheets) {
|
||||
winUtils.addSheet(stylesheet, winUtils.AGENT_SHEET);
|
||||
if (event.target === this.browser.contentDocument) {
|
||||
let winUtils = this.browser.contentWindow
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
|
||||
for (let stylesheet of global.stylesheets) {
|
||||
winUtils.addSheet(stylesheet, winUtils.AGENT_SHEET);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "DOMWindowClose":
|
||||
if (event.target === this.browser.contentWindow) {
|
||||
event.preventDefault();
|
||||
|
|
|
@ -1188,11 +1188,13 @@ html|span.ac-tag {
|
|||
height: 16px;
|
||||
}
|
||||
|
||||
.ac-type-icon[type=switchtab] {
|
||||
.ac-type-icon[type=switchtab],
|
||||
.ac-type-icon[type=remotetab] {
|
||||
list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab");
|
||||
}
|
||||
|
||||
.ac-type-icon[type=switchtab][selected] {
|
||||
.ac-type-icon[type=switchtab][selected],
|
||||
.ac-type-icon[type=remotetab][selected] {
|
||||
list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab-inverted");
|
||||
}
|
||||
|
||||
|
|
|
@ -1835,11 +1835,13 @@ html|span.ac-emphasize-text-url {
|
|||
list-style-image: url("chrome://browser/skin/places/tag.png");
|
||||
}
|
||||
|
||||
.ac-type-icon[type=switchtab] {
|
||||
.ac-type-icon[type=switchtab],
|
||||
.ac-type-icon[type=remotetab] {
|
||||
list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab");
|
||||
}
|
||||
|
||||
.ac-type-icon[type=switchtab][selected] {
|
||||
.ac-type-icon[type=switchtab][selected],
|
||||
.ac-type-icon[type=remotetab][selected] {
|
||||
list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab-inverted");
|
||||
}
|
||||
|
||||
|
|
|
@ -1573,11 +1573,13 @@ html|span.ac-emphasize-text-url {
|
|||
height: 16px;
|
||||
}
|
||||
|
||||
.ac-type-icon[type=switchtab] {
|
||||
.ac-type-icon[type=switchtab],
|
||||
.ac-type-icon[type=remotetab] {
|
||||
list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab");
|
||||
}
|
||||
|
||||
.ac-type-icon[type=switchtab][selected] {
|
||||
.ac-type-icon[type=switchtab][selected],
|
||||
.ac-type-icon[type=remotetab][selected] {
|
||||
list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab-inverted");
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,19 @@ module.exports = createClass({
|
|||
BrowserToolboxProcess.init({ addonID: target.addonID });
|
||||
},
|
||||
|
||||
reload() {
|
||||
let { client, target } = this.props;
|
||||
// This function sometimes returns a partial promise that only
|
||||
// implements then().
|
||||
client.request({
|
||||
to: target.addonActor,
|
||||
type: "reload"
|
||||
}).then(() => {}, error => {
|
||||
throw new Error(
|
||||
"Error reloading addon " + target.addonID + ": " + error);
|
||||
});
|
||||
},
|
||||
|
||||
render() {
|
||||
let { target, debugDisabled } = this.props;
|
||||
|
||||
|
@ -41,7 +54,11 @@ module.exports = createClass({
|
|||
className: "debug-button",
|
||||
onClick: this.debug,
|
||||
disabled: debugDisabled,
|
||||
}, Strings.GetStringFromName("debug"))
|
||||
}, Strings.GetStringFromName("debug")),
|
||||
dom.button({
|
||||
className: "reload-button",
|
||||
onClick: this.reload
|
||||
}, Strings.GetStringFromName("reload"))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -60,17 +60,21 @@ module.exports = createClass({
|
|||
},
|
||||
|
||||
updateAddonsList() {
|
||||
AddonManager.getAllAddons(addons => {
|
||||
let extensions = addons.filter(addon => addon.isDebuggable).map(addon => {
|
||||
return {
|
||||
name: addon.name,
|
||||
icon: addon.iconURL || ExtensionIcon,
|
||||
addonID: addon.id
|
||||
};
|
||||
});
|
||||
this.props.client.listAddons()
|
||||
.then(({addons}) => {
|
||||
let extensions = addons.filter(addon => addon.debuggable).map(addon => {
|
||||
return {
|
||||
name: addon.name,
|
||||
icon: addon.iconURL || ExtensionIcon,
|
||||
addonID: addon.id,
|
||||
addonActor: addon.actor
|
||||
};
|
||||
});
|
||||
|
||||
this.setState({ extensions });
|
||||
});
|
||||
this.setState({ extensions });
|
||||
}, error => {
|
||||
throw new Error("Client error while listing addons: " + error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,6 +14,7 @@ support-files =
|
|||
[browser_addons_debug_bootstrapped.js]
|
||||
[browser_addons_debugging_initial_state.js]
|
||||
[browser_addons_install.js]
|
||||
[browser_addons_reload.js]
|
||||
[browser_addons_toggle_debug.js]
|
||||
[browser_service_workers.js]
|
||||
[browser_service_workers_push.js]
|
||||
|
|
|
@ -22,6 +22,7 @@ add_task(function* () {
|
|||
});
|
||||
|
||||
let { tab, document } = yield openAboutDebugging("addons");
|
||||
yield waitForInitialAddonList(document);
|
||||
yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
|
||||
"test-devtools");
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ function* testCheckboxState(testData) {
|
|||
});
|
||||
|
||||
let { tab, document } = yield openAboutDebugging("addons");
|
||||
yield waitForInitialAddonList(document);
|
||||
|
||||
info("Install a test addon.");
|
||||
yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
|
||||
|
|
|
@ -7,6 +7,7 @@ const ADDON_NAME = "test-devtools";
|
|||
|
||||
add_task(function* () {
|
||||
let { tab, document } = yield openAboutDebugging("addons");
|
||||
yield waitForInitialAddonList(document);
|
||||
|
||||
// Install this add-on, and verify that it appears in the about:debugging UI
|
||||
yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
|
||||
|
@ -20,6 +21,7 @@ add_task(function* () {
|
|||
|
||||
add_task(function* () {
|
||||
let { tab, document } = yield openAboutDebugging("addons");
|
||||
yield waitForInitialAddonList(document);
|
||||
|
||||
// Start an observer that looks for the install error before
|
||||
// actually doing the install
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
const ADDON_ID = "test-devtools@mozilla.org";
|
||||
const ADDON_NAME = "test-devtools";
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when the given add-on event is fired. The
|
||||
* resolved value is an array of arguments passed for the event.
|
||||
*/
|
||||
function promiseAddonEvent(event) {
|
||||
return new Promise(resolve => {
|
||||
let listener = {
|
||||
[event]: function(...args) {
|
||||
AddonManager.removeAddonListener(listener);
|
||||
resolve(args);
|
||||
}
|
||||
};
|
||||
|
||||
AddonManager.addAddonListener(listener);
|
||||
});
|
||||
}
|
||||
|
||||
add_task(function* () {
|
||||
const { tab, document } = yield openAboutDebugging("addons");
|
||||
yield waitForInitialAddonList(document);
|
||||
yield installAddon(document, "addons/unpacked/install.rdf",
|
||||
ADDON_NAME, ADDON_NAME);
|
||||
|
||||
// Retrieve the Reload button.
|
||||
const names = [...document.querySelectorAll("#addons .target-name")];
|
||||
const name = names.filter(element => element.textContent === ADDON_NAME)[0];
|
||||
ok(name, "Found " + ADDON_NAME + " add-on in the list");
|
||||
const targetElement = name.parentNode.parentNode;
|
||||
const reloadButton = targetElement.querySelector(".reload-button");
|
||||
ok(reloadButton, "Found its reload button");
|
||||
|
||||
const onDisabled = promiseAddonEvent("onDisabled");
|
||||
const onEnabled = promiseAddonEvent("onEnabled");
|
||||
|
||||
const onBootstrapInstallCalled = new Promise(done => {
|
||||
Services.obs.addObserver(function listener() {
|
||||
Services.obs.removeObserver(listener, ADDON_NAME, false);
|
||||
ok(true, "Add-on was installed: " + ADDON_NAME);
|
||||
done();
|
||||
}, ADDON_NAME, false);
|
||||
});
|
||||
|
||||
reloadButton.click();
|
||||
|
||||
const [disabledAddon] = yield onDisabled;
|
||||
ok(disabledAddon.name === ADDON_NAME,
|
||||
"Add-on was disabled: " + disabledAddon.name);
|
||||
|
||||
const [enabledAddon] = yield onEnabled;
|
||||
ok(enabledAddon.name === ADDON_NAME,
|
||||
"Add-on was re-enabled: " + enabledAddon.name);
|
||||
|
||||
yield onBootstrapInstallCalled;
|
||||
|
||||
info("Uninstall addon installed earlier.");
|
||||
yield uninstallAddon(document, ADDON_ID, ADDON_NAME);
|
||||
yield closeAboutDebugging(tab);
|
||||
});
|
|
@ -20,6 +20,7 @@ add_task(function* () {
|
|||
});
|
||||
|
||||
let { tab, document } = yield openAboutDebugging("addons");
|
||||
yield waitForInitialAddonList(document);
|
||||
|
||||
info("Install a test addon.");
|
||||
yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
/* eslint-disable mozilla/no-cpows-in-tests */
|
||||
/* exported openAboutDebugging, closeAboutDebugging, installAddon,
|
||||
uninstallAddon, waitForMutation, assertHasTarget,
|
||||
waitForServiceWorkerRegistered, unregisterServiceWorker */
|
||||
waitForInitialAddonList, waitForServiceWorkerRegistered,
|
||||
unregisterServiceWorker */
|
||||
/* global sendAsyncMessage */
|
||||
|
||||
"use strict";
|
||||
|
@ -155,6 +156,30 @@ function* uninstallAddon(document, addonId, addonName) {
|
|||
+ names);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that will resolve when the add-on list has been updated.
|
||||
*
|
||||
* @param {Node} document
|
||||
* @return {Promise}
|
||||
*/
|
||||
function waitForInitialAddonList(document) {
|
||||
const addonListContainer = document.querySelector("#addons .targets");
|
||||
let addonCount = addonListContainer.querySelectorAll(".target");
|
||||
addonCount = addonCount ? [...addonCount].length : -1;
|
||||
info("Waiting for add-ons to load. Current add-on count: " + addonCount);
|
||||
|
||||
// This relies on the network speed of the actor responding to the
|
||||
// listAddons() request and also the speed of openAboutDebugging().
|
||||
let result;
|
||||
if (addonCount > 0) {
|
||||
info("Actually, the add-ons have already loaded");
|
||||
result = Promise.resolve();
|
||||
} else {
|
||||
result = waitForMutation(addonListContainer, { childList: true });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that will resolve after receiving a mutation matching the
|
||||
* provided mutation options on the provided target.
|
||||
|
|
|
@ -127,9 +127,10 @@ HTMLBreadcrumbs.prototype = {
|
|||
* @param {Error} err
|
||||
*/
|
||||
selectionGuardEnd: function(err) {
|
||||
if (err === "selection-changed") {
|
||||
console.warn("Asynchronous operation was aborted as selection changed.");
|
||||
} else {
|
||||
// If the error is selection-changed, this is expected, the selection
|
||||
// changed while we were waiting for a promise to resolve, so there's no
|
||||
// need to proceed with the current update, and we should be silent.
|
||||
if (err !== "selection-changed") {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
|
@ -618,7 +619,7 @@ HTMLBreadcrumbs.prototype = {
|
|||
return;
|
||||
}
|
||||
moreChildren();
|
||||
}).then(null, this.selectionGuardEnd);
|
||||
}).catch(this.selectionGuardEnd);
|
||||
};
|
||||
|
||||
moreChildren();
|
||||
|
@ -816,7 +817,7 @@ HTMLBreadcrumbs.prototype = {
|
|||
this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront);
|
||||
doneUpdating();
|
||||
});
|
||||
}).then(null, err => {
|
||||
}).catch(err => {
|
||||
doneUpdating(this.selection.nodeFront);
|
||||
this.selectionGuardEnd(err);
|
||||
});
|
||||
|
|
|
@ -1066,7 +1066,7 @@ InspectorPanel.prototype = {
|
|||
yield onMutations;
|
||||
|
||||
// Select the new node (this will auto-expand its parent).
|
||||
this.selection.setNodeFront(nodes[0]);
|
||||
this.selection.setNodeFront(nodes[0], "node-inserted");
|
||||
}),
|
||||
|
||||
/**
|
||||
|
|
|
@ -550,19 +550,25 @@ MarkupView.prototype = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Focus the current node selection's MarkupContainer if the selection
|
||||
* happened because the user picked an element using the element picker or
|
||||
* browser context menu.
|
||||
* Maybe focus the current node selection's MarkupContainer depending on why
|
||||
* the current node got selected.
|
||||
*/
|
||||
maybeFocusNewSelection: function() {
|
||||
let {reason, nodeFront} = this._inspector.selection;
|
||||
|
||||
if (reason !== "browser-context-menu" &&
|
||||
reason !== "picker-node-picked") {
|
||||
return;
|
||||
}
|
||||
// The list of reasons that should lead to focusing the node.
|
||||
let reasonsToFocus = [
|
||||
// If the user picked an element with the element picker.
|
||||
"picker-node-picked",
|
||||
// If the user selected an element with the browser context menu.
|
||||
"browser-context-menu",
|
||||
// If the user added a new node by clicking in the inspector toolbar.
|
||||
"node-inserted"
|
||||
];
|
||||
|
||||
this.getContainer(nodeFront).focus();
|
||||
if (reasonsToFocus.includes(reason)) {
|
||||
this.getContainer(nodeFront).focus();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -436,6 +436,12 @@ Rule.prototype = {
|
|||
let props = parseDeclarations(this.style.authoredText, true);
|
||||
for (let prop of props) {
|
||||
let name = prop.name;
|
||||
// If the authored text has an invalid property, it will show up
|
||||
// as nameless. Skip these as we don't currently have a good
|
||||
// way to display them.
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
// In an inherited rule, we only show inherited properties.
|
||||
// However, we must keep all properties in order for rule
|
||||
// rewriting to work properly. So, compute the "invisible"
|
||||
|
|
|
@ -121,6 +121,7 @@ skip-if = os == "mac" # Bug 1245996 : click on scrollbar not working on OSX
|
|||
[browser_rules_edit-selector_05.js]
|
||||
[browser_rules_edit-selector_06.js]
|
||||
[browser_rules_edit-selector_07.js]
|
||||
[browser_rules_edit-selector_08.js]
|
||||
[browser_rules_editable-field-focus_01.js]
|
||||
[browser_rules_editable-field-focus_02.js]
|
||||
[browser_rules_eyedropper.js]
|
||||
|
@ -133,6 +134,7 @@ skip-if = (os == "win" && debug) # bug 963492: win.
|
|||
[browser_rules_inherited-properties_02.js]
|
||||
[browser_rules_inherited-properties_03.js]
|
||||
[browser_rules_inline-source-map.js]
|
||||
[browser_rules_invalid.js]
|
||||
[browser_rules_invalid-source-map.js]
|
||||
[browser_rules_keybindings.js]
|
||||
[browser_rules_keyframes-rule_01.js]
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/* 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 reverting a selector edit does the right thing.
|
||||
// Bug 1241046.
|
||||
|
||||
const TEST_URI = `
|
||||
<style type="text/css">
|
||||
span {
|
||||
color: chartreuse;
|
||||
}
|
||||
</style>
|
||||
<span>
|
||||
<div id="testid" class="testclass">Styled Node</div>
|
||||
</span>
|
||||
`;
|
||||
|
||||
add_task(function* () {
|
||||
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
|
||||
let {inspector, view} = yield openRuleView();
|
||||
|
||||
info("Selecting the test element");
|
||||
yield selectNode("#testid", inspector);
|
||||
|
||||
let idRuleEditor = getRuleViewRuleEditor(view, 2);
|
||||
|
||||
info("Focusing an existing selector name in the rule-view");
|
||||
let editor = yield focusEditableField(view, idRuleEditor.selectorText);
|
||||
|
||||
is(inplaceEditor(idRuleEditor.selectorText), editor,
|
||||
"The selector editor got focused");
|
||||
|
||||
info("Entering a new selector name and committing");
|
||||
editor.input.value = "pre";
|
||||
|
||||
info("Waiting for rule view to update");
|
||||
let onRuleViewChanged = once(view, "ruleview-changed");
|
||||
|
||||
info("Entering the commit key");
|
||||
EventUtils.synthesizeKey("VK_RETURN", {});
|
||||
yield onRuleViewChanged;
|
||||
|
||||
info("Re-focusing the selector name in the rule-view");
|
||||
idRuleEditor = getRuleViewRuleEditor(view, 2);
|
||||
editor = yield focusEditableField(view, idRuleEditor.selectorText);
|
||||
|
||||
is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
|
||||
ok(getRuleViewRule(view, "pre"), "Rule with pre selector exists.");
|
||||
is(getRuleViewRuleEditor(view, 2).element.getAttribute("unmatched"),
|
||||
"true",
|
||||
"Rule with pre does not match the current element.");
|
||||
|
||||
// Now change it back.
|
||||
info("Re-entering original selector name and committing");
|
||||
editor.input.value = "span";
|
||||
|
||||
info("Waiting for rule view to update");
|
||||
onRuleViewChanged = once(view, "ruleview-changed");
|
||||
|
||||
info("Entering the commit key");
|
||||
EventUtils.synthesizeKey("VK_RETURN", {});
|
||||
yield onRuleViewChanged;
|
||||
|
||||
is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
|
||||
ok(getRuleViewRule(view, "span"), "Rule with span selector exists.");
|
||||
is(getRuleViewRuleEditor(view, 2).element.getAttribute("unmatched"),
|
||||
"false", "Rule with span matches the current element.");
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/* 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 an invalid property still lets us display the rule view
|
||||
// Bug 1235603.
|
||||
|
||||
const TEST_URI = `
|
||||
<style>
|
||||
div {
|
||||
background: #fff;
|
||||
font-family: sans-serif;
|
||||
url(display-table.min.htc);
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div id="testid" class="testclass">Styled Node</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
add_task(function* () {
|
||||
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
|
||||
|
||||
let {inspector, view} = yield openRuleView();
|
||||
yield selectNode("#testid", inspector);
|
||||
|
||||
is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
|
||||
// Have to actually get the rule in order to ensure that the
|
||||
// elements were created.
|
||||
ok(getRuleViewRule(view, "div"), "Rule with div selector exists");
|
||||
});
|
|
@ -4,12 +4,12 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
// Test that adding nodes does work as expected: the parent gets expanded and
|
||||
// the new node gets selected.
|
||||
// Test that adding nodes does work as expected: the parent gets expanded, the
|
||||
// new node gets selected and the corresponding markup-container focused.
|
||||
|
||||
const TEST_URL = URL_ROOT + "doc_inspector_add_node.html";
|
||||
|
||||
add_task(function*() {
|
||||
add_task(function* () {
|
||||
let {inspector} = yield openInspectorForURL(TEST_URL);
|
||||
|
||||
info("Adding in element that has no children and is collapsed");
|
||||
|
@ -37,13 +37,14 @@ add_task(function*() {
|
|||
|
||||
function* testAddNode(parentNode, inspector) {
|
||||
let btn = inspector.panelDoc.querySelector("#inspector-element-add-button");
|
||||
let markupWindow = inspector.markup.win;
|
||||
|
||||
info("Clicking on the 'add node' button and expecting a markup mutation");
|
||||
info("Clicking 'add node' and expecting a markup mutation and focus event");
|
||||
let onMutation = inspector.once("markupmutation");
|
||||
btn.click();
|
||||
let mutations = yield onMutation;
|
||||
|
||||
info("Expecting an inspector-updated event right after the mutation event "+
|
||||
info("Expecting an inspector-updated event right after the mutation event " +
|
||||
"to wait for the new node selection");
|
||||
yield inspector.once("inspector-updated");
|
||||
|
||||
|
@ -60,6 +61,13 @@ function* testAddNode(parentNode, inspector) {
|
|||
|
||||
is(inspector.selection.nodeFront.parentNode(), parentNode,
|
||||
"The new node is inside the right parent");
|
||||
|
||||
let focusedElement = markupWindow.document.activeElement;
|
||||
let focusedContainer = focusedElement.closest(".child").container;
|
||||
is(focusedContainer.node, inspector.selection.nodeFront,
|
||||
"The right container is focused in the markup-view");
|
||||
ok(focusedElement.classList.contains("tag"),
|
||||
"The tagName part of the container is focused");
|
||||
}
|
||||
|
||||
function collapseNode(node, inspector) {
|
||||
|
|
|
@ -16,6 +16,7 @@ addonDebugging.moreInfo = more info
|
|||
loadTemporaryAddon = Load Temporary Add-on
|
||||
extensions = Extensions
|
||||
selectAddonFromFile2 = Select Manifest File or Package (.xpi)
|
||||
reload = Reload
|
||||
|
||||
workers = Workers
|
||||
serviceWorkers = Service Workers
|
||||
|
|
|
@ -27,6 +27,10 @@ memory.tooltip=Memory
|
|||
# snapshot to disk.
|
||||
snapshot.io.save=Save
|
||||
|
||||
# LOCALIZATION NOTE (snapshot.io.delete): The label for the link that deletes
|
||||
# a snapshot
|
||||
snapshot.io.delete=Delete
|
||||
|
||||
# LOCALIZATION NOTE (snapshot.io.save.window): The title for the window
|
||||
# displayed when saving a snapshot to disk.
|
||||
snapshot.io.save.window=Save Heap Snapshot
|
||||
|
|
|
@ -482,6 +482,9 @@ const refreshSelectedCensus = exports.refreshSelectedCensus = function (heapWork
|
|||
const refreshSelectedTreeMap = exports.refreshSelectedTreeMap = function (heapWorker) {
|
||||
return function*(dispatch, getState) {
|
||||
let snapshot = getState().snapshots.find(s => s.selected);
|
||||
if (!snapshot || snapshot.state !== states.READ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Intermediate snapshot states will get handled by the task action that is
|
||||
// orchestrating them. For example, if the snapshot census's state is
|
||||
|
@ -489,8 +492,7 @@ const refreshSelectedTreeMap = exports.refreshSelectedTreeMap = function (heapWo
|
|||
// the inverted property matches the inverted state. If the snapshot is
|
||||
// still in the process of being saved or read, the takeSnapshotAndCensus
|
||||
// task action will follow through and ensure that a census is taken.
|
||||
if (snapshot &&
|
||||
(snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) ||
|
||||
if ((snapshot.treeMap && snapshot.treeMap.state === treeMapState.SAVED) ||
|
||||
!snapshot.treeMap) {
|
||||
yield dispatch(takeTreeMap(heapWorker, snapshot.id));
|
||||
}
|
||||
|
@ -761,6 +763,27 @@ const clearSnapshots = exports.clearSnapshots = function (heapWorker) {
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a snapshot
|
||||
*
|
||||
* @param {HeapAnalysesClient} heapWorker
|
||||
* @param {snapshotModel} snapshot
|
||||
*/
|
||||
const deleteSnapshot = exports.deleteSnapshot = function (heapWorker, snapshot) {
|
||||
return function*(dispatch, getState) {
|
||||
dispatch({ type: actions.DELETE_SNAPSHOTS_START, ids: [snapshot.id] });
|
||||
|
||||
try {
|
||||
yield heapWorker.deleteHeapSnapshot(snapshot.path);
|
||||
} catch (error) {
|
||||
reportException("deleteSnapshot", error);
|
||||
dispatch({ type: actions.SNAPSHOT_ERROR, id: snapshot.id, error });
|
||||
}
|
||||
|
||||
dispatch({ type: actions.DELETE_SNAPSHOTS_END, ids: [snapshot.id] });
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Expand the given node in the snapshot's census report.
|
||||
*
|
||||
|
|
|
@ -30,6 +30,7 @@ const {
|
|||
selectSnapshotAndRefresh,
|
||||
takeSnapshotAndCensus,
|
||||
clearSnapshots,
|
||||
deleteSnapshot,
|
||||
fetchImmediatelyDominated,
|
||||
expandCensusNode,
|
||||
collapseCensusNode,
|
||||
|
@ -210,6 +211,7 @@ const MemoryApp = createClass({
|
|||
itemComponent: SnapshotListItem,
|
||||
items: snapshots,
|
||||
onSave: snapshot => dispatch(pickFileAndExportSnapshot(snapshot)),
|
||||
onDelete: snapshot => dispatch(deleteSnapshot(heapWorker, snapshot)),
|
||||
onClick: onClickSnapshotListItem,
|
||||
diffing,
|
||||
}),
|
||||
|
|
|
@ -26,12 +26,13 @@ const SnapshotListItem = module.exports = createClass({
|
|||
propTypes: {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
item: snapshotModel.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
let { index, item: snapshot, onClick, onSave, diffing } = this.props;
|
||||
let { index, item: snapshot, onClick, onSave, onDelete, diffing } = this.props;
|
||||
let className = `snapshot-list-item ${snapshot.selected ? " selected" : ""}`;
|
||||
let statusText = getStatusText(snapshot.state);
|
||||
let wantThrobber = !!statusText;
|
||||
|
@ -88,13 +89,20 @@ const SnapshotListItem = module.exports = createClass({
|
|||
let saveLink = !snapshot.path ? void 0 : dom.a({
|
||||
onClick: () => onSave(snapshot),
|
||||
className: "save",
|
||||
}, L10N.getFormatStr("snapshot.io.save"));
|
||||
}, L10N.getStr("snapshot.io.save"));
|
||||
|
||||
let deleteButton = !snapshot.path ? void 0 : dom.button({
|
||||
onClick: () => onDelete(snapshot),
|
||||
className: "devtools-button delete",
|
||||
title: L10N.getStr("snapshot.io.delete")
|
||||
});
|
||||
|
||||
return (
|
||||
dom.li({ className, onClick },
|
||||
dom.span({ className: `snapshot-title ${wantThrobber ? " devtools-throbber" : ""}` },
|
||||
checkbox,
|
||||
title
|
||||
title,
|
||||
deleteButton
|
||||
),
|
||||
dom.span({ className: "snapshot-info" },
|
||||
details,
|
||||
|
|
|
@ -12,7 +12,9 @@ support-files =
|
|||
[test_Heap_03.html]
|
||||
[test_Heap_04.html]
|
||||
[test_Heap_05.html]
|
||||
[test_List_01.html]
|
||||
[test_ShortestPaths_01.html]
|
||||
[test_ShortestPaths_02.html]
|
||||
[test_SnapshotListItem_01.html]
|
||||
[test_Toolbar_01.html]
|
||||
[test_TreeMap_01.html]
|
||||
|
|
|
@ -53,6 +53,8 @@ var DominatorTreeComponent = React.createFactory(require("devtools/client/memory
|
|||
var DominatorTreeItem = React.createFactory(require("devtools/client/memory/components/dominator-tree-item"));
|
||||
var ShortestPaths = React.createFactory(require("devtools/client/memory/components/shortest-paths"));
|
||||
var TreeMap = React.createFactory(require("devtools/client/memory/components/tree-map"));
|
||||
var SnapshotListItem = React.createFactory(require("devtools/client/memory/components/snapshot-list-item"));
|
||||
var List = React.createFactory(require("devtools/client/memory/components/list"));
|
||||
var Toolbar = React.createFactory(require("devtools/client/memory/components/toolbar"));
|
||||
|
||||
// All tests are asynchronous.
|
||||
|
@ -163,6 +165,43 @@ var TEST_SHORTEST_PATHS_PROPS = Object.freeze({
|
|||
}),
|
||||
});
|
||||
|
||||
var TEST_SNAPSHOT = Object.freeze({
|
||||
id: 1337,
|
||||
selected: true,
|
||||
path: "/fake/path/to/snapshot",
|
||||
census: Object.freeze({
|
||||
report: Object.freeze({
|
||||
objects: Object.freeze({ count: 4, bytes: 400 }),
|
||||
scripts: Object.freeze({ count: 3, bytes: 300 }),
|
||||
strings: Object.freeze({ count: 2, bytes: 200 }),
|
||||
other: Object.freeze({ count: 1, bytes: 100 }),
|
||||
}),
|
||||
display: Object.freeze({
|
||||
displayName: "Test Display",
|
||||
tooltip: "Test display tooltup",
|
||||
inverted: false,
|
||||
breakdown: Object.freeze({
|
||||
by: "coarseType",
|
||||
objects: Object.freeze({ by: "count", count: true, bytes: true }),
|
||||
scripts: Object.freeze({ by: "count", count: true, bytes: true }),
|
||||
strings: Object.freeze({ by: "count", count: true, bytes: true }),
|
||||
other: Object.freeze({ by: "count", count: true, bytes: true }),
|
||||
}),
|
||||
}),
|
||||
state: censusState.SAVED,
|
||||
inverted: false,
|
||||
filter: null,
|
||||
expanded: new Set(),
|
||||
focused: null,
|
||||
parentMap: Object.freeze(Object.create(null))
|
||||
}),
|
||||
dominatorTree: TEST_DOMINATOR_TREE,
|
||||
error: null,
|
||||
imported: false,
|
||||
creationTime: 0,
|
||||
state: snapshotState.READ,
|
||||
});
|
||||
|
||||
var TEST_HEAP_PROPS = Object.freeze({
|
||||
onSnapshotClick: noop,
|
||||
onLoadMoreSiblings: noop,
|
||||
|
@ -175,42 +214,7 @@ var TEST_HEAP_PROPS = Object.freeze({
|
|||
onViewSourceInDebugger: noop,
|
||||
diffing: null,
|
||||
view: { state: viewState.CENSUS, },
|
||||
snapshot: Object.freeze({
|
||||
id: 1337,
|
||||
selected: true,
|
||||
path: "/fake/path/to/snapshot",
|
||||
census: Object.freeze({
|
||||
report: Object.freeze({
|
||||
objects: Object.freeze({ count: 4, bytes: 400 }),
|
||||
scripts: Object.freeze({ count: 3, bytes: 300 }),
|
||||
strings: Object.freeze({ count: 2, bytes: 200 }),
|
||||
other: Object.freeze({ count: 1, bytes: 100 }),
|
||||
}),
|
||||
display: Object.freeze({
|
||||
displayName: "Test Display",
|
||||
tooltip: "Test display tooltup",
|
||||
inverted: false,
|
||||
breakdown: Object.freeze({
|
||||
by: "coarseType",
|
||||
objects: Object.freeze({ by: "count", count: true, bytes: true }),
|
||||
scripts: Object.freeze({ by: "count", count: true, bytes: true }),
|
||||
strings: Object.freeze({ by: "count", count: true, bytes: true }),
|
||||
other: Object.freeze({ by: "count", count: true, bytes: true }),
|
||||
}),
|
||||
}),
|
||||
state: censusState.SAVED,
|
||||
inverted: false,
|
||||
filter: null,
|
||||
expanded: new Set(),
|
||||
focused: null,
|
||||
parentMap: Object.freeze(Object.create(null))
|
||||
}),
|
||||
dominatorTree: TEST_DOMINATOR_TREE,
|
||||
error: null,
|
||||
imported: false,
|
||||
creationTime: 0,
|
||||
state: snapshotState.READ,
|
||||
}),
|
||||
snapshot: TEST_SNAPSHOT,
|
||||
sizes: Object.freeze({ shortestPathsSize: .5 }),
|
||||
onShortestPathsResize: noop,
|
||||
});
|
||||
|
@ -285,6 +289,14 @@ var TEST_TREE_MAP_PROPS = Object.freeze({
|
|||
})
|
||||
});
|
||||
|
||||
var TEST_SNAPSHOT_LIST_ITEM_PROPS = Object.freeze({
|
||||
onClick: noop,
|
||||
onSave: noop,
|
||||
onDelete: noop,
|
||||
item: TEST_SNAPSHOT,
|
||||
index: 1234,
|
||||
});
|
||||
|
||||
function onNextAnimationFrame(fn) {
|
||||
return () =>
|
||||
requestAnimationFrame(() =>
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test to verify the delete button calls the onDelete handler for an item
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
const container = document.getElementById("container");
|
||||
|
||||
let deletedSnapshots = [];
|
||||
|
||||
let snapshots = [ TEST_SNAPSHOT, TEST_SNAPSHOT, TEST_SNAPSHOT ]
|
||||
.map((snapshot, index) => immutableUpdate(snapshot, {
|
||||
index: snapshot.index + index
|
||||
}));
|
||||
|
||||
yield renderComponent(
|
||||
List({
|
||||
itemComponent: SnapshotListItem,
|
||||
onClick: noop,
|
||||
onDelete: (item) => deletedSnapshots.push(item),
|
||||
items: snapshots
|
||||
}),
|
||||
container
|
||||
);
|
||||
|
||||
let deleteButtons = container.querySelectorAll('.delete');
|
||||
|
||||
is(container.querySelectorAll('.snapshot-list-item').length, 3,
|
||||
"There are 3 list items\n");
|
||||
is(deletedSnapshots.length, 0,
|
||||
"Not snapshots have been deleted\n");
|
||||
|
||||
deleteButtons[1].click();
|
||||
|
||||
is(deletedSnapshots.length, 1, "One snapshot was deleted\n");
|
||||
is(deletedSnapshots[0], snapshots[1],
|
||||
"Deleted snapshot was added to the deleted list\n");
|
||||
|
||||
deleteButtons[0].click();
|
||||
|
||||
is(deletedSnapshots.length, 2, "Two snapshots were deleted\n");
|
||||
is(deletedSnapshots[1], snapshots[0],
|
||||
"Deleted snapshot was added to the deleted list\n");
|
||||
|
||||
deleteButtons[2].click();
|
||||
|
||||
is(deletedSnapshots.length, 3, "Three snapshots were deleted\n");
|
||||
is(deletedSnapshots[2], snapshots[2],
|
||||
"Deleted snapshot was added to the deleted list\n");
|
||||
|
||||
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,53 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test to verify that the delete button only shows up for a snapshot when it has a
|
||||
path.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
const container = document.getElementById("container");
|
||||
|
||||
yield renderComponent(
|
||||
SnapshotListItem(TEST_SNAPSHOT_LIST_ITEM_PROPS),
|
||||
container
|
||||
);
|
||||
|
||||
ok(container.querySelector('.delete'),
|
||||
"Should have delete button when there is a path");
|
||||
|
||||
const pathlessProps = immutableUpdate(
|
||||
TEST_SNAPSHOT_LIST_ITEM_PROPS,
|
||||
{item: immutableUpdate(TEST_SNAPSHOT, {path: null})}
|
||||
);
|
||||
|
||||
yield renderComponent(
|
||||
SnapshotListItem(pathlessProps),
|
||||
container
|
||||
);
|
||||
|
||||
ok(!container.querySelector('.delete'),
|
||||
"No delete button should be found if there is no path\n");
|
||||
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -5,6 +5,7 @@ support-files =
|
|||
storage-cache-error.html
|
||||
storage-complex-values.html
|
||||
storage-cookies.html
|
||||
storage-empty-objectstores.html
|
||||
storage-listings.html
|
||||
storage-localstorage.html
|
||||
storage-overflow.html
|
||||
|
@ -21,11 +22,12 @@ support-files =
|
|||
[browser_storage_cookies_edit.js]
|
||||
[browser_storage_cookies_edit_keyboard.js]
|
||||
[browser_storage_cookies_tab_navigation.js]
|
||||
[browser_storage_dynamic_updates.js]
|
||||
[browser_storage_localstorage_edit.js]
|
||||
[browser_storage_delete.js]
|
||||
[browser_storage_delete_all.js]
|
||||
[browser_storage_delete_tree.js]
|
||||
[browser_storage_dynamic_updates.js]
|
||||
[browser_storage_empty_objectstores.js]
|
||||
[browser_storage_localstorage_edit.js]
|
||||
[browser_storage_overflow.js]
|
||||
[browser_storage_search.js]
|
||||
skip-if = os == "linux" && e10s # Bug 1240804 - unhandled promise rejections
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// Basic test to assert that the storage tree and table corresponding to each
|
||||
// item in the storage tree is correctly displayed.
|
||||
|
||||
"use strict";
|
||||
|
||||
// Entries that should be present in the tree for this test
|
||||
// Format for each entry in the array:
|
||||
// [
|
||||
// ["path", "to", "tree", "item"],
|
||||
// - The path to the tree item to click formed by id of each item
|
||||
// ["key_value1", "key_value2", ...]
|
||||
// - The value of the first (unique) column for each row in the table
|
||||
// corresponding to the tree item selected.
|
||||
// ]
|
||||
// These entries are formed by the cookies, local storage, session storage and
|
||||
// indexedDB entries created in storage-listings.html,
|
||||
// storage-secured-iframe.html and storage-unsecured-iframe.html
|
||||
const storeItems = [
|
||||
[["indexedDB", "http://test1.example.org"],
|
||||
["idb1", "idb2"]],
|
||||
[["indexedDB", "http://test1.example.org", "idb1"],
|
||||
["obj1", "obj2"]],
|
||||
[["indexedDB", "http://test1.example.org", "idb2"],
|
||||
[]],
|
||||
[["indexedDB", "http://test1.example.org", "idb1", "obj1"],
|
||||
[1, 2, 3]],
|
||||
[["indexedDB", "http://test1.example.org", "idb1", "obj2"],
|
||||
[1]]
|
||||
];
|
||||
|
||||
/**
|
||||
* Test that the desired number of tree items are present
|
||||
*/
|
||||
function testTree() {
|
||||
let doc = gPanelWindow.document;
|
||||
for (let [item] of storeItems) {
|
||||
ok(doc.querySelector(`[data-id='${JSON.stringify(item)}']`),
|
||||
`Tree item ${item} should be present in the storage tree`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that correct table entries are shown for each of the tree item
|
||||
*/
|
||||
let testTables = function* () {
|
||||
let doc = gPanelWindow.document;
|
||||
// Expand all nodes so that the synthesized click event actually works
|
||||
gUI.tree.expandAll();
|
||||
|
||||
// Click the tree items and wait for the table to be updated
|
||||
for (let [item, ids] of storeItems) {
|
||||
yield selectTreeItem(item);
|
||||
|
||||
// Check whether correct number of items are present in the table
|
||||
is(doc.querySelectorAll(
|
||||
".table-widget-wrapper:first-of-type .table-widget-cell"
|
||||
).length, ids.length, "Number of items in table is correct");
|
||||
|
||||
// Check if all the desired items are present in the table
|
||||
for (let id of ids) {
|
||||
ok(doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
|
||||
`Table item ${id} should be present`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
add_task(function* () {
|
||||
yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-empty-objectstores.html");
|
||||
|
||||
testTree();
|
||||
yield testTables();
|
||||
yield finishTests();
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test for proper listing indexedDB databases with no object stores</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="application/javascript;version=1.7">
|
||||
|
||||
window.setup = function* () {
|
||||
let request = indexedDB.open("idb1", 1);
|
||||
let db = yield new Promise((resolve, reject) => {
|
||||
request.onerror = e => reject(Error("error opening db connection"));
|
||||
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 = () => resolve(db);
|
||||
};
|
||||
});
|
||||
|
||||
yield new Promise(resolve => request.onsuccess = resolve);
|
||||
|
||||
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"});
|
||||
|
||||
yield new Promise(resolve => transaction.oncomplete = resolve);
|
||||
|
||||
db.close();
|
||||
|
||||
request = indexedDB.open("idb2", 1);
|
||||
let db2 = yield new Promise((resolve, reject) => {
|
||||
request.onerror = e => reject(Error("error opening db2 connection"));
|
||||
request.onupgradeneeded = event => resolve(event.target.result);
|
||||
});
|
||||
|
||||
yield new Promise(resolve => request.onsuccess = resolve);
|
||||
|
||||
db2.close();
|
||||
dump("added indexedDB items from main page\n");
|
||||
};
|
||||
|
||||
window.clear = function* () {
|
||||
for (let dbName of ["idb1", "idb2"]) {
|
||||
yield new Promise(resolve => {
|
||||
indexedDB.deleteDatabase(dbName).onsuccess = resolve;
|
||||
});
|
||||
}
|
||||
dump("removed indexedDB items from main page\n");
|
||||
};
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -197,11 +197,32 @@ html, body, #app, #memory-tool {
|
|||
font-size: 90%;
|
||||
}
|
||||
|
||||
.snapshot-list-item .snapshot-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.snapshot-list-item .save {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.snapshot-list-item .delete {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
min-height: 1em;
|
||||
min-width: 1.3em;
|
||||
}
|
||||
|
||||
.theme-light .snapshot-list-item.selected .delete {
|
||||
filter: invert(100%);
|
||||
}
|
||||
|
||||
.snapshot-list-item .delete::before {
|
||||
background-image: url("chrome://devtools/skin/images/close.svg");
|
||||
background-position: 0.2em 0;
|
||||
}
|
||||
|
||||
.snapshot-list-item > .snapshot-title {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
|
|
@ -83,6 +83,7 @@ BrowserAddonActor.prototype = {
|
|||
id: this.id,
|
||||
name: this._addon.name,
|
||||
url: this.url,
|
||||
iconURL: this._addon.iconURL,
|
||||
debuggable: this._addon.isDebuggable,
|
||||
consoleActor: this._consoleActor.actorID,
|
||||
|
||||
|
@ -155,6 +156,13 @@ BrowserAddonActor.prototype = {
|
|||
return { type: "detached" };
|
||||
},
|
||||
|
||||
onReload: function BAA_onReload() {
|
||||
return this._addon.reload()
|
||||
.then(() => {
|
||||
return {}; // send an empty response
|
||||
});
|
||||
},
|
||||
|
||||
preNest: function() {
|
||||
let e = Services.wm.getEnumerator(null);
|
||||
while (e.hasMoreElements()) {
|
||||
|
@ -249,7 +257,8 @@ BrowserAddonActor.prototype = {
|
|||
|
||||
BrowserAddonActor.prototype.requestTypes = {
|
||||
"attach": BrowserAddonActor.prototype.onAttach,
|
||||
"detach": BrowserAddonActor.prototype.onDetach
|
||||
"detach": BrowserAddonActor.prototype.onDetach,
|
||||
"reload": BrowserAddonActor.prototype.onReload
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -1536,9 +1536,13 @@ StorageActors.createActor({
|
|||
getNamesForHost: function(host) {
|
||||
let names = [];
|
||||
|
||||
for (let [dbName, metaData] of this.hostVsStores.get(host)) {
|
||||
for (let objectStore of metaData.objectStores.keys()) {
|
||||
names.push(JSON.stringify([dbName, objectStore]));
|
||||
for (let [dbName, {objectStores}] of this.hostVsStores.get(host)) {
|
||||
if (objectStores.size) {
|
||||
for (let objectStore of objectStores.keys()) {
|
||||
names.push(JSON.stringify([dbName, objectStore]));
|
||||
}
|
||||
} else {
|
||||
names.push(JSON.stringify([dbName]));
|
||||
}
|
||||
}
|
||||
return names;
|
||||
|
@ -1634,7 +1638,7 @@ StorageActors.createActor({
|
|||
return null;
|
||||
}
|
||||
|
||||
if (item.indexes) {
|
||||
if ("indexes" in item) {
|
||||
// Object store meta data
|
||||
return {
|
||||
objectStore: item.name,
|
||||
|
@ -1643,7 +1647,7 @@ StorageActors.createActor({
|
|||
indexes: item.indexes
|
||||
};
|
||||
}
|
||||
if (item.objectStores) {
|
||||
if ("objectStores" in item) {
|
||||
// DB meta data
|
||||
return {
|
||||
db: item.name,
|
||||
|
@ -2240,7 +2244,8 @@ var StorageActor = exports.StorageActor = protocol.ActorClass({
|
|||
let origin = win.document
|
||||
.nodePrincipal
|
||||
.originNoSuffix;
|
||||
if (origin === host) {
|
||||
let url = win.document.URL;
|
||||
if (origin === host || url === host) {
|
||||
return win;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -742,6 +742,30 @@ var PageStyleActor = protocol.ActorClass({
|
|||
return rules;
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a node and a CSS rule, walk up the DOM looking for a
|
||||
* matching element rule. Return an array of all found entries, in
|
||||
* the form generated by _getAllElementRules. Note that this will
|
||||
* always return an array of either zero or one element.
|
||||
*
|
||||
* @param {NodeActor} node the node
|
||||
* @param {CSSStyleRule} filterRule the rule to filter for
|
||||
* @return {Array} array of zero or one elements; if one, the element
|
||||
* is the entry as returned by _getAllElementRules.
|
||||
*/
|
||||
findEntryMatchingRule: function(node, filterRule) {
|
||||
const options = {matchedSelectors: true, inherited: true};
|
||||
let entries = [];
|
||||
let parent = this.walker.parentNode(node);
|
||||
while (parent && parent.rawNode.nodeType != Ci.nsIDOMNode.DOCUMENT_NODE) {
|
||||
entries = entries.concat(this._getAllElementRules(parent, parent,
|
||||
options));
|
||||
parent = this.walker.parentNode(parent);
|
||||
}
|
||||
|
||||
return entries.filter(entry => entry.rule.rawRule === filterRule);
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper function for getApplied that fetches a set of style properties that
|
||||
* apply to the given node and associated rules
|
||||
|
@ -752,7 +776,7 @@ var PageStyleActor = protocol.ActorClass({
|
|||
* 'ua': Include properties from user and user-agent sheets.
|
||||
* Default value is 'ua'
|
||||
* `inherited`: Include styles inherited from parent nodes.
|
||||
* `matchedSeletors`: Include an array of specific selectors that
|
||||
* `matchedSelectors`: Include an array of specific selectors that
|
||||
* caused this rule to match its node.
|
||||
* @param array entries
|
||||
* List of appliedstyle objects that lists the rules that apply to the
|
||||
|
@ -970,7 +994,7 @@ var PageStyleActor = protocol.ActorClass({
|
|||
* Helper function for adding a new rule and getting its applied style
|
||||
* properties
|
||||
* @param NodeActor node
|
||||
* @param CSSStyleRUle rule
|
||||
* @param CSSStyleRule rule
|
||||
* @returns Object containing its applied style properties
|
||||
*/
|
||||
getNewAppliedProps: function(node, rule) {
|
||||
|
@ -1626,11 +1650,9 @@ var StyleRuleActor = protocol.ActorClass({
|
|||
* matches the current selected element
|
||||
*/
|
||||
modifySelector2: method(function(node, value, editAuthored = false) {
|
||||
let ruleProps = null;
|
||||
|
||||
if (this.type === ELEMENT_STYLE ||
|
||||
this.rawRule.selectorText === value) {
|
||||
return { ruleProps, isMatching: true };
|
||||
return { ruleProps: null, isMatching: true };
|
||||
}
|
||||
|
||||
let selectorPromise = this._addNewSelector(value, editAuthored);
|
||||
|
@ -1647,17 +1669,19 @@ var StyleRuleActor = protocol.ActorClass({
|
|||
}
|
||||
|
||||
return selectorPromise.then((newCssRule) => {
|
||||
if (newCssRule) {
|
||||
ruleProps = this.pageStyle.getNewAppliedProps(node, newCssRule);
|
||||
}
|
||||
|
||||
// Determine if the new selector value matches the current
|
||||
// selected element
|
||||
let ruleProps = null;
|
||||
let isMatching = false;
|
||||
try {
|
||||
isMatching = node.rawNode.matches(value);
|
||||
} catch (e) {
|
||||
// This fails when value is an invalid selector.
|
||||
|
||||
if (newCssRule) {
|
||||
let ruleEntry = this.pageStyle.findEntryMatchingRule(node, newCssRule);
|
||||
if (ruleEntry.length === 1) {
|
||||
isMatching = true;
|
||||
ruleProps =
|
||||
this.pageStyle.getAppliedProps(node, ruleEntry,
|
||||
{ matchedSelectors: true });
|
||||
} else {
|
||||
ruleProps = this.pageStyle.getNewAppliedProps(node, newCssRule);
|
||||
}
|
||||
}
|
||||
|
||||
return { ruleProps, isMatching };
|
||||
|
|
|
@ -3459,8 +3459,15 @@ public class BrowserApp extends GeckoApp
|
|||
if (itemId == R.id.bookmark) {
|
||||
tab = Tabs.getInstance().getSelectedTab();
|
||||
if (tab != null) {
|
||||
final String extra;
|
||||
if (AboutPages.isAboutReader(tab.getURL())) {
|
||||
extra = "bookmark_reader";
|
||||
} else {
|
||||
extra = "bookmark";
|
||||
}
|
||||
|
||||
if (item.isChecked()) {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.MENU, "bookmark");
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.MENU, extra);
|
||||
tab.removeBookmark();
|
||||
item.setTitle(resolveBookmarkTitleID(false));
|
||||
if (Versions.feature11Plus) {
|
||||
|
@ -3468,7 +3475,7 @@ public class BrowserApp extends GeckoApp
|
|||
item.setIcon(resolveBookmarkIconID(false));
|
||||
}
|
||||
} else {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, "bookmark");
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, extra);
|
||||
tab.addBookmark();
|
||||
item.setTitle(resolveBookmarkTitleID(true));
|
||||
if (Versions.feature11Plus) {
|
||||
|
|
|
@ -380,11 +380,20 @@ public abstract class HomeFragment extends Fragment {
|
|||
|
||||
switch (mType) {
|
||||
case BOOKMARKS:
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.CONTEXT_MENU, "bookmark");
|
||||
SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(mContext);
|
||||
final boolean isReaderViewPage = rch.isURLCached(mUrl);
|
||||
|
||||
final String extra;
|
||||
if (isReaderViewPage) {
|
||||
extra = "bookmark_reader";
|
||||
} else {
|
||||
extra = "bookmark";
|
||||
}
|
||||
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.CONTEXT_MENU, extra);
|
||||
mDB.removeBookmarksWithURL(cr, mUrl);
|
||||
|
||||
SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(mContext);
|
||||
if (rch.isURLCached(mUrl)) {
|
||||
if (isReaderViewPage) {
|
||||
ReadingListHelper.removeCachedReaderItem(mUrl, mContext);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
<LinearLayout android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="@dimen/firstrun_min_height"
|
||||
android:background="@color/about_page_header_grey"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
|
|
|
@ -207,4 +207,14 @@ this.Async = {
|
|||
query.executeAsync(storageCallback);
|
||||
return Async.waitForSyncCallback(storageCallback.syncCb);
|
||||
},
|
||||
|
||||
promiseSpinningly(promise) {
|
||||
let cb = Async.makeSpinningCallback();
|
||||
promise.then(result => {
|
||||
cb(null, result);
|
||||
}, err => {
|
||||
cb(err || new Error("Promise rejected without explicit error"));
|
||||
});
|
||||
return cb.wait();
|
||||
},
|
||||
};
|
||||
|
|
|
@ -12,8 +12,11 @@ Cu.importGlobalProperties(['fetch']);
|
|||
|
||||
const PREF_KINTO_CHANGES_PATH = "services.kinto.changes.path";
|
||||
const PREF_KINTO_BASE = "services.kinto.base";
|
||||
const PREF_KINTO_BUCKET = "services.kinto.bucket";
|
||||
const PREF_KINTO_LAST_UPDATE = "services.kinto.last_update_seconds";
|
||||
const PREF_KINTO_LAST_ETAG = "services.kinto.last_etag";
|
||||
const PREF_KINTO_CLOCK_SKEW_SECONDS = "services.kinto.clock_skew_seconds";
|
||||
const PREF_KINTO_ONECRL_COLLECTION = "services.kinto.onecrl.collection";
|
||||
|
||||
const kintoClients = {
|
||||
};
|
||||
|
@ -21,6 +24,7 @@ const kintoClients = {
|
|||
// This is called by the ping mechanism.
|
||||
// returns a promise that rejects if something goes wrong
|
||||
this.checkVersions = function() {
|
||||
|
||||
return Task.spawn(function *() {
|
||||
// Fetch a versionInfo object that looks like:
|
||||
// {"data":[
|
||||
|
@ -33,25 +37,52 @@ this.checkVersions = function() {
|
|||
// Right now, we only use the collection name and the last modified info
|
||||
let kintoBase = Services.prefs.getCharPref(PREF_KINTO_BASE);
|
||||
let changesEndpoint = kintoBase + Services.prefs.getCharPref(PREF_KINTO_CHANGES_PATH);
|
||||
let blocklistsBucket = Services.prefs.getCharPref(PREF_KINTO_BUCKET);
|
||||
|
||||
let response = yield fetch(changesEndpoint);
|
||||
// Use ETag to obtain a `304 Not modified` when no change occurred.
|
||||
const headers = {};
|
||||
if (Services.prefs.prefHasUserValue(PREF_KINTO_LAST_ETAG)) {
|
||||
const lastEtag = Services.prefs.getCharPref(PREF_KINTO_LAST_ETAG);
|
||||
if (lastEtag) {
|
||||
headers["If-None-Match"] = lastEtag;
|
||||
}
|
||||
}
|
||||
|
||||
let response = yield fetch(changesEndpoint, {headers});
|
||||
|
||||
let versionInfo;
|
||||
// No changes since last time. Go on with empty list of changes.
|
||||
if (response.status == 304) {
|
||||
versionInfo = {data: []};
|
||||
} else {
|
||||
versionInfo = yield response.json();
|
||||
}
|
||||
|
||||
// If the server is failing, the JSON response might not contain the
|
||||
// expected data (e.g. error response - Bug 1259145)
|
||||
if (!versionInfo.hasOwnProperty("data")) {
|
||||
throw new Error("Polling for changes failed.");
|
||||
}
|
||||
|
||||
// Record new update time and the difference between local and server time
|
||||
let serverTimeMillis = Date.parse(response.headers.get("Date"));
|
||||
let clockDifference = Math.abs(Date.now() - serverTimeMillis) / 1000;
|
||||
Services.prefs.setIntPref(PREF_KINTO_LAST_UPDATE, serverTimeMillis / 1000);
|
||||
Services.prefs.setIntPref(PREF_KINTO_CLOCK_SKEW_SECONDS, clockDifference);
|
||||
|
||||
let versionInfo = yield response.json();
|
||||
Services.prefs.setIntPref(PREF_KINTO_LAST_UPDATE, serverTimeMillis / 1000);
|
||||
|
||||
let firstError;
|
||||
for (let collectionInfo of versionInfo.data) {
|
||||
// Skip changes that don't concern configured blocklist bucket.
|
||||
if (collectionInfo.bucket != blocklistsBucket) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let collection = collectionInfo.collection;
|
||||
let kintoClient = kintoClients[collection];
|
||||
if (kintoClient && kintoClient.maybeSync) {
|
||||
let lastModified = 0;
|
||||
if (collectionInfo.last_modified) {
|
||||
lastModified = collectionInfo.last_modified
|
||||
lastModified = collectionInfo.last_modified;
|
||||
}
|
||||
try {
|
||||
yield kintoClient.maybeSync(lastModified, serverTimeMillis);
|
||||
|
@ -66,6 +97,12 @@ this.checkVersions = function() {
|
|||
// cause the promise to reject by throwing the first observed error
|
||||
throw firstError;
|
||||
}
|
||||
|
||||
// Save current Etag for next poll.
|
||||
if (response.headers.has("ETag")) {
|
||||
const currentEtag = response.headers.get("ETag");
|
||||
Services.prefs.setCharPref(PREF_KINTO_LAST_ETAG, currentEtag);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -75,6 +112,5 @@ this.addTestKintoClient = function(name, kintoClient) {
|
|||
};
|
||||
|
||||
// Add the various things that we know want updates
|
||||
kintoClients.certificates =
|
||||
Cu.import("resource://services-common/KintoCertificateBlocklist.js", {})
|
||||
.OneCRLClient;
|
||||
const KintoBlocklist = Cu.import("resource://services-common/KintoCertificateBlocklist.js", {});
|
||||
kintoClients[Services.prefs.getCharPref(PREF_KINTO_ONECRL_COLLECTION)] = KintoBlocklist.OneCRLClient;
|
||||
|
|
|
@ -8,6 +8,7 @@ var server;
|
|||
|
||||
const PREF_KINTO_BASE = "services.kinto.base";
|
||||
const PREF_LAST_UPDATE = "services.kinto.last_update_seconds";
|
||||
const PREF_LAST_ETAG = "services.kinto.last_etag";
|
||||
const PREF_CLOCK_SKEW_SECONDS = "services.kinto.clock_skew_seconds";
|
||||
|
||||
// Check to ensure maybeSync is called with correct values when a changes
|
||||
|
@ -31,7 +32,7 @@ add_task(function* test_check_maybeSync(){
|
|||
response.setHeader(headerElements[0], headerElements[1].trimLeft());
|
||||
}
|
||||
|
||||
// set the
|
||||
// set the server date
|
||||
response.setHeader("Date", (new Date(2000)).toUTCString());
|
||||
|
||||
response.write(sampled.responseBody);
|
||||
|
@ -47,19 +48,20 @@ add_task(function* test_check_maybeSync(){
|
|||
`http://localhost:${server.identity.primaryPort}/v1`);
|
||||
|
||||
// set some initial values so we can check these are updated appropriately
|
||||
Services.prefs.setIntPref("services.kinto.last_update", 0);
|
||||
Services.prefs.setIntPref("services.kinto.clock_difference", 0);
|
||||
Services.prefs.setIntPref(PREF_LAST_UPDATE, 0);
|
||||
Services.prefs.setIntPref(PREF_CLOCK_SKEW_SECONDS, 0);
|
||||
Services.prefs.clearUserPref(PREF_LAST_ETAG);
|
||||
|
||||
|
||||
let startTime = Date.now();
|
||||
|
||||
let updater = Cu.import("resource://services-common/kinto-updater.js");
|
||||
|
||||
let syncPromise = new Promise(function(resolve, reject) {
|
||||
let updater = Cu.import("resource://services-common/kinto-updater.js");
|
||||
// add a test kinto client that will respond to lastModified information
|
||||
// for a collection called 'test-collection'
|
||||
updater.addTestKintoClient("test-collection", {
|
||||
"maybeSync": function(lastModified, serverTime){
|
||||
// ensire the lastModified and serverTime values are as expected
|
||||
maybeSync(lastModified, serverTime) {
|
||||
do_check_eq(lastModified, 1000);
|
||||
do_check_eq(serverTime, 2000);
|
||||
resolve();
|
||||
|
@ -80,6 +82,44 @@ add_task(function* test_check_maybeSync(){
|
|||
// we previously set the serverTime to 2 (seconds past epoch)
|
||||
do_check_eq(clockDifference <= endTime / 1000
|
||||
&& clockDifference >= Math.floor(startTime / 1000) - 2, true);
|
||||
// Last timestamp was saved. An ETag header value is a quoted string.
|
||||
let lastEtag = Services.prefs.getCharPref(PREF_LAST_ETAG);
|
||||
do_check_eq(lastEtag, "\"1100\"");
|
||||
|
||||
|
||||
// Simulate a poll with up-to-date collection.
|
||||
Services.prefs.setIntPref(PREF_LAST_UPDATE, 0);
|
||||
// If server has no change, a 304 is received, maybeSync() is not called.
|
||||
updater.addTestKintoClient("test-collection", {
|
||||
maybeSync: () => {throw new Error("Should not be called");}
|
||||
});
|
||||
yield updater.checkVersions();
|
||||
// Last update is overwritten
|
||||
do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2);
|
||||
|
||||
|
||||
// Simulate a server error.
|
||||
function simulateErrorResponse (request, response) {
|
||||
response.setHeader("Date", (new Date(3000)).toUTCString());
|
||||
response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
||||
response.write(JSON.stringify({
|
||||
code: 503,
|
||||
errno: 999,
|
||||
error: "Service Unavailable",
|
||||
}));
|
||||
response.setStatusLine(null, 503, "Service Unavailable");
|
||||
}
|
||||
server.registerPathHandler(changesPath, simulateErrorResponse);
|
||||
// checkVersions() fails with adequate error.
|
||||
let error;
|
||||
try {
|
||||
yield updater.checkVersions();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
do_check_eq(error.message, "Polling for changes failed.");
|
||||
// When an error occurs, last update was not overwritten (see Date header above).
|
||||
do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2);
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
|
@ -99,12 +139,29 @@ function getSampleResponse(req, port) {
|
|||
const responses = {
|
||||
"GET:/v1/buckets/monitor/collections/changes/records?": {
|
||||
"sampleHeaders": [
|
||||
"Content-Type: application/json; charset=UTF-8"
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"ETag: \"1100\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data":[{"host":"localhost","last_modified":1000,"bucket":"blocklists","id":"330a0c5f-fadf-ff0b-40c8-4eb0d924ff6a","collection":"test-collection"}]})
|
||||
"responseBody": JSON.stringify({"data": [{
|
||||
"host": "localhost",
|
||||
"last_modified": 1100,
|
||||
"bucket": "blocklists:aurora",
|
||||
"id": "330a0c5f-fadf-ff0b-40c8-4eb0d924ff6a",
|
||||
"collection": "test-collection"
|
||||
}, {
|
||||
"host": "localhost",
|
||||
"last_modified": 1000,
|
||||
"bucket": "blocklists",
|
||||
"id": "254cbb9e-6888-4d9f-8e60-58b74faa8778",
|
||||
"collection": "test-collection"
|
||||
}]})
|
||||
}
|
||||
};
|
||||
|
||||
if (req.hasHeader("if-none-match") && req.getHeader("if-none-match", "") == "\"1100\"")
|
||||
return {sampleHeaders: [], status: {status: 304, statusText: "Not Modified"}, responseBody: ""};
|
||||
|
||||
return responses[`${req.method}:${req.path}?${req.queryString}`] ||
|
||||
responses[req.method];
|
||||
}
|
||||
|
|
|
@ -272,69 +272,69 @@ BookmarksEngine.prototype = {
|
|||
_guidMapFailed: false,
|
||||
_buildGUIDMap: function _buildGUIDMap() {
|
||||
let guidMap = {};
|
||||
for (let guid in this._store.getAllIDs()) {
|
||||
// Figure out with which key to store the mapping.
|
||||
let key;
|
||||
let id = this._store.idForGUID(guid);
|
||||
let itemType;
|
||||
try {
|
||||
itemType = PlacesUtils.bookmarks.getItemType(id);
|
||||
} catch (ex) {
|
||||
this._log.warn("Deleting invalid bookmark record with id", id);
|
||||
try {
|
||||
PlacesUtils.bookmarks.removeItem(id);
|
||||
} catch (ex) {
|
||||
this._log.warn("Failed to delete invalid bookmark", ex);
|
||||
let tree = Async.promiseSpinningly(PlacesUtils.promiseBookmarksTree("", {
|
||||
includeItemIds: true
|
||||
}));
|
||||
function* walkBookmarksTree(tree, parent=null) {
|
||||
if (tree) {
|
||||
// Skip root node
|
||||
if (parent) {
|
||||
yield [tree, parent];
|
||||
}
|
||||
if (tree.children) {
|
||||
for (let child of tree.children) {
|
||||
yield* walkBookmarksTree(child, tree);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
switch (itemType) {
|
||||
case PlacesUtils.bookmarks.TYPE_BOOKMARK:
|
||||
}
|
||||
|
||||
// Smart bookmarks map to their annotation value.
|
||||
let queryId;
|
||||
try {
|
||||
queryId = PlacesUtils.annotations.getItemAnnotation(
|
||||
id, SMART_BOOKMARKS_ANNO);
|
||||
} catch(ex) {}
|
||||
function* walkBookmarksRoots(tree, rootGUIDs) {
|
||||
for (let guid of rootGUIDs) {
|
||||
let id = kSpecialIds.specialIdForGUID(guid, false);
|
||||
let bookmarkRoot = id === null ? null :
|
||||
tree.children.find(child => child.id === id);
|
||||
if (bookmarkRoot === null) {
|
||||
continue;
|
||||
}
|
||||
yield* walkBookmarksTree(bookmarkRoot, tree);
|
||||
}
|
||||
}
|
||||
|
||||
if (queryId) {
|
||||
key = "q" + queryId;
|
||||
let rootsToWalk = kSpecialIds.guids.filter(guid =>
|
||||
guid !== 'places' && guid !== 'tags');
|
||||
|
||||
for (let [node, parent] of walkBookmarksRoots(tree, rootsToWalk)) {
|
||||
let {guid, id, type: placeType} = node;
|
||||
guid = kSpecialIds.specialGUIDForId(id) || guid;
|
||||
let key;
|
||||
switch (placeType) {
|
||||
case PlacesUtils.TYPE_X_MOZ_PLACE:
|
||||
// Bookmark
|
||||
let query = null;
|
||||
if (node.annos && node.uri.startsWith("place:")) {
|
||||
query = node.annos.find(({name}) => name === SMART_BOOKMARKS_ANNO);
|
||||
}
|
||||
if (query && query.value) {
|
||||
key = "q" + query.value;
|
||||
} else {
|
||||
let uri;
|
||||
try {
|
||||
uri = PlacesUtils.bookmarks.getBookmarkURI(id);
|
||||
} catch (ex) {
|
||||
// Bug 1182366 - NS_ERROR_MALFORMED_URI here stops bookmarks sync.
|
||||
// Try and get the string value of the URL for diagnostic purposes.
|
||||
let url = this._getStringUrlForId(id);
|
||||
this._log.warn(`Deleting bookmark with invalid URI. url="${url}", id=${id}`);
|
||||
try {
|
||||
PlacesUtils.bookmarks.removeItem(id);
|
||||
} catch (ex) {
|
||||
this._log.warn("Failed to delete invalid bookmark", ex);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
key = "b" + uri.spec + ":" + PlacesUtils.bookmarks.getItemTitle(id);
|
||||
key = "b" + node.uri + ":" + node.title;
|
||||
}
|
||||
break;
|
||||
case PlacesUtils.bookmarks.TYPE_FOLDER:
|
||||
key = "f" + PlacesUtils.bookmarks.getItemTitle(id);
|
||||
case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
|
||||
// Folder
|
||||
key = "f" + node.title;
|
||||
break;
|
||||
case PlacesUtils.bookmarks.TYPE_SEPARATOR:
|
||||
key = "s" + PlacesUtils.bookmarks.getItemIndex(id);
|
||||
case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
|
||||
// Separator
|
||||
key = "s" + node.index;
|
||||
break;
|
||||
default:
|
||||
this._log.error("Unknown place type: '"+placeType+"'");
|
||||
continue;
|
||||
}
|
||||
|
||||
// The mapping is on a per parent-folder-name basis.
|
||||
let parent = PlacesUtils.bookmarks.getFolderIdForItem(id);
|
||||
if (parent <= 0)
|
||||
continue;
|
||||
|
||||
let parentName = PlacesUtils.bookmarks.getItemTitle(parent);
|
||||
let parentName = parent.title;
|
||||
if (guidMap[parentName] == null)
|
||||
guidMap[parentName] = {};
|
||||
|
||||
|
|
|
@ -396,7 +396,9 @@ add_test(function test_bookmark_guidMap_fail() {
|
|||
engine.lastSync = 1; // So we don't back up.
|
||||
|
||||
// Make building the GUID map fail.
|
||||
store.getAllIDs = function () { throw "Nooo"; };
|
||||
|
||||
let pbt = PlacesUtils.promiseBookmarksTree;
|
||||
PlacesUtils.promiseBookmarksTree = function() { return Promise.reject("Nooo"); };
|
||||
|
||||
// Ensure that we throw when accessing _guidMap.
|
||||
engine._syncStartup();
|
||||
|
@ -422,6 +424,7 @@ add_test(function test_bookmark_guidMap_fail() {
|
|||
}
|
||||
do_check_eq(err, "Nooo");
|
||||
|
||||
PlacesUtils.promiseBookmarksTree = pbt;
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
|
||||
|
|
|
@ -12,46 +12,6 @@ var engine = Service.engineManager.get("bookmarks");
|
|||
var store = engine._store;
|
||||
var tracker = engine._tracker;
|
||||
|
||||
// Return a promise resolved when the specified message is written to the
|
||||
// bookmarks engine log.
|
||||
function promiseLogMessage(messagePortion) {
|
||||
return new Promise(resolve => {
|
||||
let appender;
|
||||
let log = Log.repository.getLogger("Sync.Engine.Bookmarks");
|
||||
|
||||
function TestAppender() {
|
||||
Log.Appender.call(this);
|
||||
}
|
||||
TestAppender.prototype = Object.create(Log.Appender.prototype);
|
||||
TestAppender.prototype.doAppend = function(message) {
|
||||
if (message.indexOf(messagePortion) >= 0) {
|
||||
log.removeAppender(appender);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
TestAppender.prototype.level = Log.Level.Debug;
|
||||
appender = new TestAppender();
|
||||
log.addAppender(appender);
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise that resolves if the specified ID does *not* exist and
|
||||
// rejects if it does.
|
||||
function promiseNoItem(itemId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
PlacesUtils.bookmarks.getFolderIdForItem(itemId);
|
||||
reject("fetching the item didn't fail");
|
||||
} catch (ex) {
|
||||
if (ex.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
|
||||
resolve("item doesn't exist");
|
||||
} else {
|
||||
reject("unexpected exception: " + ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
add_task(function* test_ignore_invalid_uri() {
|
||||
_("Ensure that we don't die with invalid bookmarks.");
|
||||
|
||||
|
@ -70,13 +30,9 @@ add_task(function* test_ignore_invalid_uri() {
|
|||
{ id: bmid, url: "<invalid url>" });
|
||||
}));
|
||||
|
||||
// DB is now "corrupt" - setup a log appender to capture what we log.
|
||||
let promiseMessage = promiseLogMessage('Deleting bookmark with invalid URI. url="<invalid url>"');
|
||||
// This should work and log our invalid id.
|
||||
// Ensure that this doesn't throw even though the DB is now in a bad state (a
|
||||
// bookmark has an illegal url).
|
||||
engine._buildGUIDMap();
|
||||
yield promiseMessage;
|
||||
// And we should have deleted the item.
|
||||
yield promiseNoItem(bmid);
|
||||
});
|
||||
|
||||
add_task(function* test_ignore_missing_uri() {
|
||||
|
@ -96,13 +52,9 @@ add_task(function* test_ignore_missing_uri() {
|
|||
, { id: bmid });
|
||||
}));
|
||||
|
||||
// DB is now "corrupt" - bookmarks will fail to locate a string url to log
|
||||
// and use "<not found>" as a placeholder.
|
||||
let promiseMessage = promiseLogMessage('Deleting bookmark with invalid URI. url="<not found>"');
|
||||
// Ensure that this doesn't throw even though the DB is now in a bad state (a
|
||||
// bookmark has an illegal url).
|
||||
engine._buildGUIDMap();
|
||||
yield promiseMessage;
|
||||
// And we should have deleted the item.
|
||||
yield promiseNoItem(bmid);
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
|
|
|
@ -240,7 +240,6 @@ ExtensionPage = class extends BaseContext {
|
|||
this.contentWindow = contentWindow || null;
|
||||
this.uri = uri || extension.baseURI;
|
||||
this.incognito = params.incognito || false;
|
||||
this.unloaded = false;
|
||||
|
||||
// This is the MessageSender property passed to extension.
|
||||
// It can be augmented by the "page-open" hook.
|
||||
|
@ -285,8 +284,6 @@ ExtensionPage = class extends BaseContext {
|
|||
return;
|
||||
}
|
||||
|
||||
this.unloaded = true;
|
||||
|
||||
super.unload();
|
||||
|
||||
Management.emit("page-unload", this);
|
||||
|
|
|
@ -54,6 +54,11 @@ function runSafeWithoutClone(f, ...args) {
|
|||
// Run a function, cloning arguments into context.cloneScope, and
|
||||
// report exceptions. |f| is expected to be in context.cloneScope.
|
||||
function runSafeSync(context, f, ...args) {
|
||||
if (context.unloaded) {
|
||||
Cu.reportError("runSafeSync called after context unloaded");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
args = Cu.cloneInto(args, context.cloneScope);
|
||||
} catch (e) {
|
||||
|
@ -72,6 +77,10 @@ function runSafe(context, f, ...args) {
|
|||
Cu.reportError(e);
|
||||
dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`);
|
||||
}
|
||||
if (context.unloaded) {
|
||||
dump(`runSafe failure: context is already unloaded ${filterStack(new Error())}\n`);
|
||||
return undefined;
|
||||
}
|
||||
return runSafeWithoutClone(f, ...args);
|
||||
}
|
||||
|
||||
|
@ -135,6 +144,7 @@ class BaseContext {
|
|||
this.checkedLastError = false;
|
||||
this._lastError = null;
|
||||
this.contextId = ++gContextId;
|
||||
this.unloaded = false;
|
||||
}
|
||||
|
||||
get cloneScope() {
|
||||
|
@ -145,6 +155,22 @@ class BaseContext {
|
|||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
runSafe(...args) {
|
||||
if (this.unloaded) {
|
||||
Cu.reportError("context.runSafe called after context unloaded");
|
||||
} else {
|
||||
return runSafeSync(this, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
runSafeWithoutClone(...args) {
|
||||
if (this.unloaded) {
|
||||
Cu.reportError("context.runSafeWithoutClone called after context unloaded");
|
||||
} else {
|
||||
return runSafeSyncWithoutClone(...args);
|
||||
}
|
||||
}
|
||||
|
||||
checkLoadURL(url, options = {}) {
|
||||
let ssm = Services.scriptSecurityManager;
|
||||
|
||||
|
@ -271,15 +297,17 @@ class BaseContext {
|
|||
wrapPromise(promise, callback = null) {
|
||||
// Note: `promise instanceof this.cloneScope.Promise` returns true
|
||||
// here even for promises that do not belong to the content scope.
|
||||
let runSafe = runSafeSync.bind(null, this);
|
||||
let runSafe = this.runSafe.bind(this);
|
||||
if (promise.constructor === this.cloneScope.Promise) {
|
||||
runSafe = runSafeSyncWithoutClone;
|
||||
runSafe = this.runSafeWithoutClone.bind(this);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
promise.then(
|
||||
args => {
|
||||
if (args instanceof SpreadArgs) {
|
||||
if (this.unloaded) {
|
||||
dump(`Promise resolved after context unloaded\n`);
|
||||
} else if (args instanceof SpreadArgs) {
|
||||
runSafe(callback, ...args);
|
||||
} else {
|
||||
runSafe(callback, args);
|
||||
|
@ -287,21 +315,37 @@ class BaseContext {
|
|||
},
|
||||
error => {
|
||||
this.withLastError(error, () => {
|
||||
runSafeSyncWithoutClone(callback);
|
||||
if (this.unloaded) {
|
||||
dump(`Promise rejected after context unloaded\n`);
|
||||
} else {
|
||||
this.runSafeWithoutClone(callback);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return new this.cloneScope.Promise((resolve, reject) => {
|
||||
promise.then(
|
||||
value => { runSafe(resolve, value); },
|
||||
value => {
|
||||
runSafeSyncWithoutClone(reject, this.normalizeError(value));
|
||||
if (this.unloaded) {
|
||||
dump(`Promise resolved after context unloaded\n`);
|
||||
} else {
|
||||
runSafe(resolve, value);
|
||||
}
|
||||
},
|
||||
value => {
|
||||
if (this.unloaded) {
|
||||
dump(`Promise rejected after context unloaded\n`);
|
||||
} else {
|
||||
this.runSafeWithoutClone(reject, this.normalizeError(value));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unload() {
|
||||
this.unloaded = true;
|
||||
|
||||
MessageChannel.abortResponses({
|
||||
extensionId: this.extension.id,
|
||||
contextId: this.contextId,
|
||||
|
@ -570,13 +614,19 @@ EventManager.prototype = {
|
|||
|
||||
fire(...args) {
|
||||
for (let callback of this.callbacks) {
|
||||
runSafe(this.context, callback, ...args);
|
||||
Promise.resolve(callback).then(callback => {
|
||||
if (this.context.unloaded) {
|
||||
dump(`${this.name} event fired after context unloaded.`);
|
||||
} else if (this.callbacks.has(callback)) {
|
||||
this.context.runSafe(callback, ...args);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
fireWithoutClone(...args) {
|
||||
for (let callback of this.callbacks) {
|
||||
runSafeSyncWithoutClone(callback, ...args);
|
||||
this.context.runSafeWithoutClone(callback, ...args);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -609,7 +659,15 @@ function SingletonEventManager(context, name, register) {
|
|||
|
||||
SingletonEventManager.prototype = {
|
||||
addListener(callback, ...args) {
|
||||
let unregister = this.register(callback, ...args);
|
||||
let wrappedCallback = (...args) => {
|
||||
if (this.context.unloaded) {
|
||||
dump(`${this.name} event fired after context unloaded.`);
|
||||
} else if (this.unregister.has(callback)) {
|
||||
return callback(...args);
|
||||
}
|
||||
};
|
||||
|
||||
let unregister = this.register(wrappedCallback, ...args);
|
||||
this.unregister.set(callback, unregister);
|
||||
},
|
||||
|
||||
|
@ -933,7 +991,7 @@ Messenger.prototype = {
|
|||
this.delegate.getSender(this.context, target, sender);
|
||||
}
|
||||
let port = new Port(this.context, mm, name, portId, sender);
|
||||
runSafeSyncWithoutClone(callback, port.api());
|
||||
this.context.runSafeWithoutClone(callback, port.api());
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -150,9 +150,26 @@ add_task(function* webnav_ordering() {
|
|||
ok(index1 < index2, `Action ${JSON.stringify(action1)} happened before ${JSON.stringify(action2)}`);
|
||||
}
|
||||
|
||||
// As required in the webNavigation API documentation:
|
||||
// If a navigating frame contains subframes, its onCommitted is fired before any
|
||||
// of its children's onBeforeNavigate; while onCompleted is fired after
|
||||
// all of its children's onCompleted.
|
||||
checkBefore({url: URL, event: "onCommitted"}, {url: FRAME, event: "onBeforeNavigate"});
|
||||
checkBefore({url: FRAME, event: "onCompleted"}, {url: URL, event: "onCompleted"});
|
||||
|
||||
// As required in the webNAvigation API documentation, check the event sequence:
|
||||
// onBeforeNavigate -> onCommitted -> onDOMContentLoaded -> onCompleted
|
||||
let expectedEventSequence = [
|
||||
"onBeforeNavigate", "onCommitted", "onDOMContentLoaded", "onCompleted",
|
||||
];
|
||||
|
||||
for (let i = 1; i < expectedEventSequence.length; i++) {
|
||||
let after = expectedEventSequence[i];
|
||||
let before = expectedEventSequence[i - 1];
|
||||
checkBefore({url: URL, event: before}, {url: URL, event: after});
|
||||
checkBefore({url: FRAME, event: before}, {url: FRAME, event: after});
|
||||
}
|
||||
|
||||
yield loadAndWait(win, "onCompleted", FRAME2, () => { win.frames[0].location = FRAME2; });
|
||||
|
||||
checkRequired(FRAME2);
|
||||
|
|
|
@ -1740,7 +1740,7 @@ this.PlacesUtils = {
|
|||
* - guid (string): the item's GUID (same as aItemGuid for the top item).
|
||||
* - [deprecated] id (number): the item's id. This is only if
|
||||
* aOptions.includeItemIds is set.
|
||||
* - type (number): the item's type. @see PlacesUtils.TYPE_X_*
|
||||
* - type (string): the item's type. @see PlacesUtils.TYPE_X_*
|
||||
* - title (string): the item's title. If it has no title, this property
|
||||
* isn't set.
|
||||
* - dateAdded (number, microseconds from the epoch): the date-added value of
|
||||
|
@ -1749,6 +1749,7 @@ this.PlacesUtils = {
|
|||
* value of the item.
|
||||
* - annos (see getAnnotationsForItem): the item's annotations. This is not
|
||||
* set if there are no annotations set for the item).
|
||||
* - index: the item's index under it's parent.
|
||||
*
|
||||
* The root object (i.e. the one for aItemGuid) also has the following
|
||||
* properties set:
|
||||
|
|
|
@ -1256,7 +1256,7 @@ Search.prototype = {
|
|||
// the URLBar.
|
||||
value: makeActionURL("remotetab", { url, deviceName }),
|
||||
comment: title || url,
|
||||
style: "action",
|
||||
style: "action remotetab",
|
||||
// we want frecency > FRECENCY_DEFAULT so it doesn't get pushed out
|
||||
// by "remote" matches.
|
||||
frecency: FRECENCY_DEFAULT + 1,
|
||||
|
|
|
@ -53,7 +53,7 @@ function makeRemoteTabMatch(url, deviceName, extra = {}) {
|
|||
return {
|
||||
uri: makeActionURI("remotetab", {url, deviceName}),
|
||||
title: extra.title || url,
|
||||
style: [ "action" ],
|
||||
style: [ "action", "remotetab" ],
|
||||
icon: extra.icon,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
|
|||
"resource://gre/modules/ProfileAge.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
|
||||
"resource://gre/modules/UpdateUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
|
||||
"resource://gre/modules/WindowsRegistry.jsm");
|
||||
|
||||
const CHANGE_THROTTLE_INTERVAL_MS = 5 * 60 * 1000;
|
||||
|
||||
|
@ -317,16 +319,17 @@ function getGfxAdapter(aSuffix = "") {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the service pack information on Windows platforms. This was copied from
|
||||
* nsUpdateService.js.
|
||||
* Gets the service pack and build information on Windows platforms. The initial version
|
||||
* was copied from nsUpdateService.js.
|
||||
*
|
||||
* @return An object containing the service pack major and minor versions.
|
||||
* @return An object containing the service pack major and minor versions, along with the
|
||||
* build number.
|
||||
*/
|
||||
function getServicePack() {
|
||||
const UNKNOWN_SERVICE_PACK = {major: null, minor: null};
|
||||
function getWindowsVersionInfo() {
|
||||
const UNKNOWN_VERSION_INFO = {servicePackMajor: null, servicePackMinor: null, buildNumber: null};
|
||||
|
||||
if (AppConstants.platform !== "win") {
|
||||
return UNKNOWN_SERVICE_PACK;
|
||||
return UNKNOWN_VERSION_INFO;
|
||||
}
|
||||
|
||||
const BYTE = ctypes.uint8_t;
|
||||
|
@ -367,11 +370,12 @@ function getServicePack() {
|
|||
}
|
||||
|
||||
return {
|
||||
major: winVer.wServicePackMajor,
|
||||
minor: winVer.wServicePackMinor,
|
||||
servicePackMajor: winVer.wServicePackMajor,
|
||||
servicePackMinor: winVer.wServicePackMinor,
|
||||
buildNumber: winVer.dwBuildNumber,
|
||||
};
|
||||
} catch (e) {
|
||||
return UNKNOWN_SERVICE_PACK;
|
||||
return UNKNOWN_VERSION_INFO;
|
||||
} finally {
|
||||
kernel32.close();
|
||||
}
|
||||
|
@ -1243,9 +1247,23 @@ EnvironmentCache.prototype = {
|
|||
if (["gonk", "android"].includes(AppConstants.platform)) {
|
||||
data.kernelVersion = getSysinfoProperty("kernel_version", null);
|
||||
} else if (AppConstants.platform === "win") {
|
||||
let servicePack = getServicePack();
|
||||
data.servicePackMajor = servicePack.major;
|
||||
data.servicePackMinor = servicePack.minor;
|
||||
// The path to the "UBR" key, queried to get additional version details on Windows.
|
||||
const WINDOWS_UBR_KEY_PATH = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
|
||||
|
||||
let versionInfo = getWindowsVersionInfo();
|
||||
data.servicePackMajor = versionInfo.servicePackMajor;
|
||||
data.servicePackMinor = versionInfo.servicePackMinor;
|
||||
// We only need the build number and UBR if we're at or above Windows 10.
|
||||
if (typeof(data.version) === 'string' &&
|
||||
Services.vc.compare(data.version, "10") >= 0) {
|
||||
data.windowsBuildNumber = versionInfo.buildNumber;
|
||||
// Query the UBR key and only add it to the environment if it's available.
|
||||
// |readRegKey| doesn't throw, but rather returns 'undefined' on error.
|
||||
let ubr = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
|
||||
WINDOWS_UBR_KEY_PATH, "UBR",
|
||||
Ci.nsIWindowsRegKey.WOW64_64);
|
||||
data.windowsUBR = (ubr !== undefined) ? ubr : null;
|
||||
}
|
||||
data.installYear = getSysinfoProperty("installYear", null);
|
||||
}
|
||||
|
||||
|
|
|
@ -108,6 +108,9 @@ Structure::
|
|||
kernelVersion: <string>, // android/b2g only or null on failure
|
||||
servicePackMajor: <number>, // windows only or null on failure
|
||||
servicePackMinor: <number>, // windows only or null on failure
|
||||
windowsBuildNumber: <number>, // windows 10 only or null on failure
|
||||
windowsUBR: <number>, // windows 10 only or null on failure
|
||||
installYear: <number>, // windows only or null on failure
|
||||
locale: <string>, // "en" or null on failure
|
||||
},
|
||||
hdd: {
|
||||
|
@ -323,6 +326,24 @@ Important values for ``distributionId`` include:
|
|||
- "canonical", for the `Ubuntu Firefox repack <http://bazaar.launchpad.net/~mozillateam/firefox/firefox.trusty/view/head:/debian/distribution.ini>`_.
|
||||
- "yandex", for the Firefox Build by Yandex.
|
||||
|
||||
system
|
||||
------
|
||||
|
||||
os
|
||||
~~
|
||||
|
||||
This object contains operating system information.
|
||||
|
||||
- ``name``: the name of the OS.
|
||||
- ``version``: a string representing the OS version.
|
||||
- ``kernelVersion``: an Android/B2G only string representing the kernel version.
|
||||
- ``servicePackMajor``: the Windows only major version number for the installed service pack.
|
||||
- ``servicePackMinor``: the Windows only minor version number for the installed service pack.
|
||||
- ``windowsBuildNumber``: the Windows build number, only available for Windows >= 10.
|
||||
- ``windowsUBR``: the Windows UBR number, only available for Windows >= 10. This value is incremented by Windows cumulative updates patches.
|
||||
- ``installYear``: the Windows only integer representing the year the OS was installed.
|
||||
- ``locale``: the string representing the OS locale.
|
||||
|
||||
addons
|
||||
------
|
||||
|
||||
|
|
|
@ -511,6 +511,16 @@ function checkSystemSection(data) {
|
|||
"ServicePackMajor must be a number.");
|
||||
Assert.ok(Number.isFinite(osData["servicePackMinor"]),
|
||||
"ServicePackMinor must be a number.");
|
||||
if ("windowsBuildNumber" in osData) {
|
||||
// This might not be available on all Windows platforms.
|
||||
Assert.ok(Number.isFinite(osData["windowsBuildNumber"]),
|
||||
"windowsBuildNumber must be a number.");
|
||||
}
|
||||
if ("windowsUBR" in osData) {
|
||||
// This might not be available on all Windows platforms.
|
||||
Assert.ok((osData["windowsUBR"] === null) || Number.isFinite(osData["windowsUBR"]),
|
||||
"windowsUBR must be null or a number.");
|
||||
}
|
||||
} else if (gIsAndroid || gIsGonk) {
|
||||
Assert.ok(checkNullOrString(osData.kernelVersion));
|
||||
}
|
||||
|
|
|
@ -17,15 +17,19 @@ var WindowsRegistry = {
|
|||
* The registry path to the key.
|
||||
* @param aKey
|
||||
* The key name.
|
||||
* @param [aRegistryNode=0]
|
||||
* Optionally set to nsIWindowsRegKey.WOW64_64 (or nsIWindowsRegKey.WOW64_32)
|
||||
* to access a 64-bit (32-bit) key from either a 32-bit or 64-bit application.
|
||||
* @return The key value or undefined if it doesn't exist. If the key is
|
||||
* a REG_MULTI_SZ, an array is returned.
|
||||
*/
|
||||
readRegKey: function(aRoot, aPath, aKey) {
|
||||
readRegKey: function(aRoot, aPath, aKey, aRegistryNode=0) {
|
||||
const kRegMultiSz = 7;
|
||||
const kMode = Ci.nsIWindowsRegKey.ACCESS_READ | aRegistryNode;
|
||||
let registry = Cc["@mozilla.org/windows-registry-key;1"].
|
||||
createInstance(Ci.nsIWindowsRegKey);
|
||||
try {
|
||||
registry.open(aRoot, aPath, Ci.nsIWindowsRegKey.ACCESS_READ);
|
||||
registry.open(aRoot, aPath, kMode);
|
||||
if (registry.hasValue(aKey)) {
|
||||
let type = registry.getValueType(aKey);
|
||||
switch (type) {
|
||||
|
@ -57,13 +61,17 @@ var WindowsRegistry = {
|
|||
* The registry path to the key.
|
||||
* @param aKey
|
||||
* The key name.
|
||||
* @param [aRegistryNode=0]
|
||||
* Optionally set to nsIWindowsRegKey.WOW64_64 (or nsIWindowsRegKey.WOW64_32)
|
||||
* to access a 64-bit (32-bit) key from either a 32-bit or 64-bit application.
|
||||
*/
|
||||
removeRegKey: function(aRoot, aPath, aKey) {
|
||||
removeRegKey: function(aRoot, aPath, aKey, aRegistryNode=0) {
|
||||
let registry = Cc["@mozilla.org/windows-registry-key;1"].
|
||||
createInstance(Ci.nsIWindowsRegKey);
|
||||
try {
|
||||
let mode = Ci.nsIWindowsRegKey.ACCESS_QUERY_VALUE |
|
||||
Ci.nsIWindowsRegKey.ACCESS_SET_VALUE;
|
||||
Ci.nsIWindowsRegKey.ACCESS_SET_VALUE |
|
||||
aRegistryNode;
|
||||
registry.open(aRoot, aPath, mode);
|
||||
if (registry.hasValue(aKey)) {
|
||||
registry.removeValue(aKey);
|
||||
|
|
|
@ -25,13 +25,15 @@ var Manager = {
|
|||
init() {
|
||||
Services.mm.addMessageListener("Extension:DOMContentLoaded", this);
|
||||
Services.mm.addMessageListener("Extension:StateChange", this);
|
||||
Services.mm.addMessageListener("Extension:LocationChange", this);
|
||||
Services.mm.addMessageListener("Extension:DocumentChange", this);
|
||||
Services.mm.addMessageListener("Extension:HistoryChange", this);
|
||||
Services.mm.loadFrameScript("resource://gre/modules/WebNavigationContent.js", true);
|
||||
},
|
||||
|
||||
uninit() {
|
||||
Services.mm.removeMessageListener("Extension:StateChange", this);
|
||||
Services.mm.removeMessageListener("Extension:LocationChange", this);
|
||||
Services.mm.removeMessageListener("Extension:DocumentChange", this);
|
||||
Services.mm.removeMessageListener("Extension:HistoryChange", this);
|
||||
Services.mm.removeMessageListener("Extension:DOMContentLoaded", this);
|
||||
Services.mm.removeDelayedFrameScript("resource://gre/modules/WebNavigationContent.js");
|
||||
Services.mm.broadcastAsyncMessage("Extension:DisableWebNavigation");
|
||||
|
@ -70,8 +72,12 @@ var Manager = {
|
|||
this.onStateChange(target, data);
|
||||
break;
|
||||
|
||||
case "Extension:LocationChange":
|
||||
this.onLocationChange(target, data);
|
||||
case "Extension:DocumentChange":
|
||||
this.onDocumentChange(target, data);
|
||||
break;
|
||||
|
||||
case "Extension:HistoryChange":
|
||||
this.onHistoryChange(target, data);
|
||||
break;
|
||||
|
||||
case "Extension:DOMContentLoaded":
|
||||
|
@ -97,15 +103,19 @@ var Manager = {
|
|||
}
|
||||
},
|
||||
|
||||
onLocationChange(browser, data) {
|
||||
onDocumentChange(browser, data) {
|
||||
let url = data.location;
|
||||
|
||||
this.fire("onCommitted", browser, data, {url});
|
||||
},
|
||||
|
||||
onHistoryChange(browser, data) {
|
||||
let url = data.location;
|
||||
|
||||
if (data.isReferenceFragmentUpdated) {
|
||||
this.fire("onReferenceFragmentUpdated", browser, data, {url});
|
||||
} else if (data.isHistoryStateUpdated) {
|
||||
this.fire("onHistoryStateUpdated", browser, data, {url});
|
||||
} else {
|
||||
this.fire("onCommitted", browser, data, {url});
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -54,29 +54,28 @@ var WebProgressListener = {
|
|||
},
|
||||
|
||||
onStateChange: function onStateChange(webProgress, request, stateFlags, status) {
|
||||
let data = {
|
||||
requestURL: request.QueryInterface(Ci.nsIChannel).URI.spec,
|
||||
windowId: webProgress.DOMWindowID,
|
||||
parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
|
||||
status,
|
||||
stateFlags,
|
||||
};
|
||||
let locationURI = request.QueryInterface(Ci.nsIChannel).URI;
|
||||
|
||||
sendAsyncMessage("Extension:StateChange", data);
|
||||
this.sendStateChange({webProgress, locationURI, stateFlags, status});
|
||||
|
||||
if (webProgress.DOMWindow.top != webProgress.DOMWindow) {
|
||||
let webNav = webProgress.QueryInterface(Ci.nsIWebNavigation);
|
||||
if (!webNav.canGoBack) {
|
||||
// For some reason we don't fire onLocationChange for the
|
||||
// initial navigation of a sub-frame. So we need to simulate
|
||||
// it here.
|
||||
this.onLocationChange(webProgress, request, request.QueryInterface(Ci.nsIChannel).URI, 0);
|
||||
}
|
||||
// Based on the docs of the webNavigation.onCommitted event, it should be raised when:
|
||||
// "The document might still be downloading, but at least part of
|
||||
// the document has been received"
|
||||
// and for some reason we don't fire onLocationChange for the
|
||||
// initial navigation of a sub-frame.
|
||||
// For the above two reasons, when the navigation event is related to
|
||||
// a sub-frame we process the document change here and
|
||||
// then send an "Extension:DocumentChange" message to the main process,
|
||||
// where it will be turned into a webNavigation.onCommitted event.
|
||||
// (see Bug 1264936 and Bug 125662 for rationale)
|
||||
if ((webProgress.DOMWindow.top != webProgress.DOMWindow) &&
|
||||
(stateFlags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT)) {
|
||||
this.sendDocumentChange({webProgress, locationURI});
|
||||
}
|
||||
},
|
||||
|
||||
onLocationChange: function onLocationChange(webProgress, request, locationURI, flags) {
|
||||
let {DOMWindow, loadType} = webProgress;
|
||||
let {DOMWindow} = webProgress;
|
||||
|
||||
// Get the previous URI loaded in the DOMWindow.
|
||||
let previousURI = this.previousURIMap.get(DOMWindow);
|
||||
|
@ -85,37 +84,79 @@ var WebProgressListener = {
|
|||
this.previousURIMap.set(DOMWindow, locationURI);
|
||||
|
||||
let isSameDocument = (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
|
||||
let isHistoryStateUpdated = false;
|
||||
let isReferenceFragmentUpdated = false;
|
||||
|
||||
// When a frame navigation doesn't change the current loaded document
|
||||
// (which can be due to history.pushState/replaceState or to a changed hash in the url),
|
||||
// it is reported only to the onLocationChange, for this reason
|
||||
// we process the history change here and then we are going to send
|
||||
// an "Extension:HistoryChange" to the main process, where it will be turned
|
||||
// into a webNavigation.onHistoryStateUpdated/onReferenceFragmentUpdated event.
|
||||
if (isSameDocument) {
|
||||
let pathChanged = !(previousURI && locationURI.equalsExceptRef(previousURI));
|
||||
let hashChanged = !(previousURI && previousURI.ref == locationURI.ref);
|
||||
|
||||
// When the location changes but the document is the same:
|
||||
// - path not changed and hash changed -> |onReferenceFragmentUpdated|
|
||||
// (even if it changed using |history.pushState|)
|
||||
// - path not changed and hash not changed -> |onHistoryStateUpdated|
|
||||
// (only if it changes using |history.pushState|)
|
||||
// - path changed -> |onHistoryStateUpdated|
|
||||
|
||||
if (!pathChanged && hashChanged) {
|
||||
isReferenceFragmentUpdated = true;
|
||||
} else if (loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE) {
|
||||
isHistoryStateUpdated = true;
|
||||
} else if (loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY) {
|
||||
isHistoryStateUpdated = true;
|
||||
}
|
||||
this.sendHistoryChange({webProgress, previousURI, locationURI});
|
||||
} else if (webProgress.DOMWindow.top == webProgress.DOMWindow) {
|
||||
// We have to catch the document changes from top level frames here,
|
||||
// where we can detect the "server redirect" transition.
|
||||
// (see Bug 1264936 and Bug 125662 for rationale)
|
||||
this.sendDocumentChange({webProgress, locationURI, request});
|
||||
}
|
||||
},
|
||||
|
||||
sendStateChange({webProgress, locationURI, stateFlags, status}) {
|
||||
let data = {
|
||||
requestURL: locationURI.spec,
|
||||
windowId: webProgress.DOMWindowID,
|
||||
parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
|
||||
status,
|
||||
stateFlags,
|
||||
};
|
||||
|
||||
sendAsyncMessage("Extension:StateChange", data);
|
||||
},
|
||||
|
||||
sendDocumentChange({webProgress, locationURI}) {
|
||||
let data = {
|
||||
isHistoryStateUpdated, isReferenceFragmentUpdated,
|
||||
location: locationURI ? locationURI.spec : "",
|
||||
windowId: webProgress.DOMWindowID,
|
||||
parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
|
||||
};
|
||||
|
||||
sendAsyncMessage("Extension:LocationChange", data);
|
||||
sendAsyncMessage("Extension:DocumentChange", data);
|
||||
},
|
||||
|
||||
sendHistoryChange({webProgress, previousURI, locationURI}) {
|
||||
let {loadType} = webProgress;
|
||||
|
||||
let isHistoryStateUpdated = false;
|
||||
let isReferenceFragmentUpdated = false;
|
||||
|
||||
let pathChanged = !(previousURI && locationURI.equalsExceptRef(previousURI));
|
||||
let hashChanged = !(previousURI && previousURI.ref == locationURI.ref);
|
||||
|
||||
// When the location changes but the document is the same:
|
||||
// - path not changed and hash changed -> |onReferenceFragmentUpdated|
|
||||
// (even if it changed using |history.pushState|)
|
||||
// - path not changed and hash not changed -> |onHistoryStateUpdated|
|
||||
// (only if it changes using |history.pushState|)
|
||||
// - path changed -> |onHistoryStateUpdated|
|
||||
|
||||
if (!pathChanged && hashChanged) {
|
||||
isReferenceFragmentUpdated = true;
|
||||
} else if (loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE) {
|
||||
isHistoryStateUpdated = true;
|
||||
} else if (loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY) {
|
||||
isHistoryStateUpdated = true;
|
||||
}
|
||||
|
||||
if (isHistoryStateUpdated || isReferenceFragmentUpdated) {
|
||||
let data = {
|
||||
isHistoryStateUpdated, isReferenceFragmentUpdated,
|
||||
location: locationURI ? locationURI.spec : "",
|
||||
windowId: webProgress.DOMWindowID,
|
||||
parentWindowId: WebNavigationFrames.getParentWindowId(webProgress.DOMWindow),
|
||||
};
|
||||
|
||||
sendAsyncMessage("Extension:HistoryChange", data);
|
||||
}
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
|
||||
|
|
Загрузка…
Ссылка в новой задаче