зеркало из https://github.com/mozilla/gecko-dev.git
Merge fx-team to mozilla-central
This commit is contained in:
Коммит
f9cfa703c8
|
@ -30,6 +30,13 @@
|
|||
<menupopup id="customization-toolbar-menu" onpopupshowing="onViewToolbarsPopupShowing(event)"/>
|
||||
</button>
|
||||
<spacer flex="1"/>
|
||||
<label id="customization-undo-reset"
|
||||
hidden="true"
|
||||
onclick="gCustomizeMode.undoReset();"
|
||||
onkeypress="gCustomizeMode.undoReset();"
|
||||
class="text-link">
|
||||
&undoCmd.label;
|
||||
</label>
|
||||
<button id="customization-reset-button" oncommand="gCustomizeMode.reset();" label="&customizeMode.restoreDefaults;" class="customizationmode-button"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
|
|
|
@ -289,6 +289,12 @@
|
|||
break;
|
||||
case "popupshowing":
|
||||
this.setAttribute("panelopen", "true");
|
||||
// Bug 941196 - The panel can get taller when opening a subview. Disabling
|
||||
// autoPositioning means that the panel won't jump around if an opened
|
||||
// subview causes the panel to exceed the dimensions of the screen in the
|
||||
// direction that the panel originally opened in. This property resets
|
||||
// every time the popup closes, which is why we have to set it each time.
|
||||
this._panel.autoPosition = false;
|
||||
this._syncContainerWithMainView();
|
||||
break;
|
||||
case "popupshown":
|
||||
|
|
|
@ -128,6 +128,8 @@ let gGroupWrapperCache = new Map();
|
|||
let gSingleWrapperCache = new WeakMap();
|
||||
let gListeners = new Set();
|
||||
|
||||
let gUIStateBeforeReset = null;
|
||||
|
||||
let gModuleName = "[CustomizableUI]";
|
||||
#include logging.js
|
||||
|
||||
|
@ -693,6 +695,10 @@ let CustomizableUIInternal = {
|
|||
|
||||
onWidgetAdded: function(aWidgetId, aArea, aPosition) {
|
||||
this.insertNode(aWidgetId, aArea, aPosition, true);
|
||||
|
||||
if (!gResetting) {
|
||||
gUIStateBeforeReset = null;
|
||||
}
|
||||
},
|
||||
|
||||
onWidgetRemoved: function(aWidgetId, aArea) {
|
||||
|
@ -749,10 +755,20 @@ let CustomizableUIInternal = {
|
|||
windowCache.delete(aWidgetId);
|
||||
}
|
||||
}
|
||||
if (!gResetting) {
|
||||
gUIStateBeforeReset = null;
|
||||
}
|
||||
},
|
||||
|
||||
onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) {
|
||||
this.insertNode(aWidgetId, aArea, aNewPosition);
|
||||
if (!gResetting) {
|
||||
gUIStateBeforeReset = null;
|
||||
}
|
||||
},
|
||||
|
||||
onCustomizeEnd: function(aWindow) {
|
||||
gUIStateBeforeReset = null;
|
||||
},
|
||||
|
||||
registerBuildArea: function(aArea, aNode) {
|
||||
|
@ -2049,6 +2065,20 @@ let CustomizableUIInternal = {
|
|||
|
||||
reset: function() {
|
||||
gResetting = true;
|
||||
this._resetUIState();
|
||||
|
||||
// Rebuild each registered area (across windows) to reflect the state that
|
||||
// was reset above.
|
||||
this._rebuildRegisteredAreas();
|
||||
|
||||
gResetting = false;
|
||||
},
|
||||
|
||||
_resetUIState: function() {
|
||||
try {
|
||||
gUIStateBeforeReset = Services.prefs.getCharPref(kPrefCustomizationState);
|
||||
} catch(e) { }
|
||||
|
||||
Services.prefs.clearUserPref(kPrefCustomizationState);
|
||||
LOG("State reset");
|
||||
|
||||
|
@ -2062,9 +2092,9 @@ let CustomizableUIInternal = {
|
|||
for (let [areaId,] of gAreas) {
|
||||
this.restoreStateForArea(areaId);
|
||||
}
|
||||
},
|
||||
|
||||
// Rebuild each registered area (across windows) to reflect the state that
|
||||
// was reset above.
|
||||
_rebuildRegisteredAreas: function() {
|
||||
for (let [areaId, areaNodes] of gBuildAreas) {
|
||||
let placements = gPlacements.get(areaId);
|
||||
for (let areaNode of areaNodes) {
|
||||
|
@ -2078,7 +2108,23 @@ let CustomizableUIInternal = {
|
|||
}
|
||||
}
|
||||
}
|
||||
gResetting = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Undoes a previous reset, restoring the state of the UI to the state prior to the reset.
|
||||
*/
|
||||
undoReset: function() {
|
||||
if (!gUIStateBeforeReset) {
|
||||
return;
|
||||
}
|
||||
Services.prefs.setCharPref(kPrefCustomizationState, gUIStateBeforeReset);
|
||||
this.loadSavedState();
|
||||
for (let areaId of Object.keys(gSavedState.placements)) {
|
||||
let placements = gSavedState.placements[areaId];
|
||||
gPlacements.set(areaId, placements);
|
||||
}
|
||||
this._rebuildRegisteredAreas();
|
||||
gUIStateBeforeReset = null;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -2832,6 +2878,25 @@ this.CustomizableUI = {
|
|||
reset: function() {
|
||||
CustomizableUIInternal.reset();
|
||||
},
|
||||
|
||||
/**
|
||||
* Undo the previous reset, can only be called immediately after a reset.
|
||||
* @return a promise that will be resolved when the operation is complete.
|
||||
*/
|
||||
undoReset: function() {
|
||||
CustomizableUIInternal.undoReset();
|
||||
},
|
||||
|
||||
/**
|
||||
* Can the last Restore Defaults operation be undone.
|
||||
*
|
||||
* @return A boolean stating whether an undo of the
|
||||
* Restore Defaults can be performed.
|
||||
*/
|
||||
get canUndoReset() {
|
||||
return !!gUIStateBeforeReset;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the placement of a widget. This is by far the best way to obtain
|
||||
* information about what the state of your widget is. The internals of
|
||||
|
|
|
@ -239,6 +239,7 @@ CustomizeMode.prototype = {
|
|||
document.getElementById("PanelUI-quit").setAttribute("disabled", true);
|
||||
|
||||
this._updateResetButton();
|
||||
this._updateUndoResetButton();
|
||||
|
||||
this._skipSourceNodeCheck = Services.prefs.getPrefType(kSkipSourceNodePref) == Ci.nsIPrefBranch.PREF_BOOL &&
|
||||
Services.prefs.getBoolPref(kSkipSourceNodePref);
|
||||
|
@ -854,12 +855,35 @@ CustomizeMode.prototype = {
|
|||
this.persistCurrentSets(true);
|
||||
|
||||
this._updateResetButton();
|
||||
this._updateUndoResetButton();
|
||||
this._updateEmptyPaletteNotice();
|
||||
this._showPanelCustomizationPlaceholders();
|
||||
this.resetting = false;
|
||||
}.bind(this)).then(null, ERROR);
|
||||
},
|
||||
|
||||
undoReset: function() {
|
||||
this.resetting = true;
|
||||
|
||||
return Task.spawn(function() {
|
||||
this._removePanelCustomizationPlaceholders();
|
||||
yield this.depopulatePalette();
|
||||
yield this._unwrapToolbarItems();
|
||||
|
||||
CustomizableUI.undoReset();
|
||||
|
||||
yield this._wrapToolbarItems();
|
||||
yield this.populatePalette();
|
||||
|
||||
this.persistCurrentSets(true);
|
||||
|
||||
this._updateResetButton();
|
||||
this._updateUndoResetButton();
|
||||
this._updateEmptyPaletteNotice();
|
||||
this.resetting = false;
|
||||
}.bind(this)).then(null, ERROR);
|
||||
},
|
||||
|
||||
_onToolbarVisibilityChange: function(aEvent) {
|
||||
let toolbar = aEvent.target;
|
||||
if (aEvent.detail.visible && toolbar.getAttribute("customizable") == "true") {
|
||||
|
@ -958,6 +982,7 @@ CustomizeMode.prototype = {
|
|||
this._changed = true;
|
||||
if (!this.resetting) {
|
||||
this._updateResetButton();
|
||||
this._updateUndoResetButton();
|
||||
this._updateEmptyPaletteNotice();
|
||||
}
|
||||
this.dispatchToolboxEvent("customizationchange");
|
||||
|
@ -973,6 +998,11 @@ CustomizeMode.prototype = {
|
|||
btn.disabled = CustomizableUI.inDefaultState;
|
||||
},
|
||||
|
||||
_updateUndoResetButton: function() {
|
||||
let undoReset = this.document.getElementById("customization-undo-reset");
|
||||
undoReset.hidden = !CustomizableUI.canUndoReset;
|
||||
},
|
||||
|
||||
handleEvent: function(aEvent) {
|
||||
switch(aEvent.type) {
|
||||
case "toolbarvisibilitychange":
|
||||
|
|
|
@ -65,4 +65,5 @@ skip-if = os == "linux"
|
|||
[browser_956602_remove_special_widget.js]
|
||||
[browser_969427_recreate_destroyed_widget_after_reset.js]
|
||||
[browser_969661_character_encoding_navbar_disabled.js]
|
||||
[browser_970511_undo_restore_default.js]
|
||||
[browser_panel_toggle.js]
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Restoring default should show an "undo" option which undoes the restoring operation.
|
||||
add_task(function() {
|
||||
let homeButtonId = "home-button";
|
||||
CustomizableUI.removeWidgetFromArea(homeButtonId);
|
||||
yield startCustomizing();
|
||||
ok(!CustomizableUI.inDefaultState, "Not in default state to begin with");
|
||||
is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette");
|
||||
let undoReset = document.getElementById("customization-undo-reset");
|
||||
is(undoReset.hidden, true, "The undo button is hidden before reset");
|
||||
|
||||
yield gCustomizeMode.reset();
|
||||
|
||||
ok(CustomizableUI.inDefaultState, "In default state after reset");
|
||||
is(undoReset.hidden, false, "The undo button is visible after reset");
|
||||
|
||||
undoReset.click();
|
||||
yield waitForCondition(function() !gCustomizeMode.resetting);
|
||||
ok(!CustomizableUI.inDefaultState, "Not in default state after reset-undo");
|
||||
is(undoReset.hidden, true, "The undo button is hidden after clicking on the undo button");
|
||||
is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette");
|
||||
|
||||
yield gCustomizeMode.reset();
|
||||
});
|
||||
|
||||
// Performing an action after a reset will hide the reset button.
|
||||
add_task(function() {
|
||||
let homeButtonId = "home-button";
|
||||
CustomizableUI.removeWidgetFromArea(homeButtonId);
|
||||
ok(!CustomizableUI.inDefaultState, "Not in default state to begin with");
|
||||
is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette");
|
||||
let undoReset = document.getElementById("customization-undo-reset");
|
||||
is(undoReset.hidden, true, "The undo button is hidden before reset");
|
||||
|
||||
yield gCustomizeMode.reset();
|
||||
|
||||
ok(CustomizableUI.inDefaultState, "In default state after reset");
|
||||
is(undoReset.hidden, false, "The undo button is visible after reset");
|
||||
|
||||
CustomizableUI.addWidgetToArea(homeButtonId, CustomizableUI.AREA_PANEL);
|
||||
is(undoReset.hidden, true, "The undo button is hidden after another change");
|
||||
});
|
||||
|
||||
// "Restore defaults", exiting customize, and re-entering shouldn't show the Undo button
|
||||
add_task(function() {
|
||||
let undoReset = document.getElementById("customization-undo-reset");
|
||||
is(undoReset.hidden, true, "The undo button is hidden before a reset");
|
||||
ok(!CustomizableUI.inDefaultState, "The browser should not be in default state");
|
||||
yield gCustomizeMode.reset();
|
||||
|
||||
is(undoReset.hidden, false, "The undo button is hidden after a reset");
|
||||
yield endCustomizing();
|
||||
yield startCustomizing();
|
||||
is(undoReset.hidden, true, "The undo reset button should be hidden after entering customization mode");
|
||||
});
|
||||
|
||||
add_task(function asyncCleanup() {
|
||||
yield gCustomizeMode.reset();
|
||||
yield endCustomizing();
|
||||
});
|
|
@ -13,8 +13,10 @@ support-files =
|
|||
# Bug 916763 - too many intermittent failures
|
||||
skip-if = true
|
||||
[browser_inspector_markup_edit.js]
|
||||
# Bug 904953 - too many intermittent failures on Linux
|
||||
skip-if = os == "linux"
|
||||
[browser_inspector_markup_edit_2.js]
|
||||
[browser_inspector_markup_edit_3.js]
|
||||
[browser_inspector_markup_edit_4.js]
|
||||
[browser_inspector_markup_add_attributes.js]
|
||||
[browser_inspector_markup_edit_outerhtml.js]
|
||||
[browser_inspector_markup_edit_outerhtml2.js]
|
||||
[browser_inspector_markup_mutation.js]
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
/* Any copyright", " is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Tests that adding various types of attributes to nodes in the markup-view
|
||||
* works as expected. Also checks that the changes are properly undoable and
|
||||
* redoable. For each step in the test, we:
|
||||
* - Create a new DIV
|
||||
* - Make the change, check that the change was made as we expect
|
||||
* - Undo the change, check that the node is back in its original state
|
||||
* - Redo the change, check that the node change was made again correctly.
|
||||
*/
|
||||
|
||||
waitForExplicitFinish();
|
||||
|
||||
let TEST_URL = "data:text/html,<div>markup-view attributes addition test</div>";
|
||||
let TEST_DATA = [{
|
||||
desc: "Add an attribute value without closing \"",
|
||||
enteredText: 'style="display: block;',
|
||||
expectedAttributes: {
|
||||
style: "display: block;"
|
||||
}
|
||||
}, {
|
||||
desc: "Add an attribute value without closing '",
|
||||
enteredText: "style='display: inline;",
|
||||
expectedAttributes: {
|
||||
style: "display: inline;"
|
||||
}
|
||||
}, {
|
||||
desc: "Add an attribute wrapped with with double quotes double quote in it",
|
||||
enteredText: 'style="display: "inline',
|
||||
expectedAttributes: {
|
||||
style: "display: ",
|
||||
inline: ""
|
||||
}
|
||||
}, {
|
||||
desc: "Add an attribute wrapped with single quotes with single quote in it",
|
||||
enteredText: "style='display: 'inline",
|
||||
expectedAttributes: {
|
||||
style: "display: ",
|
||||
inline: ""
|
||||
}
|
||||
}, {
|
||||
desc: "Add an attribute with no value",
|
||||
enteredText: "disabled",
|
||||
expectedAttributes: {
|
||||
disabled: ""
|
||||
}
|
||||
}, {
|
||||
desc: "Add multiple attributes with no value",
|
||||
enteredText: "disabled autofocus",
|
||||
expectedAttributes: {
|
||||
disabled: "",
|
||||
autofocus: ""
|
||||
}
|
||||
}, {
|
||||
desc: "Add multiple attributes with no value, and some with value",
|
||||
enteredText: "disabled name='name' data-test='test' autofocus",
|
||||
expectedAttributes: {
|
||||
disabled: "",
|
||||
autofocus: "",
|
||||
name: "name",
|
||||
'data-test': "test"
|
||||
}
|
||||
}, {
|
||||
desc: "Add attribute with xmlns",
|
||||
enteredText: "xmlns:edi='http://ecommerce.example.org/schema'",
|
||||
expectedAttributes: {
|
||||
'xmlns:edi': "http://ecommerce.example.org/schema"
|
||||
}
|
||||
}, {
|
||||
desc: "Mixed single and double quotes",
|
||||
enteredText: "name=\"hi\" maxlength='not a number'",
|
||||
expectedAttributes: {
|
||||
maxlength: "not a number",
|
||||
name: "hi"
|
||||
}
|
||||
}, {
|
||||
desc: "Invalid attribute name",
|
||||
enteredText: "x='y' <why-would-you-do-this>=\"???\"",
|
||||
expectedAttributes: {
|
||||
x: "y"
|
||||
}
|
||||
}, {
|
||||
desc: "Double quote wrapped in single quotes",
|
||||
enteredText: "x='h\"i'",
|
||||
expectedAttributes: {
|
||||
x: "h\"i"
|
||||
}
|
||||
}, {
|
||||
desc: "Single quote wrapped in double quotes",
|
||||
enteredText: "x=\"h'i\"",
|
||||
expectedAttributes: {
|
||||
x: "h'i"
|
||||
}
|
||||
}, {
|
||||
desc: "No quote wrapping",
|
||||
enteredText: "a=b x=y data-test=Some spaced data",
|
||||
expectedAttributes: {
|
||||
a: "b",
|
||||
x: "y",
|
||||
"data-test": "Some",
|
||||
spaced: "",
|
||||
data: ""
|
||||
}
|
||||
}, {
|
||||
desc: "Duplicate Attributes",
|
||||
enteredText: "a=b a='c' a=\"d\"",
|
||||
expectedAttributes: {
|
||||
a: "b"
|
||||
}
|
||||
}, {
|
||||
desc: "Inline styles",
|
||||
enteredText: "style=\"font-family: 'Lucida Grande', sans-serif; font-size: 75%;\"",
|
||||
expectedAttributes: {
|
||||
style: "font-family: 'Lucida Grande', sans-serif; font-size: 75%;"
|
||||
}
|
||||
}, {
|
||||
desc: "Object attribute names",
|
||||
enteredText: "toString=\"true\" hasOwnProperty=\"false\"",
|
||||
expectedAttributes: {
|
||||
toString: "true",
|
||||
hasOwnProperty: "false"
|
||||
}
|
||||
}, {
|
||||
desc: "Add event handlers",
|
||||
enteredText: "onclick=\"javascript: throw new Error('wont fire');\" onload=\"alert('here');\"",
|
||||
expectedAttributes: {
|
||||
onclick: "javascript: throw new Error('wont fire');",
|
||||
onload: "alert('here');"
|
||||
}
|
||||
}];
|
||||
|
||||
function test() {
|
||||
Task.spawn(function() {
|
||||
info("Opening the inspector on the test URL");
|
||||
let args = yield addTab(TEST_URL).then(openInspector);
|
||||
let inspector = args.inspector;
|
||||
let markup = inspector.markup;
|
||||
|
||||
info("Selecting the test node");
|
||||
let div = getNode("div");
|
||||
yield selectNode(div, inspector);
|
||||
let editor = getContainerForRawNode(markup, div).editor;
|
||||
|
||||
for (let test of TEST_DATA) {
|
||||
info("Starting test: " + test.desc);
|
||||
|
||||
info("Enter the new attribute(s) test: " + test.enteredText);
|
||||
let nodeMutated = inspector.once("markupmutation");
|
||||
setEditableFieldValue(editor.newAttr, test.enteredText, inspector);
|
||||
yield nodeMutated;
|
||||
|
||||
info("Assert that the attribute(s) has/have been applied correctly");
|
||||
assertAttributes(div, test.expectedAttributes);
|
||||
|
||||
info("Undo the change");
|
||||
yield undoChange(inspector);
|
||||
|
||||
info("Assert that the attribute(s) has/have been removed correctly");
|
||||
assertAttributes(div, {});
|
||||
}
|
||||
|
||||
yield inspector.once("inspector-updated");
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
}).then(null, ok.bind(null, false)).then(finish);
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,51 @@
|
|||
/* Any copyright", " is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests that an existing attribute can be modified
|
||||
|
||||
waitForExplicitFinish();
|
||||
|
||||
const TEST_URL = "data:text/html,<div id='test-div'>Test modifying my ID attribute</div>";
|
||||
|
||||
function test() {
|
||||
Task.spawn(function() {
|
||||
info("Opening the inspector on the test page");
|
||||
let {toolbox, inspector} = yield addTab(TEST_URL).then(openInspector);
|
||||
|
||||
info("Selecting the test node");
|
||||
let node = content.document.getElementById("test-div");
|
||||
yield selectNode(node, inspector);
|
||||
|
||||
info("Verify attributes, only ID should be there for now");
|
||||
assertAttributes(node, {
|
||||
id: "test-div"
|
||||
});
|
||||
|
||||
info("Focus the ID attribute and change its content");
|
||||
let editor = getContainerForRawNode(inspector.markup, node).editor;
|
||||
let attr = editor.attrs["id"].querySelector(".editable");
|
||||
let mutated = inspector.once("markupmutation");
|
||||
setEditableFieldValue(attr,
|
||||
attr.textContent + ' class="newclass" style="color:green"', inspector);
|
||||
yield mutated;
|
||||
|
||||
info("Verify attributes, should have ID, class and style");
|
||||
assertAttributes(node, {
|
||||
id: "test-div",
|
||||
class: "newclass",
|
||||
style: "color:green"
|
||||
});
|
||||
|
||||
info("Trying to undo the change");
|
||||
yield undoChange(inspector);
|
||||
assertAttributes(node, {
|
||||
id: "test-div"
|
||||
});
|
||||
|
||||
yield inspector.once("inspector-updated");
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
}).then(null, ok.bind(null, false)).then(finish);
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/* Any copyright", " is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests that a node's tagname can be edited in the markup-view
|
||||
|
||||
waitForExplicitFinish();
|
||||
|
||||
const TEST_URL = "data:text/html,<div id='retag-me'><div id='retag-me-2'></div></div>";
|
||||
|
||||
function test() {
|
||||
Task.spawn(function() {
|
||||
info("Opening the inspector on the test page");
|
||||
let {toolbox, inspector} = yield addTab(TEST_URL).then(openInspector);
|
||||
|
||||
yield inspector.markup.expandAll();
|
||||
|
||||
info("Selecting the test node");
|
||||
let node = content.document.getElementById("retag-me");
|
||||
let child = content.document.querySelector("#retag-me-2");
|
||||
yield selectNode(node, inspector);
|
||||
|
||||
let container = getContainerForRawNode(inspector.markup, node);
|
||||
is(node.tagName, "DIV", "We've got #retag-me element, it's a DIV");
|
||||
ok(container.expanded, "It is expanded");
|
||||
is(child.parentNode, node, "Child #retag-me-2 is inside #retag-me");
|
||||
|
||||
info("Changing the tagname");
|
||||
let mutated = inspector.once("markupmutation");
|
||||
let tagEditor = container.editor.tag;
|
||||
setEditableFieldValue(tagEditor, "p", inspector);
|
||||
yield mutated;
|
||||
|
||||
info("Checking that the tagname change was done");
|
||||
let node = content.document.getElementById("retag-me");
|
||||
let container = getContainerForRawNode(inspector.markup, node);
|
||||
is(node.tagName, "P", "We've got #retag-me, it should now be a P");
|
||||
ok(container.expanded, "It is still expanded");
|
||||
ok(container.selected, "It is still selected");
|
||||
is(child.parentNode, node, "Child #retag-me-2 is still inside #retag-me");
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
}).then(null, ok.bind(null, false)).then(finish);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/* Any copyright", " is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests that a node can be deleted from the markup-view with the delete key
|
||||
|
||||
waitForExplicitFinish();
|
||||
|
||||
const TEST_URL = "data:text/html,<div id='delete-me'></div>";
|
||||
|
||||
function test() {
|
||||
Task.spawn(function() {
|
||||
info("Opening the inspector on the test page");
|
||||
let {toolbox, inspector} = yield addTab(TEST_URL).then(openInspector);
|
||||
|
||||
info("Selecting the test node by clicking on it to make sure it receives focus");
|
||||
let node = content.document.getElementById("delete-me");
|
||||
yield clickContainer(node, inspector);
|
||||
|
||||
info("Deleting the element with the keyboard");
|
||||
let mutated = inspector.once("markupmutation");
|
||||
EventUtils.sendKey("delete", inspector.panelWin);
|
||||
yield mutated;
|
||||
|
||||
info("Checking that it's gone, baby gone!");
|
||||
ok(!content.document.getElementById("delete-me"), "The test node does not exist");
|
||||
|
||||
yield undoChange(inspector);
|
||||
ok(content.document.getElementById("delete-me"), "The test node is back!");
|
||||
|
||||
yield inspector.once("inspector-updated");
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
}).then(null, ok.bind(null, false)).then(finish);
|
||||
}
|
|
@ -8,6 +8,7 @@ let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
|
|||
let TargetFactory = devtools.TargetFactory;
|
||||
let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
|
||||
let promise = devtools.require("sdk/core/promise");
|
||||
let {getInplaceEditorForSpan: inplaceEditor} = devtools.require("devtools/shared/inplace-editor");
|
||||
|
||||
// Clear preferences that may be set during the course of tests.
|
||||
function clearUserPrefs() {
|
||||
|
@ -18,15 +19,23 @@ function clearUserPrefs() {
|
|||
|
||||
registerCleanupFunction(clearUserPrefs);
|
||||
|
||||
Services.prefs.setBoolPref("devtools.debugger.log", true);
|
||||
SimpleTest.registerCleanupFunction(() => {
|
||||
Services.prefs.clearUserPref("devtools.debugger.log");
|
||||
});
|
||||
/**
|
||||
* Add a new test tab in the browser and load the given url.
|
||||
* @param {String} url The url to be loaded in the new tab
|
||||
* @return a promise that resolves when the url is loaded
|
||||
*/
|
||||
function addTab(url) {
|
||||
let def = promise.defer();
|
||||
|
||||
function getContainerForRawNode(markupView, rawNode) {
|
||||
let front = markupView.walker.frontForRawNode(rawNode);
|
||||
let container = markupView.getContainer(front);
|
||||
return container;
|
||||
gBrowser.selectedTab = gBrowser.addTab();
|
||||
gBrowser.selectedBrowser.addEventListener("load", function onload() {
|
||||
gBrowser.selectedBrowser.removeEventListener("load", onload, true);
|
||||
info("URL " + url + " loading complete into new test tab");
|
||||
waitForFocus(def.resolve, content);
|
||||
}, true);
|
||||
content.location = url;
|
||||
|
||||
return def.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -34,19 +43,41 @@ function getContainerForRawNode(markupView, rawNode) {
|
|||
* @return a promise that resolves when the inspector is ready
|
||||
*/
|
||||
function openInspector() {
|
||||
let deferred = promise.defer();
|
||||
let def = promise.defer();
|
||||
|
||||
let target = TargetFactory.forTab(gBrowser.selectedTab);
|
||||
gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
|
||||
info("Toolbox open");
|
||||
let inspector = toolbox.getCurrentPanel();
|
||||
inspector.once("inspector-updated", () => {
|
||||
deferred.resolve({toolbox: toolbox, inspector: inspector});
|
||||
info("Inspector panel active and ready");
|
||||
def.resolve({toolbox: toolbox, inspector: inspector});
|
||||
});
|
||||
}).then(null, console.error);
|
||||
|
||||
return deferred.promise;
|
||||
return def.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MarkupContainer object instance that corresponds to the given
|
||||
* HTML node
|
||||
* @param {MarkupView} markupView The instance of MarkupView currently loaded into the inspector panel
|
||||
* @param {DOMNode} rawNode The DOM node for which the container is required
|
||||
* @return {MarkupContainer}
|
||||
*/
|
||||
function getContainerForRawNode(markupView, rawNode) {
|
||||
let front = markupView.walker.frontForRawNode(rawNode);
|
||||
let container = markupView.getContainer(front);
|
||||
ok(container, "A markup-container object was found");
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple DOM node accesor function that takes either a node or a string css
|
||||
* selector as argument and returns the corresponding node
|
||||
* @param {String|DOMNode} nodeOrSelector
|
||||
* @return {DOMNode}
|
||||
*/
|
||||
function getNode(nodeOrSelector) {
|
||||
let node = nodeOrSelector;
|
||||
|
||||
|
@ -61,23 +92,30 @@ function getNode(nodeOrSelector) {
|
|||
/**
|
||||
* Set the inspector's current selection to a node or to the first match of the
|
||||
* given css selector
|
||||
* @param {String|DOMNode} nodeOrSelector
|
||||
* @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
|
||||
* @param {String} reason Defaults to "test" which instructs the inspector not to highlight the node upon selection
|
||||
* @return a promise that resolves when the inspector is updated with the new
|
||||
* node
|
||||
*/
|
||||
function selectNode(nodeOrSelector, inspector) {
|
||||
function selectNode(nodeOrSelector, inspector, reason="test") {
|
||||
info("Selecting the node " + nodeOrSelector);
|
||||
let node = getNode(nodeOrSelector);
|
||||
let updated = inspector.once("inspector-updated");
|
||||
inspector.selection.setNode(node, "test");
|
||||
inspector.selection.setNode(node, reason);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate a mouse-over on the markup-container (a line in the markup-view)
|
||||
* that corresponds to the node or selector passed.
|
||||
* @param {String|DOMNode} nodeOrSelector
|
||||
* @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
|
||||
* @return a promise that resolves when the container is hovered and the higlighter
|
||||
* is shown on the corresponding node
|
||||
*/
|
||||
function hoverContainer(nodeOrSelector, inspector) {
|
||||
info("Hovering over the markup-container for node " + nodeOrSelector);
|
||||
let highlit = inspector.toolbox.once("node-highlight");
|
||||
let container = getContainerForRawNode(inspector.markup, getNode(nodeOrSelector));
|
||||
EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"},
|
||||
|
@ -88,9 +126,12 @@ function hoverContainer(nodeOrSelector, inspector) {
|
|||
/**
|
||||
* Simulate a click on the markup-container (a line in the markup-view)
|
||||
* that corresponds to the node or selector passed.
|
||||
* @param {String|DOMNode} nodeOrSelector
|
||||
* @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
|
||||
* @return a promise that resolves when the node has been selected.
|
||||
*/
|
||||
function clickContainer(nodeOrSelector, inspector) {
|
||||
info("Clicking on the markup-container for node " + nodeOrSelector);
|
||||
let updated = inspector.once("inspector-updated");
|
||||
let container = getContainerForRawNode(inspector.markup, getNode(nodeOrSelector));
|
||||
EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousedown"},
|
||||
|
@ -102,6 +143,7 @@ function clickContainer(nodeOrSelector, inspector) {
|
|||
|
||||
/**
|
||||
* Checks if the highlighter is visible currently
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function isHighlighterVisible() {
|
||||
let outline = gBrowser.selectedBrowser.parentNode.querySelector(".highlighter-container .highlighter-outline");
|
||||
|
@ -110,18 +152,91 @@ function isHighlighterVisible() {
|
|||
|
||||
/**
|
||||
* Simulate the mouse leaving the markup-view area
|
||||
* @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
|
||||
* @return a promise when done
|
||||
*/
|
||||
function mouseLeaveMarkupView(inspector) {
|
||||
let deferred = promise.defer();
|
||||
info("Leaving the markup-view area");
|
||||
let def = promise.defer();
|
||||
|
||||
// Find another element to mouseover over in order to leave the markup-view
|
||||
let btn = inspector.toolbox.doc.querySelector(".toolbox-dock-button");
|
||||
|
||||
EventUtils.synthesizeMouse(btn, 2, 2, {type: "mousemove"},
|
||||
inspector.toolbox.doc.defaultView);
|
||||
executeSoon(deferred.resolve);
|
||||
executeSoon(def.resolve);
|
||||
|
||||
return deferred.promise;
|
||||
return def.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus a given editable element, enter edit mode, set value, and commit
|
||||
* @param {DOMNode} field The element that gets editable after receiving focus and <ENTER> keypress
|
||||
* @param {String} value The string value to be set into the edited field
|
||||
* @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
|
||||
*/
|
||||
function setEditableFieldValue(field, value, inspector) {
|
||||
field.focus();
|
||||
EventUtils.sendKey("return", inspector.panelWin);
|
||||
let input = inplaceEditor(field).input;
|
||||
ok(input, "Found editable field for setting value: " + value);
|
||||
input.value = value;
|
||||
EventUtils.sendKey("return", inspector.panelWin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that a node has the given attributes
|
||||
*
|
||||
* @param {HTMLNode} element The node to check.
|
||||
* @param {Object} attrs An object containing the attributes to check.
|
||||
* e.g. {id: "id1", class: "someclass"}
|
||||
*
|
||||
* Note that node.getAttribute() returns attribute values provided by the HTML
|
||||
* parser. The parser only provides unescaped entities so & will return &.
|
||||
*/
|
||||
function assertAttributes(element, attrs) {
|
||||
is(element.attributes.length, Object.keys(attrs).length,
|
||||
"Node has the correct number of attributes.");
|
||||
for (let attr in attrs) {
|
||||
is(element.getAttribute(attr), attrs[attr],
|
||||
"Node has the correct " + attr + " attribute.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo the last markup-view action and wait for the corresponding mutation to
|
||||
* occur
|
||||
* @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
|
||||
* @return a promise that resolves when the markup-mutation has been treated or
|
||||
* rejects if no undo action is possible
|
||||
*/
|
||||
function undoChange(inspector) {
|
||||
let canUndo = inspector.markup.undo.canUndo();
|
||||
ok(canUndo, "The last change in the markup-view can be undone");
|
||||
if (!canUndo) {
|
||||
return promise.reject();
|
||||
}
|
||||
|
||||
let mutated = inspector.once("markupmutation");
|
||||
inspector.markup.undo.undo();
|
||||
return mutated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redo the last markup-view action and wait for the corresponding mutation to
|
||||
* occur
|
||||
* @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
|
||||
* @return a promise that resolves when the markup-mutation has been treated or
|
||||
* rejects if no redo action is possible
|
||||
*/
|
||||
function redoChange(inspector) {
|
||||
let canRedo = inspector.markup.undo.canRedo();
|
||||
ok(canRedo, "The last change in the markup-view can be redone");
|
||||
if (!canRedo) {
|
||||
return promise.reject();
|
||||
}
|
||||
|
||||
let mutated = inspector.once("markupmutation");
|
||||
inspector.markup.undo.redo();
|
||||
return mutated;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const cssTokenizer = require("devtools/sourceeditor/css-tokenizer");
|
||||
|
||||
/**
|
||||
* Returns the string enclosed in quotes
|
||||
*/
|
||||
function quoteString(string) {
|
||||
let hasDoubleQuotes = string.contains('"');
|
||||
let hasSingleQuotes = string.contains("'");
|
||||
|
||||
if (hasDoubleQuotes && !hasSingleQuotes) {
|
||||
// In this case, no escaping required, just enclose in single-quotes
|
||||
return "'" + string + "'";
|
||||
}
|
||||
|
||||
// In all other cases, enclose in double-quotes, and escape any double-quote
|
||||
// that may be in the string
|
||||
return '"' + string.replace(/"/g, '\"') + '"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of CSS declarations given an string.
|
||||
* For example, parseDeclarations("width: 1px; height: 1px") would return
|
||||
* [{name:"width", value: "1px"}, {name: "height", "value": "1px"}]
|
||||
*
|
||||
* The input string is assumed to only contain declarations so { and } characters
|
||||
* will be treated as part of either the property or value, depending where it's
|
||||
* found.
|
||||
*
|
||||
* @param {string} inputString
|
||||
* An input string of CSS
|
||||
* @return {Array} an array of objects with the following signature:
|
||||
* [{"name": string, "value": string, "priority": string}, ...]
|
||||
*/
|
||||
function parseDeclarations(inputString) {
|
||||
let tokens = cssTokenizer(inputString);
|
||||
|
||||
let declarations = [{name: "", value: "", priority: ""}];
|
||||
|
||||
let current = "", hasBang = false, lastProp;
|
||||
for (let token of tokens) {
|
||||
lastProp = declarations[declarations.length - 1];
|
||||
|
||||
if (token.tokenType === ":") {
|
||||
if (!lastProp.name) {
|
||||
// Set the current declaration name if there's no name yet
|
||||
lastProp.name = current.trim();
|
||||
current = "";
|
||||
hasBang = false;
|
||||
} else {
|
||||
// Otherwise, just append ':' to the current value (declaration value
|
||||
// with colons)
|
||||
current += ":";
|
||||
}
|
||||
} else if (token.tokenType === ";") {
|
||||
lastProp.value = current.trim();
|
||||
current = "";
|
||||
hasBang = false;
|
||||
declarations.push({name: "", value: "", priority: ""});
|
||||
} else {
|
||||
switch(token.tokenType) {
|
||||
case "IDENT":
|
||||
if (token.value === "important" && hasBang) {
|
||||
lastProp.priority = "important";
|
||||
hasBang = false;
|
||||
} else {
|
||||
if (hasBang) {
|
||||
current += "!";
|
||||
}
|
||||
current += token.value;
|
||||
}
|
||||
break;
|
||||
case "WHITESPACE":
|
||||
current += " ";
|
||||
break;
|
||||
case "DIMENSION":
|
||||
current += token.repr;
|
||||
break;
|
||||
case "HASH":
|
||||
current += "#" + token.value;
|
||||
break;
|
||||
case "URL":
|
||||
current += "url(" + quoteString(token.value) + ")";
|
||||
break;
|
||||
case "FUNCTION":
|
||||
current += token.value + "(";
|
||||
break;
|
||||
case ")":
|
||||
current += token.tokenType;
|
||||
break;
|
||||
case "EOF":
|
||||
break;
|
||||
case "DELIM":
|
||||
if (token.value === "!") {
|
||||
hasBang = true;
|
||||
} else {
|
||||
current += token.value;
|
||||
}
|
||||
break;
|
||||
case "STRING":
|
||||
current += quoteString(token.value);
|
||||
break;
|
||||
case "{":
|
||||
case "}":
|
||||
current += token.tokenType;
|
||||
break;
|
||||
default:
|
||||
current += token.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle whatever trailing properties or values might still be there
|
||||
if (current) {
|
||||
if (!lastProp.name) {
|
||||
// Trailing property found, e.g. p1:v1;p2:v2;p3
|
||||
lastProp.name = current.trim();
|
||||
} else {
|
||||
// Trailing value found, i.e. value without an ending ;
|
||||
lastProp.value += current.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove declarations that have neither a name nor a value
|
||||
declarations = declarations.filter(prop => prop.name || prop.value);
|
||||
|
||||
return declarations;
|
||||
};
|
||||
exports.parseDeclarations = parseDeclarations;
|
||||
|
||||
/**
|
||||
* Expects a single CSS value to be passed as the input and parses the value
|
||||
* and priority.
|
||||
*
|
||||
* @param {string} value The value from the text editor.
|
||||
* @return {object} an object with 'value' and 'priority' properties.
|
||||
*/
|
||||
function parseSingleValue(value) {
|
||||
let declaration = parseDeclarations("a: " + value + ";")[0];
|
||||
return {
|
||||
value: declaration ? declaration.value : "",
|
||||
priority: declaration ? declaration.priority : ""
|
||||
};
|
||||
};
|
||||
exports.parseSingleValue = parseSingleValue;
|
|
@ -14,7 +14,8 @@ const {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles"
|
|||
const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
|
||||
const {Tooltip, SwatchColorPickerTooltip} = require("devtools/shared/widgets/Tooltip");
|
||||
const {OutputParser} = require("devtools/output-parser");
|
||||
const { PrefObserver, PREF_ORIG_SOURCES } = require("devtools/styleeditor/utils");
|
||||
const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils");
|
||||
const {parseSingleValue, parseDeclarations} = require("devtools/styleinspector/css-parsing-utils");
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
@ -582,7 +583,7 @@ Rule.prototype = {
|
|||
|
||||
let promise = aModifications.apply().then(() => {
|
||||
let cssProps = {};
|
||||
for (let cssProp of parseCSSText(this.style.cssText)) {
|
||||
for (let cssProp of parseDeclarations(this.style.cssText)) {
|
||||
cssProps[cssProp.name] = cssProp;
|
||||
}
|
||||
|
||||
|
@ -692,7 +693,7 @@ Rule.prototype = {
|
|||
_getTextProperties: function() {
|
||||
let textProps = [];
|
||||
let store = this.elementStyle.store;
|
||||
let props = parseCSSText(this.style.cssText);
|
||||
let props = parseDeclarations(this.style.cssText);
|
||||
for (let prop of props) {
|
||||
let name = prop.name;
|
||||
if (this.inherited && !domUtils.isInheritedProperty(name)) {
|
||||
|
@ -1850,17 +1851,13 @@ RuleEditor.prototype = {
|
|||
return;
|
||||
}
|
||||
|
||||
// Deal with adding declarations later (once editor has been destroyed).
|
||||
// If aValue is just a name, will make a new property with empty value.
|
||||
this.multipleAddedProperties = parseCSSText(aValue);
|
||||
if (!this.multipleAddedProperties.length) {
|
||||
this.multipleAddedProperties = [{
|
||||
name: aValue,
|
||||
value: "",
|
||||
priority: ""
|
||||
}];
|
||||
}
|
||||
// parseDeclarations allows for name-less declarations, but in the present
|
||||
// case, we're creating a new declaration, it doesn't make sense to accept
|
||||
// these entries
|
||||
this.multipleAddedProperties = parseDeclarations(aValue).filter(d => d.name);
|
||||
|
||||
// Blur the editor field now and deal with adding declarations later when
|
||||
// the field gets destroyed (see _newPropertyDestroy)
|
||||
this.editor.input.blur();
|
||||
},
|
||||
|
||||
|
@ -2263,17 +2260,16 @@ TextPropertyEditor.prototype = {
|
|||
if (aValue.trim() === "") {
|
||||
this.remove();
|
||||
} else {
|
||||
|
||||
// Adding multiple rules inside of name field overwrites the current
|
||||
// property with the first, then adds any more onto the property list.
|
||||
let properties = parseCSSText(aValue);
|
||||
if (properties.length > 0) {
|
||||
this.prop.setName(properties[0].name);
|
||||
this.prop.setValue(properties[0].value, properties[0].priority);
|
||||
let properties = parseDeclarations(aValue);
|
||||
|
||||
this.ruleEditor.addProperties(properties.slice(1), this.prop);
|
||||
} else {
|
||||
this.prop.setName(aValue);
|
||||
if (properties.length) {
|
||||
this.prop.setName(properties[0].name);
|
||||
if (properties.length > 1) {
|
||||
this.prop.setValue(properties[0].value, properties[0].priority);
|
||||
this.ruleEditor.addProperties(properties.slice(1), this.prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2320,7 +2316,7 @@ TextPropertyEditor.prototype = {
|
|||
let {propertiesToAdd,firstValue} = this._getValueAndExtraProperties(aValue);
|
||||
|
||||
// First, set this property value (common case, only modified a property)
|
||||
let val = parseCSSValue(firstValue);
|
||||
let val = parseSingleValue(firstValue);
|
||||
this.prop.setValue(val.value, val.priority);
|
||||
this.removeOnRevert = false;
|
||||
this.committed.value = this.prop.value;
|
||||
|
@ -2356,36 +2352,31 @@ TextPropertyEditor.prototype = {
|
|||
* firstValue: A string containing a simple value, like
|
||||
* "red" or "100px!important"
|
||||
* propertiesToAdd: An array with additional properties, following the
|
||||
* parseCSSText format of {name,value,priority}
|
||||
* parseDeclarations format of {name,value,priority}
|
||||
*/
|
||||
_getValueAndExtraProperties: function(aValue) {
|
||||
// The inplace editor will prevent manual typing of multiple properties,
|
||||
// but we need to deal with the case during a paste event.
|
||||
// Adding multiple properties inside of value editor sets value with the
|
||||
// first, then adds any more onto the property list (below this property).
|
||||
let properties = parseCSSText(aValue);
|
||||
let propertiesToAdd = [];
|
||||
let firstValue = aValue;
|
||||
let propertiesToAdd = [];
|
||||
|
||||
if (properties.length > 0) {
|
||||
// If text like "red; width: 1px;" was entered in, handle this as two
|
||||
// separate properties (setting value here to red and adding a new prop).
|
||||
let propertiesNoName = parseCSSText("a:" + aValue);
|
||||
let enteredValueFirst = propertiesNoName.length > properties.length;
|
||||
let properties = parseDeclarations(aValue);
|
||||
|
||||
let firstProp = properties[0];
|
||||
propertiesToAdd = properties.slice(1);
|
||||
|
||||
if (enteredValueFirst) {
|
||||
firstProp = propertiesNoName[0];
|
||||
propertiesToAdd = propertiesNoName.slice(1);
|
||||
// Check to see if the input string can be parsed as multiple properties
|
||||
if (properties.length) {
|
||||
// Get the first property value (if any), and any remaining properties (if any)
|
||||
if (!properties[0].name && properties[0].value) {
|
||||
firstValue = properties[0].value;
|
||||
propertiesToAdd = properties.slice(1);
|
||||
}
|
||||
// In some cases, the value could be a property:value pair itself.
|
||||
// Join them as one value string and append potentially following properties
|
||||
else if (properties[0].name && properties[0].value) {
|
||||
firstValue = properties[0].name + ": " + properties[0].value;
|
||||
propertiesToAdd = properties.slice(1);
|
||||
}
|
||||
|
||||
// If "red; width: 1px", then set value to "red"
|
||||
// If "color: red; width: 1px;", then set value to "color: red;"
|
||||
firstValue = enteredValueFirst ?
|
||||
firstProp.value + "!" + firstProp.priority :
|
||||
firstProp.name + ": " + firstProp.value + "!" + firstProp.priority;
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -2395,7 +2386,7 @@ TextPropertyEditor.prototype = {
|
|||
},
|
||||
|
||||
_applyNewValue: function(aValue) {
|
||||
let val = parseCSSValue(aValue);
|
||||
let val = parseSingleValue(aValue);
|
||||
// Any property should be removed if has an empty value.
|
||||
if (val.value.trim() === "") {
|
||||
this.remove();
|
||||
|
@ -2419,7 +2410,7 @@ TextPropertyEditor.prototype = {
|
|||
return;
|
||||
}
|
||||
|
||||
let val = parseCSSValue(aValue);
|
||||
let val = parseSingleValue(aValue);
|
||||
|
||||
// Live previewing the change without committing just yet, that'll be done in _onValueDone
|
||||
// If it was not a valid value, apply an empty string to reset the live preview
|
||||
|
@ -2439,7 +2430,7 @@ TextPropertyEditor.prototype = {
|
|||
isValid: function(aValue) {
|
||||
let name = this.prop.name;
|
||||
let value = typeof aValue == "undefined" ? this.prop.value : aValue;
|
||||
let val = parseCSSValue(value);
|
||||
let val = parseSingleValue(value);
|
||||
|
||||
let style = this.doc.createElementNS(HTML_NS, "div").style;
|
||||
let prefs = Services.prefs;
|
||||
|
@ -2605,64 +2596,14 @@ function throttle(func, wait, scope) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull priority (!important) out of the value provided by a
|
||||
* value editor.
|
||||
*
|
||||
* @param {string} aValue
|
||||
* The value from the text editor.
|
||||
* @return {object} an object with 'value' and 'priority' properties.
|
||||
*/
|
||||
function parseCSSValue(aValue) {
|
||||
let pieces = aValue.split("!", 2);
|
||||
return {
|
||||
value: pieces[0].trim(),
|
||||
priority: (pieces.length > 1 ? pieces[1].trim() : "")
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of CSS properties given an input string
|
||||
* For example, parseCSSText("width: 1px; height: 1px") would return
|
||||
* [{name:"width", value: "1px"}, {name: "height", "value": "1px"}]
|
||||
*
|
||||
* @param {string} aCssText
|
||||
* An input string of CSS
|
||||
* @return {Array} an array of objects with the following signature:
|
||||
* [{"name": string, "value": string, "priority": string}, ...]
|
||||
*/
|
||||
function parseCSSText(aCssText) {
|
||||
let lines = aCssText.match(CSS_LINE_RE);
|
||||
let props = [];
|
||||
|
||||
[].forEach.call(lines, (line, i) => {
|
||||
let [, name, value, priority] = CSS_PROP_RE.exec(line) || [];
|
||||
|
||||
// If this is ending with an unfinished line, add it onto the end
|
||||
// with an empty value
|
||||
if (!name && line && i > 0) {
|
||||
name = line;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
props.push({
|
||||
name: name.trim(),
|
||||
value: value || "",
|
||||
priority: priority || ""
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler that causes a blur on the target if the input has
|
||||
* multiple CSS properties as the value.
|
||||
*/
|
||||
function blurOnMultipleProperties(e) {
|
||||
setTimeout(() => {
|
||||
if (parseCSSText(e.target.value).length) {
|
||||
let props = parseDeclarations(e.target.value);
|
||||
if (props.length > 1) {
|
||||
e.target.blur();
|
||||
}
|
||||
}, 0);
|
||||
|
|
|
@ -55,7 +55,6 @@ support-files = browser_ruleview_pseudoelement.html
|
|||
[browser_bug765105_background_image_tooltip.js]
|
||||
[browser_bug889638_rule_view_color_picker.js]
|
||||
[browser_bug726427_csstransform_tooltip.js]
|
||||
|
||||
[browser_bug940500_rule_view_pick_gradient_color.js]
|
||||
[browser_ruleview_original_source_link.js]
|
||||
support-files =
|
||||
|
@ -67,3 +66,4 @@ support-files =
|
|||
[browser_bug946331_close_tooltip_on_new_selection.js]
|
||||
[browser_bug942297_user_property_reset.js]
|
||||
[browser_styleinspector_outputparser.js]
|
||||
[browser_bug970532_mathml_element.js]
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/* Any copyright", " is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests that the rule-view displays correctly on MathML elements
|
||||
|
||||
waitForExplicitFinish();
|
||||
|
||||
const TEST_URL = [
|
||||
"data:text/html,",
|
||||
"<div>",
|
||||
" <math xmlns=\"http://www.w3.org/1998/Math/MathML\">",
|
||||
" <mfrac>",
|
||||
" <msubsup>",
|
||||
" <mi>a</mi>",
|
||||
" <mi>i</mi>",
|
||||
" <mi>j</mi>",
|
||||
" </msubsup>",
|
||||
" <msub>",
|
||||
" <mi>x</mi>",
|
||||
" <mn>0</mn>",
|
||||
" </msub>",
|
||||
" </mfrac>",
|
||||
" </math>",
|
||||
"</div>"
|
||||
].join("");
|
||||
|
||||
function test() {
|
||||
gBrowser.selectedTab = gBrowser.addTab();
|
||||
gBrowser.selectedBrowser.addEventListener("load", function onload(evt) {
|
||||
gBrowser.selectedBrowser.removeEventListener("load", onload, true);
|
||||
waitForFocus(runTests, content);
|
||||
}, true);
|
||||
content.location = TEST_URL;
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
openRuleView((inspector, ruleView) => {
|
||||
Task.spawn(function() {
|
||||
info("Select the DIV node and verify the rule-view shows rules");
|
||||
yield selectNode("div", inspector);
|
||||
ok(ruleView.element.querySelectorAll(".ruleview-rule").length,
|
||||
"The rule-view shows rules for the div element");
|
||||
|
||||
info("Select various MathML nodes and verify the rule-view is empty");
|
||||
yield selectNode("math", inspector);
|
||||
ok(!ruleView.element.querySelectorAll(".ruleview-rule").length,
|
||||
"The rule-view is empty for the math element");
|
||||
|
||||
yield selectNode("msubsup", inspector);
|
||||
ok(!ruleView.element.querySelectorAll(".ruleview-rule").length,
|
||||
"The rule-view is empty for the msubsup element");
|
||||
|
||||
yield selectNode("mn", inspector);
|
||||
ok(!ruleView.element.querySelectorAll(".ruleview-rule").length,
|
||||
"The rule-view is empty for the mn element");
|
||||
|
||||
info("Select again the DIV node and verify the rule-view shows rules");
|
||||
yield selectNode("div", inspector);
|
||||
ok(ruleView.element.querySelectorAll(".ruleview-rule").length,
|
||||
"The rule-view shows rules for the div element");
|
||||
}).then(null, ok.bind(null, false)).then(finishUp);
|
||||
});
|
||||
}
|
||||
|
||||
function finishUp() {
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
}
|
|
@ -6,6 +6,8 @@ let doc;
|
|||
let ruleWindow;
|
||||
let ruleView;
|
||||
let inspector;
|
||||
let TEST_URL = 'url("http://example.com/browser/browser/devtools/' +
|
||||
'styleinspector/test/test-image.png")';
|
||||
|
||||
function startTest()
|
||||
{
|
||||
|
@ -140,7 +142,7 @@ function testEditProperty()
|
|||
let value = idRuleEditor.rule.domRule._rawStyle().getPropertyValue("border-color");
|
||||
is(value, "red", "border-color should have been set.");
|
||||
is(propEditor.isValid(), true, "red should be a valid entry");
|
||||
finishTest();
|
||||
testEditPropertyWithColon();
|
||||
}));
|
||||
});
|
||||
|
||||
|
@ -159,6 +161,43 @@ function testEditProperty()
|
|||
ruleWindow);
|
||||
}
|
||||
|
||||
function testEditPropertyWithColon()
|
||||
{
|
||||
let idRuleEditor = ruleView.element.children[1]._ruleEditor;
|
||||
let propEditor = idRuleEditor.rule.textProps[0].editor;
|
||||
waitForEditorFocus(propEditor.element, function onNewElement(aEditor) {
|
||||
is(inplaceEditor(propEditor.nameSpan), aEditor, "Next focused editor should be the name editor.");
|
||||
let input = aEditor.input;
|
||||
waitForEditorFocus(propEditor.element, function onNewName(aEditor) {
|
||||
promiseDone(expectRuleChange(idRuleEditor.rule).then(() => {
|
||||
input = aEditor.input;
|
||||
is(inplaceEditor(propEditor.valueSpan), aEditor, "Focus should have moved to the value.");
|
||||
|
||||
waitForEditorBlur(aEditor, function() {
|
||||
promiseDone(expectRuleChange(idRuleEditor.rule).then(() => {
|
||||
let value = idRuleEditor.rule.domRule._rawStyle().getPropertyValue("background-image");
|
||||
is(value, TEST_URL, "background-image should have been set.");
|
||||
is(propEditor.isValid(), true, "the test URL should be a valid entry");
|
||||
finishTest();
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
for (let ch of (TEST_URL + ";")) {
|
||||
EventUtils.sendChar(ch, ruleWindow);
|
||||
}
|
||||
}));
|
||||
});
|
||||
for (let ch of "background-image:") {
|
||||
EventUtils.sendChar(ch, ruleWindow);
|
||||
}
|
||||
});
|
||||
|
||||
EventUtils.synthesizeMouse(propEditor.nameSpan, 32, 1,
|
||||
{ },
|
||||
ruleWindow);
|
||||
}
|
||||
|
||||
function finishTest()
|
||||
{
|
||||
inspector = ruleWindow = ruleView = null;
|
||||
|
|
|
@ -28,13 +28,15 @@ function selectNewElement()
|
|||
let newElement = doc.createElement("div");
|
||||
newElement.textContent = "Test Element";
|
||||
doc.body.appendChild(newElement);
|
||||
inspector.selection.setNode(newElement);
|
||||
|
||||
inspector.selection.setNode(newElement, "test");
|
||||
let def = promise.defer();
|
||||
ruleView.element.addEventListener("CssRuleViewRefreshed", function changed() {
|
||||
ruleView.element.removeEventListener("CssRuleViewRefreshed", changed);
|
||||
elementRuleEditor = ruleView.element.children[0]._ruleEditor;
|
||||
def.resolve();
|
||||
});
|
||||
|
||||
return def.promise;
|
||||
}
|
||||
|
||||
|
|
|
@ -79,6 +79,42 @@ function openComputedView(callback)
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple DOM node accesor function that takes either a node or a string css
|
||||
* selector as argument and returns the corresponding node
|
||||
* @param {String|DOMNode} nodeOrSelector
|
||||
* @return {DOMNode}
|
||||
*/
|
||||
function getNode(nodeOrSelector)
|
||||
{
|
||||
let node = nodeOrSelector;
|
||||
|
||||
if (typeof nodeOrSelector === "string") {
|
||||
node = content.document.querySelector(nodeOrSelector);
|
||||
ok(node, "A node was found for selector " + nodeOrSelector);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the inspector's current selection to a node or to the first match of the
|
||||
* given css selector
|
||||
* @param {String|DOMNode} nodeOrSelector
|
||||
* @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
|
||||
* @param {String} reason Defaults to "test" which instructs the inspector not to highlight the node upon selection
|
||||
* @return a promise that resolves when the inspector is updated with the new
|
||||
* node
|
||||
*/
|
||||
function selectNode(nodeOrSelector, inspector, reason="test")
|
||||
{
|
||||
info("Selecting the node " + nodeOrSelector);
|
||||
let node = getNode(nodeOrSelector);
|
||||
let updated = inspector.once("inspector-updated");
|
||||
inspector.selection.setNode(node, reason);
|
||||
return updated;
|
||||
}
|
||||
|
||||
function addStyle(aDocument, aString)
|
||||
{
|
||||
let node = aDocument.createElement('style');
|
||||
|
|
|
@ -5,3 +5,4 @@
|
|||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += ['browser.ini']
|
||||
XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const Cu = Components.utils;
|
||||
Cu.import("resource://gre/modules/devtools/Loader.jsm");
|
||||
const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils");
|
||||
|
||||
const TEST_DATA = [
|
||||
// Simple test
|
||||
{
|
||||
input: "p:v;",
|
||||
expected: [{name: "p", value: "v", priority: ""}]
|
||||
},
|
||||
// Simple test
|
||||
{
|
||||
input: "this:is;a:test;",
|
||||
expected: [
|
||||
{name: "this", value: "is", priority: ""},
|
||||
{name: "a", value: "test", priority: ""}
|
||||
]
|
||||
},
|
||||
// Test a single declaration with semi-colon
|
||||
{
|
||||
input: "name:value;",
|
||||
expected: [{name: "name", value: "value", priority: ""}]
|
||||
},
|
||||
// Test a single declaration without semi-colon
|
||||
{
|
||||
input: "name:value",
|
||||
expected: [{name: "name", value: "value", priority: ""}]
|
||||
},
|
||||
// Test multiple declarations separated by whitespaces and carriage returns and tabs
|
||||
{
|
||||
input: "p1 : v1 ; \t\t \n p2:v2; \n\n\n\n\t p3 : v3;",
|
||||
expected: [
|
||||
{name: "p1", value: "v1", priority: ""},
|
||||
{name: "p2", value: "v2", priority: ""},
|
||||
{name: "p3", value: "v3", priority: ""},
|
||||
]
|
||||
},
|
||||
// Test simple priority
|
||||
{
|
||||
input: "p1: v1; p2: v2 !important;",
|
||||
expected: [
|
||||
{name: "p1", value: "v1", priority: ""},
|
||||
{name: "p2", value: "v2", priority: "important"}
|
||||
]
|
||||
},
|
||||
// Test simple priority
|
||||
{
|
||||
input: "p1: v1 !important; p2: v2",
|
||||
expected: [
|
||||
{name: "p1", value: "v1", priority: "important"},
|
||||
{name: "p2", value: "v2", priority: ""}
|
||||
]
|
||||
},
|
||||
// Test simple priority
|
||||
{
|
||||
input: "p1: v1 ! important; p2: v2 ! important;",
|
||||
expected: [
|
||||
{name: "p1", value: "v1", priority: "important"},
|
||||
{name: "p2", value: "v2", priority: "important"}
|
||||
]
|
||||
},
|
||||
// Test invalid priority
|
||||
{
|
||||
input: "p1: v1 important;",
|
||||
expected: [
|
||||
{name: "p1", value: "v1 important", priority: ""}
|
||||
]
|
||||
},
|
||||
// Test various types of background-image urls
|
||||
{
|
||||
input: "background-image: url(../../relative/image.png)",
|
||||
expected: [{name: "background-image", value: "url(\"../../relative/image.png\")", priority: ""}]
|
||||
},
|
||||
{
|
||||
input: "background-image: url(http://site.com/test.png)",
|
||||
expected: [{name: "background-image", value: "url(\"http://site.com/test.png\")", priority: ""}]
|
||||
},
|
||||
{
|
||||
input: "background-image: url(wow.gif)",
|
||||
expected: [{name: "background-image", value: "url(\"wow.gif\")", priority: ""}]
|
||||
},
|
||||
// Test that urls with :;{} characters in them are parsed correctly
|
||||
{
|
||||
input: "background: red url(\"http://site.com/image{}:;.png?id=4#wat\") repeat top right",
|
||||
expected: [
|
||||
{name: "background", value: "red url(\"http://site.com/image{}:;.png?id=4#wat\") repeat top right", priority: ""}
|
||||
]
|
||||
},
|
||||
// Test that an empty string results in an empty array
|
||||
{input: "", expected: []},
|
||||
// Test that a string comprised only of whitespaces results in an empty array
|
||||
{input: " \n \n \n \n \t \t\t\t ", expected: []},
|
||||
// Test that a null input throws an exception
|
||||
{input: null, throws: true},
|
||||
// Test that a undefined input throws an exception
|
||||
{input: undefined, throws: true},
|
||||
// Test that :;{} characters in quoted content are not parsed as multiple declarations
|
||||
{
|
||||
input: "content: \";color:red;}selector{color:yellow;\"",
|
||||
expected: [
|
||||
{name: "content", value: "\";color:red;}selector{color:yellow;\"", priority: ""}
|
||||
]
|
||||
},
|
||||
// Test that rules aren't parsed, just declarations. So { and } found after a
|
||||
// property name should be part of the property name, same for values.
|
||||
{
|
||||
input: "body {color:red;} p {color: blue;}",
|
||||
expected: [
|
||||
{name: "body {color", value: "red", priority: ""},
|
||||
{name: "} p {color", value: "blue", priority: ""},
|
||||
{name: "}", value: "", priority: ""}
|
||||
]
|
||||
},
|
||||
// Test unbalanced : and ;
|
||||
{
|
||||
input: "color :red : font : arial;",
|
||||
expected : [
|
||||
{name: "color", value: "red : font : arial", priority: ""}
|
||||
]
|
||||
},
|
||||
{input: "background: red;;;;;", expected: [{name: "background", value: "red", priority: ""}]},
|
||||
{input: "background:;", expected: [{name: "background", value: "", priority: ""}]},
|
||||
{input: ";;;;;", expected: []},
|
||||
{input: ":;:;", expected: []},
|
||||
// Test name only
|
||||
{input: "color", expected: [
|
||||
{name: "color", value: "", priority: ""}
|
||||
]},
|
||||
// Test trailing name without :
|
||||
{input: "color:blue;font", expected: [
|
||||
{name: "color", value: "blue", priority: ""},
|
||||
{name: "font", value: "", priority: ""}
|
||||
]},
|
||||
// Test trailing name with :
|
||||
{input: "color:blue;font:", expected: [
|
||||
{name: "color", value: "blue", priority: ""},
|
||||
{name: "font", value: "", priority: ""}
|
||||
]},
|
||||
// Test leading value
|
||||
{input: "Arial;color:blue;", expected: [
|
||||
{name: "", value: "Arial", priority: ""},
|
||||
{name: "color", value: "blue", priority: ""}
|
||||
]},
|
||||
// Test hex colors
|
||||
{input: "color: #333", expected: [{name: "color", value: "#333", priority: ""}]},
|
||||
{input: "color: #456789", expected: [{name: "color", value: "#456789", priority: ""}]},
|
||||
{input: "wat: #XYZ", expected: [{name: "wat", value: "#XYZ", priority: ""}]},
|
||||
// Test string/url quotes escaping
|
||||
{input: "content: \"this is a 'string'\"", expected: [{name: "content", value: "\"this is a 'string'\"", priority: ""}]},
|
||||
{input: 'content: "this is a \\"string\\""', expected: [{name: "content", value: '\'this is a "string"\'', priority: ""}]},
|
||||
{input: "content: 'this is a \"string\"'", expected: [{name: "content", value: '\'this is a "string"\'', priority: ""}]},
|
||||
{input: "content: 'this is a \\'string\\'", expected: [{name: "content", value: '"this is a \'string\'"', priority: ""}]},
|
||||
{input: "content: 'this \\' is a \" really strange string'", expected: [{name: "content", value: '"this \' is a \" really strange string"', priority: ""}]},
|
||||
{
|
||||
input: "content: \"a not s\\\
|
||||
o very long title\"",
|
||||
expected: [
|
||||
{name: "content", value: '"a not s\
|
||||
o very long title"', priority: ""}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
function run_test() {
|
||||
for (let test of TEST_DATA) {
|
||||
do_print("Test input string " + test.input);
|
||||
let output;
|
||||
try {
|
||||
output = parseDeclarations(test.input);
|
||||
} catch (e) {
|
||||
do_print("parseDeclarations threw an exception with the given input string");
|
||||
if (test.throws) {
|
||||
do_print("Exception expected");
|
||||
do_check_true(true);
|
||||
} else {
|
||||
do_print("Exception unexpected\n" + e);
|
||||
do_check_true(false);
|
||||
}
|
||||
}
|
||||
if (output) {
|
||||
assertOutput(output, test.expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertOutput(actual, expected) {
|
||||
if (actual.length === expected.length) {
|
||||
for (let i = 0; i < expected.length; i ++) {
|
||||
do_check_true(!!actual[i]);
|
||||
do_print("Check that the output item has the expected name, value and priority");
|
||||
do_check_eq(expected[i].name, actual[i].name);
|
||||
do_check_eq(expected[i].value, actual[i].value);
|
||||
do_check_eq(expected[i].priority, actual[i].priority);
|
||||
}
|
||||
} else {
|
||||
for (let prop of actual) {
|
||||
do_print("Actual output contained: {name: "+prop.name+", value: "+prop.value+", priority: "+prop.priority+"}");
|
||||
}
|
||||
do_check_eq(actual.length, expected.length);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const Cu = Components.utils;
|
||||
Cu.import("resource://gre/modules/devtools/Loader.jsm");
|
||||
const {parseSingleValue} = devtools.require("devtools/styleinspector/css-parsing-utils");
|
||||
|
||||
const TEST_DATA = [
|
||||
{input: null, throws: true},
|
||||
{input: undefined, throws: true},
|
||||
{input: "", expected: {value: "", priority: ""}},
|
||||
{input: " \t \t \n\n ", expected: {value: "", priority: ""}},
|
||||
{input: "blue", expected: {value: "blue", priority: ""}},
|
||||
{input: "blue !important", expected: {value: "blue", priority: "important"}},
|
||||
{input: "blue!important", expected: {value: "blue", priority: "important"}},
|
||||
{input: "blue ! important", expected: {value: "blue", priority: "important"}},
|
||||
{input: "blue ! important", expected: {value: "blue", priority: "important"}},
|
||||
{input: "blue !", expected: {value: "blue", priority: ""}},
|
||||
{input: "blue !mportant", expected: {value: "blue !mportant", priority: ""}},
|
||||
{input: " blue !important ", expected: {value: "blue", priority: "important"}},
|
||||
{
|
||||
input: "url(\"http://url.com/whyWouldYouDoThat!important.png\") !important",
|
||||
expected: {
|
||||
value: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
|
||||
priority: "important"
|
||||
}
|
||||
},
|
||||
{
|
||||
input: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
|
||||
expected: {
|
||||
value: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
|
||||
priority: ""
|
||||
}
|
||||
},
|
||||
{
|
||||
input: "\"content!important\" !important",
|
||||
expected: {
|
||||
value: "\"content!important\"",
|
||||
priority: "important"
|
||||
}
|
||||
},
|
||||
{
|
||||
input: "\"content!important\"",
|
||||
expected: {
|
||||
value: "\"content!important\"",
|
||||
priority: ""
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function run_test() {
|
||||
for (let test of TEST_DATA) {
|
||||
do_print("Test input value " + test.input);
|
||||
try {
|
||||
let output = parseSingleValue(test.input);
|
||||
assertOutput(output, test.expected);
|
||||
} catch (e) {
|
||||
do_print("parseSingleValue threw an exception with the given input value");
|
||||
if (test.throws) {
|
||||
do_print("Exception expected");
|
||||
do_check_true(true);
|
||||
} else {
|
||||
do_print("Exception unexpected\n" + e);
|
||||
do_check_true(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertOutput(actual, expected) {
|
||||
do_print("Check that the output has the expected value and priority");
|
||||
do_check_eq(expected.value, actual.value);
|
||||
do_check_eq(expected.priority, actual.priority);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
[DEFAULT]
|
||||
head =
|
||||
tail =
|
||||
firefox-appdir = browser
|
||||
|
||||
[test_parseDeclarations.js]
|
||||
[test_parseSingleValue.js]
|
|
@ -12,51 +12,36 @@
|
|||
const TEST_URI = "data:text/html;charset=utf-8,<div style='font-size:3em;" +
|
||||
"foobarCssParser:baz'>test CSS parser filter</div>";
|
||||
|
||||
function onContentLoaded()
|
||||
{
|
||||
browser.removeEventListener("load", onContentLoaded, true);
|
||||
|
||||
let HUD = HUDService.getHudByWindow(content);
|
||||
let hudId = HUD.hudId;
|
||||
let outputNode = HUD.outputNode;
|
||||
|
||||
HUD.jsterm.clearOutput();
|
||||
|
||||
waitForSuccess({
|
||||
name: "css error displayed",
|
||||
validatorFn: function()
|
||||
{
|
||||
return outputNode.textContent.indexOf("foobarCssParser") > -1;
|
||||
},
|
||||
successFn: function()
|
||||
{
|
||||
HUD.setFilterState("cssparser", false);
|
||||
|
||||
let msg = "the unknown CSS property warning is not displayed, " +
|
||||
"after filtering";
|
||||
testLogEntry(outputNode, "foobarCssParser", msg, true, true);
|
||||
|
||||
HUD.setFilterState("cssparser", true);
|
||||
finishTest();
|
||||
},
|
||||
failureFn: finishTest,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unit test for bug 589162:
|
||||
* CSS filtering on the console does not work
|
||||
*/
|
||||
function test()
|
||||
{
|
||||
addTab(TEST_URI);
|
||||
browser.addEventListener("load", function onLoad() {
|
||||
browser.removeEventListener("load", onLoad, true);
|
||||
function test() {
|
||||
Task.spawn(runner).then(finishTest);
|
||||
|
||||
openConsole(null, function() {
|
||||
browser.addEventListener("load", onContentLoaded, true);
|
||||
content.location.reload();
|
||||
function* runner() {
|
||||
let {tab} = yield loadTab(TEST_URI);
|
||||
let hud = yield openConsole(tab);
|
||||
|
||||
// CSS warnings are disabled by default.
|
||||
hud.setFilterState("cssparser", true);
|
||||
hud.jsterm.clearOutput();
|
||||
|
||||
content.location.reload();
|
||||
|
||||
yield waitForMessages({
|
||||
webconsole: hud,
|
||||
messages: [{
|
||||
text: "foobarCssParser",
|
||||
category: CATEGORY_CSS,
|
||||
severity: SEVERITY_WARNING,
|
||||
}],
|
||||
});
|
||||
}, true);
|
||||
}
|
||||
|
||||
hud.setFilterState("cssparser", false);
|
||||
|
||||
let msg = "the unknown CSS property warning is not displayed, " +
|
||||
"after filtering";
|
||||
testLogEntry(hud.outputNode, "foobarCssParser", msg, true, true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,9 @@ XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
|
|||
"@mozilla.org/xre/app-info;1", "nsICrashReporter");
|
||||
#endif
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CrashMonitor",
|
||||
"resource://gre/modules/CrashMonitor.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
|
||||
"@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
|
||||
|
||||
|
@ -76,6 +79,25 @@ SessionStore.prototype = {
|
|||
// swallow exception that occurs if metro-tabs measure is already set up
|
||||
}
|
||||
|
||||
CrashMonitor.previousCheckpoints.then(checkpoints => {
|
||||
let previousSessionCrashed = false;
|
||||
|
||||
if (checkpoints) {
|
||||
// If the previous session finished writing the final state, we'll
|
||||
// assume there was no crash.
|
||||
previousSessionCrashed = !checkpoints["sessionstore-final-state-write-complete"];
|
||||
} else {
|
||||
// If no checkpoints are saved, this is the first run with CrashMonitor or the
|
||||
// metroSessionCheckpoints file was corrupted/deleted, so fallback to defining
|
||||
// a crash as init-ing with an unexpected previousExecutionState
|
||||
// 1 == RUNNING, 2 == SUSPENDED
|
||||
previousSessionCrashed = Services.metro.previousExecutionState == 1 ||
|
||||
Services.metro.previousExecutionState == 2;
|
||||
}
|
||||
|
||||
Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!previousSessionCrashed);
|
||||
});
|
||||
|
||||
try {
|
||||
let shutdownWasUnclean = false;
|
||||
|
||||
|
@ -291,8 +313,8 @@ SessionStore.prototype = {
|
|||
if (this._saveTimer) {
|
||||
this._saveTimer.cancel();
|
||||
this._saveTimer = null;
|
||||
this.saveState();
|
||||
}
|
||||
this.saveState();
|
||||
break;
|
||||
case "browser:purge-session-history": // catch sanitization
|
||||
this._clearDisk();
|
||||
|
@ -644,6 +666,9 @@ SessionStore.prototype = {
|
|||
let istream = converter.convertToInputStream(aData);
|
||||
NetUtil.asyncCopy(istream, ostream, function(rc) {
|
||||
if (Components.isSuccessCode(rc)) {
|
||||
if (Services.startup.shuttingDown) {
|
||||
Services.obs.notifyObservers(null, "sessionstore-final-state-write-complete", "");
|
||||
}
|
||||
Services.obs.notifyObservers(null, "sessionstore-state-write-complete", "");
|
||||
}
|
||||
});
|
||||
|
|
|
@ -109,6 +109,10 @@
|
|||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.theme-light .ruleview-overridden {
|
||||
-moz-text-decoration-color: #667380; /* Content (Text) - Dark Grey */
|
||||
}
|
||||
|
||||
.styleinspector-propertyeditor {
|
||||
border: 1px solid #CCC;
|
||||
padding: 0;
|
||||
|
|
|
@ -113,6 +113,10 @@
|
|||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.theme-light .ruleview-overridden {
|
||||
-moz-text-decoration-color: #667380; /* Content (Text) - Dark Grey */
|
||||
}
|
||||
|
||||
.styleinspector-propertyeditor {
|
||||
border: 1px solid #CCC;
|
||||
padding: 0;
|
||||
|
|
|
@ -104,6 +104,15 @@
|
|||
inset 0 1px rgb(196, 196, 196);
|
||||
}
|
||||
|
||||
#customization-undo-reset {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
%ifdef XP_MACOSX
|
||||
padding-top: 6px;
|
||||
%else
|
||||
padding-top: 7px;
|
||||
%endif
|
||||
}
|
||||
|
||||
#main-window[customize-entered] #customization-panel-container {
|
||||
background-image: url("chrome://browser/skin/customizableui/customizeMode-separatorHorizontal.png"),
|
||||
|
|
|
@ -109,6 +109,10 @@
|
|||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.theme-light .ruleview-overridden {
|
||||
-moz-text-decoration-color: #667380; /* Content (Text) - Dark Grey */
|
||||
}
|
||||
|
||||
.styleinspector-propertyeditor {
|
||||
border: 1px solid #CCC;
|
||||
padding: 0;
|
||||
|
|
|
@ -73,7 +73,7 @@ crashreporter
|
|||
Always defined.
|
||||
|
||||
datareporting
|
||||
Whether data reporting (MOZ_DATA_REPORTING) is enabled for this build.
|
||||
Whether data reporting (MOZ_DATA_REPORTING) is enabled for this build.
|
||||
|
||||
Values are ``true`` and ``false``.
|
||||
|
||||
|
@ -86,6 +86,13 @@ debug
|
|||
|
||||
Always defined.
|
||||
|
||||
healthreport
|
||||
Whether the Health Report feature is enabled.
|
||||
|
||||
Values are ``true`` and ``false``.
|
||||
|
||||
Always defined.
|
||||
|
||||
mozconfig
|
||||
The path of the :ref:`mozconfig file <mozconfig>` used to produce this build.
|
||||
|
||||
|
@ -133,3 +140,16 @@ topsrcdir
|
|||
|
||||
Always defined.
|
||||
|
||||
wave
|
||||
Whether Wave audio support is enabled.
|
||||
|
||||
Values are ``true`` and ``false``.
|
||||
|
||||
Always defined.
|
||||
|
||||
webm
|
||||
Whether WebM support is enabled.
|
||||
|
||||
Values are ``true`` and ``false``.
|
||||
|
||||
Always defined.
|
||||
|
|
|
@ -249,6 +249,7 @@
|
|||
android:label="@string/crash_reporter_title"
|
||||
android:icon="@drawable/crash_reporter"
|
||||
android:theme="@style/Gecko"
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true">
|
||||
<intent-filter>
|
||||
<action android:name="org.mozilla.gecko.reportCrash" />
|
||||
|
|
|
@ -12,6 +12,7 @@ import com.squareup.picasso.Picasso;
|
|||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
@ -50,9 +51,14 @@ public class PanelListRow extends TwoLineRow {
|
|||
int imageIndex = cursor.getColumnIndexOrThrow(HomeItems.IMAGE_URL);
|
||||
final String imageUrl = cursor.getString(imageIndex);
|
||||
|
||||
Picasso.with(getContext())
|
||||
.load(imageUrl)
|
||||
.error(R.drawable.favicon)
|
||||
.into(mIcon);
|
||||
final boolean hasImageUrl = !TextUtils.isEmpty(imageUrl);
|
||||
mIcon.setVisibility(hasImageUrl ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (hasImageUrl) {
|
||||
Picasso.with(getContext())
|
||||
.load(imageUrl)
|
||||
.error(R.drawable.favicon)
|
||||
.into(mIcon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
|
|||
XPCOMUtils.defineLazyModuleGetter(this, "HelperApps",
|
||||
"resource://gre/modules/HelperApps.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SSLExceptions",
|
||||
"resource://gre/modules/SSLExceptions.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
|
||||
"resource://gre/modules/FormHistory.jsm");
|
||||
|
||||
|
@ -289,6 +292,14 @@ var BrowserApp = {
|
|||
dump("zerdatime " + Date.now() + " - browser chrome startup finished.");
|
||||
|
||||
this.deck = document.getElementById("browsers");
|
||||
this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() {
|
||||
try {
|
||||
BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);
|
||||
Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
|
||||
sendMessageToJava({ type: "Gecko:DelayedStartup" });
|
||||
} catch(ex) { console.log(ex); }
|
||||
}, false);
|
||||
|
||||
BrowserEventHandler.init();
|
||||
ViewportHandler.init();
|
||||
|
||||
|
@ -3790,8 +3801,9 @@ Tab.prototype = {
|
|||
}
|
||||
|
||||
// Show page actions for helper apps.
|
||||
if (BrowserApp.selectedTab == this)
|
||||
ExternalApps.updatePageAction(this.browser.currentURI);
|
||||
let uri = this.browser.currentURI;
|
||||
if (BrowserApp.selectedTab == this && ExternalApps.shouldCheckUri(uri))
|
||||
ExternalApps.updatePageAction(uri);
|
||||
|
||||
if (!Reader.isEnabledForParseOnLoad)
|
||||
return;
|
||||
|
@ -3800,7 +3812,7 @@ Tab.prototype = {
|
|||
Reader.parseDocumentFromTab(this.id, function (article) {
|
||||
// Do nothing if there's no article or the page in this tab has
|
||||
// changed
|
||||
let tabURL = this.browser.currentURI.specIgnoringRef;
|
||||
let tabURL = uri.specIgnoringRef;
|
||||
if (article == null || (article.url != tabURL)) {
|
||||
// Don't clear the article for about:reader pages since we want to
|
||||
// use the article from the previous page
|
||||
|
@ -8004,6 +8016,14 @@ var ExternalApps = {
|
|||
HelperApps.launchUri(uri);
|
||||
},
|
||||
|
||||
shouldCheckUri: function(uri) {
|
||||
if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
updatePageAction: function updatePageAction(uri) {
|
||||
let apps = HelperApps.getAppsForUri(uri);
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
|
||||
<script type="application/javascript" src="chrome://browser/content/browser.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/downloads.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/exceptions.js"/>
|
||||
|
||||
<deck id="browsers" flex="1"/>
|
||||
|
||||
|
|
|
@ -34,7 +34,6 @@ chrome.jar:
|
|||
* content/browser.js (content/browser.js)
|
||||
content/bindings/checkbox.xml (content/bindings/checkbox.xml)
|
||||
content/bindings/settings.xml (content/bindings/settings.xml)
|
||||
content/exceptions.js (content/exceptions.js)
|
||||
content/downloads.js (content/downloads.js)
|
||||
content/netError.xhtml (content/netError.xhtml)
|
||||
content/SelectHelper.js (content/SelectHelper.js)
|
||||
|
|
|
@ -88,6 +88,9 @@ BrowserCLH.prototype = {
|
|||
if (!uri)
|
||||
return;
|
||||
|
||||
// Let's get a head start on opening the network connection to the URI we are about to load
|
||||
Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
|
||||
|
||||
let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
|
||||
if (browserWin) {
|
||||
if (!pinned) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict"
|
||||
|
||||
let Cc = Components.classes;
|
||||
let Ci = Components.interfaces;
|
||||
|
@ -8,6 +9,8 @@ let Cu = Components.utils;
|
|||
|
||||
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["SSLExceptions"];
|
||||
|
||||
/**
|
||||
A class to add exceptions to override SSL certificate problems. The functionality
|
||||
itself is borrowed from exceptionDialog.js.
|
||||
|
@ -48,8 +51,8 @@ SSLExceptions.prototype = {
|
|||
*/
|
||||
_checkCert: function SSLE_checkCert(aURI) {
|
||||
this._sslStatus = null;
|
||||
|
||||
let req = new XMLHttpRequest();
|
||||
|
||||
let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
|
||||
try {
|
||||
if (aURI) {
|
||||
req.open("GET", aURI.prePath, false);
|
|
@ -17,6 +17,7 @@ EXTRA_JS_MODULES += [
|
|||
'Sanitizer.jsm',
|
||||
'SharedPreferences.jsm',
|
||||
'SimpleServiceDiscovery.jsm',
|
||||
'SSLExceptions.jsm',
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_ANDROID_SYNTHAPKS']:
|
||||
|
|
|
@ -78,6 +78,7 @@ def build_dict(config, env=os.environ):
|
|||
d['debug'] = substs.get('MOZ_DEBUG') == '1'
|
||||
d['crashreporter'] = bool(substs.get('MOZ_CRASHREPORTER'))
|
||||
d['datareporting'] = bool(substs.get('MOZ_DATA_REPORTING'))
|
||||
d['healthreport'] = substs.get('MOZ_SERVICES_HEALTHREPORT') == '1'
|
||||
d['asan'] = substs.get('MOZ_ASAN') == '1'
|
||||
d['tests_enabled'] = substs.get('ENABLE_TESTS') == "1"
|
||||
d['bin_suffix'] = substs.get('BIN_SUFFIX', '')
|
||||
|
|
|
@ -15,8 +15,7 @@ Cu.import("resource://services-crypto/utils.js");
|
|||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://gre/modules/Credentials.jsm");
|
||||
|
||||
// Default can be changed by the preference 'identity.fxaccounts.auth.uri'
|
||||
let _host = "https://api-accounts.dev.lcip.org/v1";
|
||||
let _host = "https://api.accounts.firefox.com/v1"
|
||||
try {
|
||||
_host = Services.prefs.getCharPref("identity.fxaccounts.auth.uri");
|
||||
} catch(keepDefault) {}
|
||||
|
|
|
@ -1313,6 +1313,74 @@ Example
|
|||
"google.urlbar": 7
|
||||
},
|
||||
|
||||
org.mozilla.sync.sync
|
||||
---------------------
|
||||
|
||||
This daily measurement contains information about the Sync service.
|
||||
|
||||
Values should be recorded for every day FHR measurements occurred.
|
||||
|
||||
Version 1
|
||||
^^^^^^^^^
|
||||
|
||||
This version debuted with Firefox 30 on desktop. It contains the following
|
||||
properties:
|
||||
|
||||
enabled
|
||||
Daily numeric indicating whether Sync is configured and enabled. 1 if so,
|
||||
0 otherwise.
|
||||
|
||||
preferredProtocol
|
||||
String version of the maximum Sync protocol version the client supports.
|
||||
This will be ``1.1`` for for legacy Sync and ``1.5`` for clients that
|
||||
speak the Firefox Accounts protocol.
|
||||
|
||||
actualProtocol
|
||||
The actual Sync protocol version the client is configured to use.
|
||||
|
||||
This will be ``1.1`` if the client is configured with the legacy Sync
|
||||
service or if the client only supports ``1.1``.
|
||||
|
||||
It will be ``1.5`` if the client supports ``1.5`` and either a) the
|
||||
client is not configured b) the client is using Firefox Accounts Sync.
|
||||
|
||||
syncStart
|
||||
Count of sync operations performed.
|
||||
|
||||
syncSuccess
|
||||
Count of sync operations that completed successfully.
|
||||
|
||||
syncError
|
||||
Count of sync operations that did not complete successfully.
|
||||
|
||||
This is a measure of overall sync success. This does *not* reflect
|
||||
recoverable errors (such as record conflict) that can occur during
|
||||
sync. This is thus a rough proxy of whether the sync service is
|
||||
operating without error.
|
||||
|
||||
org.mozilla.sync.devices
|
||||
------------------------
|
||||
|
||||
This daily measurement contains information about the device type composition
|
||||
for the configured Sync account.
|
||||
|
||||
Version 1
|
||||
^^^^^^^^^
|
||||
|
||||
Version 1 was introduced with Firefox 30.
|
||||
|
||||
Field names are dynamic according to the client-reported device types from
|
||||
Sync records. All fields are daily last seen integer values corresponding to
|
||||
the number of devices of that type.
|
||||
|
||||
Common values include:
|
||||
|
||||
desktop
|
||||
Corresponds to a Firefox desktop client.
|
||||
|
||||
mobile
|
||||
Corresponds to a Fennec client.
|
||||
|
||||
org.mozilla.sysinfo.sysinfo
|
||||
---------------------------
|
||||
|
||||
|
|
|
@ -160,10 +160,14 @@ this.ProviderManager.prototype = Object.freeze({
|
|||
* @return Promise<null>
|
||||
*/
|
||||
registerProvider: function (provider) {
|
||||
if (!(provider instanceof Provider)) {
|
||||
throw new Error("Argument must be a Provider instance.");
|
||||
// We should perform an instanceof check here. However, due to merged
|
||||
// compartments, the Provider type may belong to one of two JSMs
|
||||
// isinstance gets confused depending on which module Provider comes
|
||||
// from. Some code references Provider from dataprovider.jsm; others from
|
||||
// Metrics.jsm.
|
||||
if (!provider.name) {
|
||||
throw new Error("Provider is not valid: does not have a name.");
|
||||
}
|
||||
|
||||
if (this._providers.has(provider.name)) {
|
||||
return CommonUtils.laterTickResolvingPromise();
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ add_task(function test_register_provider() {
|
|||
try {
|
||||
manager.registerProvider({});
|
||||
} catch (ex) {
|
||||
do_check_true(ex.message.startsWith("Argument must be a Provider"));
|
||||
do_check_true(ex.message.startsWith("Provider is not valid"));
|
||||
failed = true;
|
||||
} finally {
|
||||
do_check_true(failed);
|
||||
|
|
|
@ -20,6 +20,7 @@ sync_modules := \
|
|||
addonutils.js \
|
||||
browserid_identity.js \
|
||||
engines.js \
|
||||
healthreport.jsm \
|
||||
identity.js \
|
||||
jpakeclient.js \
|
||||
keys.js \
|
||||
|
|
|
@ -23,3 +23,7 @@ contract @mozilla.org/network/protocol/about;1?what=sync-log {d28f8a0b-95da-48f4
|
|||
# Register resource aliases
|
||||
# (Note, for tests these are also set up in addResourceAlias)
|
||||
resource services-sync resource://gre/modules/services-sync/
|
||||
|
||||
#ifdef MOZ_SERVICES_HEALTHREPORT
|
||||
category healthreport-js-provider-default SyncProvider resource://services-sync/healthreport.jsm
|
||||
#endif
|
||||
|
|
|
@ -92,6 +92,11 @@ WeaveService.prototype = {
|
|||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether Firefox Accounts is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
get fxAccountsEnabled() {
|
||||
// work out what identity manager to use. This is stored in a preference;
|
||||
// if the preference exists, we trust it.
|
||||
|
@ -112,6 +117,21 @@ WeaveService.prototype = {
|
|||
return fxAccountsEnabled;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether Sync appears to be enabled.
|
||||
*
|
||||
* This returns true if all the Sync preferences for storing account
|
||||
* and server configuration are populated.
|
||||
*
|
||||
* It does *not* perform a robust check to see if the client is working.
|
||||
* For that, you'll want to check Weave.Status.checkSetup().
|
||||
*/
|
||||
get enabled() {
|
||||
let prefs = Services.prefs.getBranch(SYNC_PREFS_BRANCH);
|
||||
return prefs.prefHasUserValue("username") &&
|
||||
prefs.prefHasUserValue("clusterURL");
|
||||
},
|
||||
|
||||
observe: function (subject, topic, data) {
|
||||
switch (topic) {
|
||||
case "app-startup":
|
||||
|
|
|
@ -71,6 +71,28 @@ ClientEngine.prototype = {
|
|||
return stats;
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtain information about device types.
|
||||
*
|
||||
* Returns a Map of device types to integer counts.
|
||||
*/
|
||||
get deviceTypes() {
|
||||
let counts = new Map();
|
||||
|
||||
counts.set(this.localType, 1);
|
||||
|
||||
for each (let record in this._store._remoteClients) {
|
||||
let type = record.type;
|
||||
if (!counts.has(type)) {
|
||||
counts.set(type, 0);
|
||||
}
|
||||
|
||||
counts.set(type, counts.get(type) + 1);
|
||||
}
|
||||
|
||||
return counts;
|
||||
},
|
||||
|
||||
get localID() {
|
||||
// Generate a random GUID id we don't have one
|
||||
let localID = Svc.Prefs.get("client.GUID", "");
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"SyncProvider",
|
||||
];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Metrics.jsm", this);
|
||||
Cu.import("resource://gre/modules/Promise.jsm", this);
|
||||
Cu.import("resource://gre/modules/Services.jsm", this);
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
|
||||
|
||||
const DAILY_LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC};
|
||||
const DAILY_LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
|
||||
const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER};
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Weave",
|
||||
"resource://services-sync/main.js");
|
||||
|
||||
function SyncMeasurement1() {
|
||||
Metrics.Measurement.call(this);
|
||||
}
|
||||
|
||||
SyncMeasurement1.prototype = Object.freeze({
|
||||
__proto__: Metrics.Measurement.prototype,
|
||||
|
||||
name: "sync",
|
||||
version: 1,
|
||||
|
||||
fields: {
|
||||
enabled: DAILY_LAST_NUMERIC_FIELD,
|
||||
preferredProtocol: DAILY_LAST_TEXT_FIELD,
|
||||
activeProtocol: DAILY_LAST_TEXT_FIELD,
|
||||
syncStart: DAILY_COUNTER_FIELD,
|
||||
syncSuccess: DAILY_COUNTER_FIELD,
|
||||
syncError: DAILY_COUNTER_FIELD,
|
||||
},
|
||||
});
|
||||
|
||||
function SyncDevicesMeasurement1() {
|
||||
Metrics.Measurement.call(this);
|
||||
}
|
||||
|
||||
SyncDevicesMeasurement1.prototype = Object.freeze({
|
||||
__proto__: Metrics.Measurement.prototype,
|
||||
|
||||
name: "devices",
|
||||
version: 1,
|
||||
|
||||
fields: {},
|
||||
|
||||
shouldIncludeField: function (name) {
|
||||
return true;
|
||||
},
|
||||
|
||||
fieldType: function (name) {
|
||||
return Metrics.Storage.FIELD_DAILY_COUNTER;
|
||||
},
|
||||
});
|
||||
|
||||
this.SyncProvider = function () {
|
||||
Metrics.Provider.call(this);
|
||||
};
|
||||
SyncProvider.prototype = Object.freeze({
|
||||
__proto__: Metrics.Provider.prototype,
|
||||
|
||||
name: "org.mozilla.sync",
|
||||
|
||||
measurementTypes: [
|
||||
SyncDevicesMeasurement1,
|
||||
SyncMeasurement1,
|
||||
],
|
||||
|
||||
_OBSERVERS: [
|
||||
"weave:service:sync:start",
|
||||
"weave:service:sync:finish",
|
||||
"weave:service:sync:error",
|
||||
],
|
||||
|
||||
postInit: function () {
|
||||
for (let o of this._OBSERVERS) {
|
||||
Services.obs.addObserver(this, o, false);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
onShutdown: function () {
|
||||
for (let o of this._OBSERVERS) {
|
||||
Services.obs.removeObserver(this, o);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
observe: function (subject, topic, data) {
|
||||
let field;
|
||||
switch (topic) {
|
||||
case "weave:service:sync:start":
|
||||
field = "syncStart";
|
||||
break;
|
||||
|
||||
case "weave:service:sync:finish":
|
||||
field = "syncSuccess";
|
||||
break;
|
||||
|
||||
case "weave:service:sync:error":
|
||||
field = "syncError";
|
||||
break;
|
||||
}
|
||||
|
||||
let m = this.getMeasurement(SyncMeasurement1.prototype.name,
|
||||
SyncMeasurement1.prototype.version);
|
||||
return this.enqueueStorageOperation(function recordSyncEvent() {
|
||||
return m.incrementDailyCounter(field);
|
||||
});
|
||||
},
|
||||
|
||||
collectDailyData: function () {
|
||||
return this.storage.enqueueTransaction(this._populateDailyData.bind(this));
|
||||
},
|
||||
|
||||
_populateDailyData: function* () {
|
||||
let m = this.getMeasurement(SyncMeasurement1.prototype.name,
|
||||
SyncMeasurement1.prototype.version);
|
||||
|
||||
let svc = Cc["@mozilla.org/weave/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject;
|
||||
|
||||
let enabled = svc.enabled;
|
||||
yield m.setDailyLastNumeric("enabled", enabled ? 1 : 0);
|
||||
|
||||
// preferredProtocol is constant and only changes as the client
|
||||
// evolves.
|
||||
yield m.setDailyLastText("preferredProtocol", "1.5");
|
||||
|
||||
let protocol = svc.fxAccountsEnabled ? "1.5" : "1.1";
|
||||
yield m.setDailyLastText("activeProtocol", protocol);
|
||||
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Before grabbing more information, be sure the Sync service
|
||||
// is fully initialized. This has the potential to initialize
|
||||
// Sync on the spot. This may be undesired if Sync appears to
|
||||
// be enabled but it really isn't. That responsibility should
|
||||
// be up to svc.enabled to not return false positives, however.
|
||||
yield svc.whenLoaded();
|
||||
|
||||
if (Weave.Status.service != Weave.STATUS_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Device types are dynamic. So we need to dynamically create fields if
|
||||
// they don't exist.
|
||||
let dm = this.getMeasurement(SyncDevicesMeasurement1.prototype.name,
|
||||
SyncDevicesMeasurement1.prototype.version);
|
||||
let devices = Weave.Service.clientsEngine.deviceTypes;
|
||||
for (let [field, count] of devices) {
|
||||
let hasField = this.storage.hasFieldFromMeasurement(dm.id, field,
|
||||
this.storage.FIELD_DAILY_LAST_NUMERIC);
|
||||
let fieldID;
|
||||
if (hasField) {
|
||||
fieldID = this.storage.fieldIDFromMeasurement(dm.id, field);
|
||||
} else {
|
||||
fieldID = yield this.storage.registerField(dm.id, field,
|
||||
this.storage.FIELD_DAILY_LAST_NUMERIC);
|
||||
}
|
||||
|
||||
yield this.storage.setDailyLastNumericFromFieldID(fieldID, count);
|
||||
}
|
||||
},
|
||||
});
|
|
@ -8,6 +8,9 @@ DIRS += ['locales']
|
|||
TEST_DIRS += ['tests']
|
||||
|
||||
EXTRA_COMPONENTS += [
|
||||
'SyncComponents.manifest',
|
||||
'Weave.js',
|
||||
]
|
||||
|
||||
EXTRA_PP_COMPONENTS += [
|
||||
'SyncComponents.manifest',
|
||||
]
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/Metrics.jsm", this);
|
||||
Cu.import("resource://gre/modules/Preferences.jsm", this);
|
||||
Cu.import("resource://gre/modules/Promise.jsm", this);
|
||||
Cu.import("resource://services-sync/main.js", this);
|
||||
Cu.import("resource://services-sync/healthreport.jsm", this);
|
||||
Cu.import("resource://testing-common/services-common/logging.js", this);
|
||||
Cu.import("resource://testing-common/services/healthreport/utils.jsm", this);
|
||||
|
||||
function run_test() {
|
||||
initTestLogging();
|
||||
|
||||
// A head JS file always sets the
|
||||
// services.sync.fxaccounts.enabled pref. This prevents us from testing
|
||||
// pristine profile conditions and likely indicates there isn't test
|
||||
// coverage of the Sync service's fxAccountsEnabled property. Check
|
||||
// that pre-condition and hack around it.
|
||||
let branch = new Preferences("services.sync.");
|
||||
Assert.ok(branch.isSet("fxaccounts.enabled"), "Check precondition");
|
||||
branch.reset("fxaccounts.enabled");
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function test_constructor() {
|
||||
let provider = new SyncProvider();
|
||||
});
|
||||
|
||||
// Provider can initialize and de-initialize properly.
|
||||
add_task(function* test_init() {
|
||||
let storage = yield Metrics.Storage("init");
|
||||
let provider = new SyncProvider();
|
||||
yield provider.init(storage);
|
||||
yield provider.shutdown();
|
||||
yield storage.close();
|
||||
});
|
||||
|
||||
add_task(function* test_collect() {
|
||||
let storage = yield Metrics.Storage("collect");
|
||||
let provider = new SyncProvider();
|
||||
yield provider.init(storage);
|
||||
|
||||
// Initially nothing should be configured.
|
||||
let now = new Date();
|
||||
yield provider.collectDailyData();
|
||||
|
||||
let m = provider.getMeasurement("sync", 1);
|
||||
let values = yield m.getValues();
|
||||
Assert.equal(values.days.size, 1);
|
||||
Assert.ok(values.days.hasDay(now));
|
||||
let day = values.days.getDay(now);
|
||||
Assert.ok(day.has("enabled"));
|
||||
Assert.ok(day.has("activeProtocol"));
|
||||
Assert.ok(day.has("preferredProtocol"));
|
||||
Assert.equal(day.get("enabled"), 0);
|
||||
Assert.equal(day.get("preferredProtocol"), "1.5");
|
||||
Assert.equal(day.get("activeProtocol"), "1.5",
|
||||
"Protocol without setup should be FX Accounts version.");
|
||||
|
||||
// Now check for old Sync setup.
|
||||
let branch = new Preferences("services.sync.");
|
||||
branch.set("username", "foo");
|
||||
branch.reset("fxaccounts.enabled");
|
||||
yield provider.collectDailyData();
|
||||
values = yield m.getValues();
|
||||
Assert.equal(values.days.getDay(now).get("activeProtocol"), "1.1",
|
||||
"Protocol with old Sync setup is correct.");
|
||||
|
||||
Assert.equal(Weave.Status.__authManager, undefined, "Detect code changes");
|
||||
|
||||
// Let's enable Sync so we can get more useful data.
|
||||
// We need to do this because the FHR probe only records more info if Sync
|
||||
// is configured properly.
|
||||
Weave.Service.identity.account = "johndoe";
|
||||
Weave.Service.identity.basicPassword = "ilovejane";
|
||||
Weave.Service.identity.syncKey = Weave.Utils.generatePassphrase();
|
||||
Weave.Service.clusterURL = "http://localhost/";
|
||||
Assert.equal(Weave.Status.checkSetup(), Weave.STATUS_OK);
|
||||
|
||||
yield provider.collectDailyData();
|
||||
values = yield m.getValues();
|
||||
day = values.days.getDay(now);
|
||||
Assert.equal(day.get("enabled"), 1);
|
||||
|
||||
// An empty account should have 1 device: us.
|
||||
let dm = provider.getMeasurement("devices", 1);
|
||||
values = yield dm.getValues();
|
||||
Assert.ok(values.days.hasDay(now));
|
||||
day = values.days.getDay(now);
|
||||
Assert.equal(day.size, 1);
|
||||
let engine = Weave.Service.clientsEngine;
|
||||
Assert.ok(engine);
|
||||
Assert.ok(day.has(engine.localType));
|
||||
Assert.equal(day.get(engine.localType), 1);
|
||||
|
||||
// Add some devices and ensure they show up.
|
||||
engine._store._remoteClients["id1"] = {type: "mobile"};
|
||||
engine._store._remoteClients["id2"] = {type: "tablet"};
|
||||
engine._store._remoteClients["id3"] = {type: "mobile"};
|
||||
|
||||
yield provider.collectDailyData();
|
||||
values = yield dm.getValues();
|
||||
day = values.days.getDay(now);
|
||||
|
||||
let expected = {
|
||||
"foobar": 0,
|
||||
"tablet": 1,
|
||||
"mobile": 2,
|
||||
"desktop": 0,
|
||||
};
|
||||
|
||||
for (let type in expected) {
|
||||
let count = expected[type];
|
||||
|
||||
if (engine.localType == type) {
|
||||
count++;
|
||||
}
|
||||
|
||||
if (!count) {
|
||||
Assert.ok(!day.has(type));
|
||||
} else {
|
||||
Assert.ok(day.has(type));
|
||||
Assert.equal(day.get(type), count);
|
||||
}
|
||||
}
|
||||
|
||||
engine._store._remoteClients = {};
|
||||
|
||||
yield provider.shutdown();
|
||||
yield storage.close();
|
||||
});
|
||||
|
||||
add_task(function* test_sync_events() {
|
||||
let storage = yield Metrics.Storage("sync_events");
|
||||
let provider = new SyncProvider();
|
||||
yield provider.init(storage);
|
||||
|
||||
let m = provider.getMeasurement("sync", 1);
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
Services.obs.notifyObservers(null, "weave:service:sync:start", null);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
Services.obs.notifyObservers(null, "weave:service:sync:finish", null);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
Services.obs.notifyObservers(null, "weave:service:sync:error", null);
|
||||
}
|
||||
|
||||
// Wait for storage to complete.
|
||||
yield m.storage.enqueueOperation(() => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
let values = yield m.getValues();
|
||||
let now = new Date();
|
||||
Assert.ok(values.days.hasDay(now));
|
||||
let day = values.days.getDay(now);
|
||||
|
||||
Assert.ok(day.has("syncStart"));
|
||||
Assert.ok(day.has("syncSuccess"));
|
||||
Assert.ok(day.has("syncError"));
|
||||
Assert.equal(day.get("syncStart"), 5);
|
||||
Assert.equal(day.get("syncSuccess"), 3);
|
||||
Assert.equal(day.get("syncError"), 2);
|
||||
|
||||
yield provider.shutdown();
|
||||
yield storage.close();
|
||||
});
|
||||
|
||||
add_task(function* test_healthreporter_json() {
|
||||
let reporter = yield getHealthReporter("healthreporter_json");
|
||||
yield reporter.init();
|
||||
try {
|
||||
yield reporter._providerManager.registerProvider(new SyncProvider());
|
||||
yield reporter.collectMeasurements();
|
||||
let payload = yield reporter.getJSONPayload(true);
|
||||
let now = new Date();
|
||||
let today = reporter._formatDate(now);
|
||||
|
||||
Assert.ok(today in payload.data.days);
|
||||
let day = payload.data.days[today];
|
||||
|
||||
Assert.ok("org.mozilla.sync.sync" in day);
|
||||
Assert.ok("org.mozilla.sync.devices" in day);
|
||||
|
||||
let devices = day["org.mozilla.sync.devices"];
|
||||
let engine = Weave.Service.clientsEngine;
|
||||
Assert.ok(engine);
|
||||
let type = engine.localType;
|
||||
Assert.ok(type);
|
||||
Assert.ok(type in devices);
|
||||
Assert.equal(devices[type], 1);
|
||||
} finally {
|
||||
reporter._shutdown();
|
||||
}
|
||||
});
|
|
@ -13,8 +13,14 @@ function run_test() {
|
|||
_("When imported, Service.onStartup is called");
|
||||
initTestLogging("Trace");
|
||||
|
||||
let xps = Cc["@mozilla.org/weave/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject;
|
||||
do_check_false(xps.enabled);
|
||||
|
||||
// Test fixtures
|
||||
Service.identity.username = "johndoe";
|
||||
do_check_false(xps.enabled);
|
||||
|
||||
Cu.import("resource://services-sync/service.js");
|
||||
|
||||
|
@ -29,10 +35,6 @@ function run_test() {
|
|||
_("Observers are notified of startup");
|
||||
do_test_pending();
|
||||
|
||||
let xps = Cc["@mozilla.org/weave/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject;
|
||||
|
||||
do_check_false(Service.status.ready);
|
||||
do_check_false(xps.ready);
|
||||
Observers.add("weave:service:ready", function (subject, data) {
|
||||
|
@ -43,4 +45,10 @@ function run_test() {
|
|||
Svc.Prefs.resetBranch("");
|
||||
do_test_finished();
|
||||
});
|
||||
|
||||
do_check_false(xps.enabled);
|
||||
|
||||
Service.identity.account = "johndoe";
|
||||
Service.clusterURL = "http://localhost/";
|
||||
do_check_true(xps.enabled);
|
||||
}
|
||||
|
|
|
@ -164,3 +164,6 @@ skip-if = debug
|
|||
[test_tab_engine.js]
|
||||
[test_tab_store.js]
|
||||
[test_tab_tracker.js]
|
||||
|
||||
[test_healthreport.js]
|
||||
skip-if = ! healthreport
|
||||
|
|
|
@ -46,8 +46,9 @@ class InvalidTestPathError(Exception):
|
|||
class XPCShellRunner(MozbuildObject):
|
||||
"""Run xpcshell tests."""
|
||||
def run_suite(self, **kwargs):
|
||||
manifest = os.path.join(self.topobjdir, '_tests', 'xpcshell',
|
||||
'xpcshell.ini')
|
||||
from manifestparser import TestManifest
|
||||
manifest = TestManifest(manifests=[os.path.join(self.topobjdir,
|
||||
'_tests', 'xpcshell', 'xpcshell.ini')])
|
||||
|
||||
return self._run_xpcshell_harness(manifest=manifest, **kwargs)
|
||||
|
||||
|
|
|
@ -86,9 +86,13 @@ let CrashMonitorInternal = {
|
|||
* Path to checkpoint file.
|
||||
*
|
||||
* Each time a new notification is received, this file is written to
|
||||
* disc to reflect the information in |checkpoints|.
|
||||
* disc to reflect the information in |checkpoints|. Although Firefox for
|
||||
* Desktop and Metro share the same profile, they need to keep record of
|
||||
* crashes separately.
|
||||
*/
|
||||
path: OS.Path.join(OS.Constants.Path.profileDir, "sessionCheckpoints.json"),
|
||||
path: (Services.metro && Services.metro.immersive) ?
|
||||
OS.Path.join(OS.Constants.Path.profileDir, "metro", "sessionCheckpoints.json"):
|
||||
OS.Path.join(OS.Constants.Path.profileDir, "sessionCheckpoints.json"),
|
||||
|
||||
/**
|
||||
* Load checkpoints from previous session asynchronously.
|
||||
|
@ -181,6 +185,7 @@ this.CrashMonitor = {
|
|||
);
|
||||
|
||||
CrashMonitorInternal.initialized = true;
|
||||
OS.File.makeDir(OS.Path.join(OS.Constants.Path.profileDir, "metro"));
|
||||
return promise;
|
||||
},
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ Cu.import("resource://gre/modules/osfile/ospath.jsm", Path);
|
|||
|
||||
// The library of promises.
|
||||
Cu.import("resource://gre/modules/Promise.jsm", this);
|
||||
Cu.import("resource://gre/modules/Task.jsm", this);
|
||||
|
||||
// The implementation of communications
|
||||
Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
|
||||
|
@ -83,6 +84,9 @@ const EXCEPTION_CONSTRUCTORS = {
|
|||
},
|
||||
URIError: function(error) {
|
||||
return new URIError(error.message, error.fileName, error.lineNumber);
|
||||
},
|
||||
OSError: function(error) {
|
||||
return OS.File.Error.fromMsg(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -145,9 +149,25 @@ let Scheduler = {
|
|||
shutdown: false,
|
||||
|
||||
/**
|
||||
* The latest promise returned.
|
||||
* A promise resolved once all operations are complete.
|
||||
*
|
||||
* This promise is never rejected and the result is always undefined.
|
||||
*/
|
||||
latestPromise: Promise.resolve("OS.File scheduler hasn't been launched yet"),
|
||||
queue: Promise.resolve(),
|
||||
|
||||
/**
|
||||
* The latest message sent and still waiting for a reply. This
|
||||
* field is stored only in DEBUG builds, to avoid hoarding memory in
|
||||
* release builds.
|
||||
*/
|
||||
latestSent: undefined,
|
||||
|
||||
/**
|
||||
* The latest reply received, or null if we are waiting for a reply.
|
||||
* This field is stored only in DEBUG builds, to avoid hoarding
|
||||
* memory in release builds.
|
||||
*/
|
||||
latestReceived: undefined,
|
||||
|
||||
/**
|
||||
* A timer used to automatically shut down the worker after some time.
|
||||
|
@ -169,6 +189,32 @@ let Scheduler = {
|
|||
this.resetTimer = setTimeout(File.resetWorker, delay);
|
||||
},
|
||||
|
||||
/**
|
||||
* Push a task at the end of the queue.
|
||||
*
|
||||
* @param {function} code A function returning a Promise.
|
||||
* This function will be executed once all the previously
|
||||
* pushed tasks have completed.
|
||||
* @return {Promise} A promise with the same behavior as
|
||||
* the promise returned by |code|.
|
||||
*/
|
||||
push: function(code) {
|
||||
let promise = this.queue.then(code);
|
||||
// By definition, |this.queue| can never reject.
|
||||
this.queue = promise.then(null, () => undefined);
|
||||
// Fork |promise| to ensure that uncaught errors are reported
|
||||
return promise.then(null, null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Post a message to the worker thread.
|
||||
*
|
||||
* @param {string} method The name of the method to call.
|
||||
* @param {...} args The arguments to pass to the method. These arguments
|
||||
* must be clonable.
|
||||
* @return {Promise} A promise conveying the result/error caused by
|
||||
* calling |method| with arguments |args|.
|
||||
*/
|
||||
post: function post(method, ...args) {
|
||||
if (this.shutdown) {
|
||||
LOG("OS.File is not available anymore. The following request has been rejected.",
|
||||
|
@ -194,77 +240,71 @@ let Scheduler = {
|
|||
if (methodArgs) {
|
||||
options = methodArgs[methodArgs.length - 1];
|
||||
}
|
||||
let promise = worker.post(method,...args);
|
||||
return this.latestPromise = promise.then(
|
||||
function onSuccess(data) {
|
||||
if (firstLaunch) {
|
||||
Scheduler._updateTelemetry();
|
||||
}
|
||||
|
||||
// Don't restart the timer when reseting the worker, since that will
|
||||
// lead to an endless "resetWorker()" loop.
|
||||
if (method != "Meta_reset") {
|
||||
Scheduler.restartTimer();
|
||||
}
|
||||
|
||||
// Check for duration and return result.
|
||||
if (!options) {
|
||||
return data.ok;
|
||||
}
|
||||
// Check for options.outExecutionDuration.
|
||||
if (typeof options !== "object" ||
|
||||
!("outExecutionDuration" in options)) {
|
||||
return data.ok;
|
||||
}
|
||||
// If data.durationMs is not present, return data.ok (there was an
|
||||
// exception applying the method).
|
||||
if (!("durationMs" in data)) {
|
||||
return data.ok;
|
||||
}
|
||||
// Bug 874425 demonstrates that two successive calls to Date.now()
|
||||
// can actually produce an interval with negative duration.
|
||||
// We assume that this is due to an operation that is so short
|
||||
// that Date.now() is not monotonic, so we round this up to 0.
|
||||
let durationMs = Math.max(0, data.durationMs);
|
||||
// Accumulate (or initialize) outExecutionDuration
|
||||
if (typeof options.outExecutionDuration == "number") {
|
||||
options.outExecutionDuration += durationMs;
|
||||
} else {
|
||||
options.outExecutionDuration = durationMs;
|
||||
}
|
||||
return data.ok;
|
||||
},
|
||||
function onError(error) {
|
||||
if (firstLaunch) {
|
||||
Scheduler._updateTelemetry();
|
||||
}
|
||||
|
||||
// Don't restart the timer when reseting the worker, since that will
|
||||
// lead to an endless "resetWorker()" loop.
|
||||
if (method != "Meta_reset") {
|
||||
Scheduler.restartTimer();
|
||||
}
|
||||
// Check and throw EvalError | InternalError | RangeError
|
||||
// | ReferenceError | SyntaxError | TypeError | URIError
|
||||
if (error.data && error.data.exn in EXCEPTION_CONSTRUCTORS) {
|
||||
throw EXCEPTION_CONSTRUCTORS[error.data.exn](error.data);
|
||||
}
|
||||
// Decode any serialized error
|
||||
if (error instanceof PromiseWorker.WorkerError) {
|
||||
throw OS.File.Error.fromMsg(error.data);
|
||||
}
|
||||
// Extract something meaningful from ErrorEvent
|
||||
if (error instanceof ErrorEvent) {
|
||||
let message = error.message;
|
||||
if (message == "uncaught exception: [object StopIteration]") {
|
||||
throw StopIteration;
|
||||
}
|
||||
throw new Error(message, error.filename, error.lineno);
|
||||
}
|
||||
|
||||
throw error;
|
||||
return this.push(() => Task.spawn(function*() {
|
||||
if (OS.Constants.Sys.DEBUG) {
|
||||
// Update possibly memory-expensive debugging information
|
||||
Scheduler.latestReceived = null;
|
||||
Scheduler.latestSent = [method, ...args];
|
||||
}
|
||||
);
|
||||
let data;
|
||||
let reply;
|
||||
try {
|
||||
data = yield worker.post(method, ...args);
|
||||
reply = data;
|
||||
} catch (error if error instanceof PromiseWorker.WorkerError) {
|
||||
reply = error;
|
||||
throw EXCEPTION_CONSTRUCTORS[error.data.exn || "OSError"](error.data);
|
||||
} catch (error if error instanceof ErrorEvent) {
|
||||
reply = error;
|
||||
let message = error.message;
|
||||
if (message == "uncaught exception: [object StopIteration]") {
|
||||
throw StopIteration;
|
||||
}
|
||||
throw new Error(message, error.filename, error.lineno);
|
||||
} finally {
|
||||
if (OS.Constants.Sys.DEBUG) {
|
||||
// Update possibly memory-expensive debugging information
|
||||
Scheduler.latestSent = null;
|
||||
Scheduler.latestReceived = reply;
|
||||
}
|
||||
if (firstLaunch) {
|
||||
Scheduler._updateTelemetry();
|
||||
}
|
||||
|
||||
// Don't restart the timer when reseting the worker, since that will
|
||||
// lead to an endless "resetWorker()" loop.
|
||||
if (method != "Meta_reset") {
|
||||
Scheduler.restartTimer();
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duration and return result.
|
||||
if (!options) {
|
||||
return data.ok;
|
||||
}
|
||||
// Check for options.outExecutionDuration.
|
||||
if (typeof options !== "object" ||
|
||||
!("outExecutionDuration" in options)) {
|
||||
return data.ok;
|
||||
}
|
||||
// If data.durationMs is not present, return data.ok (there was an
|
||||
// exception applying the method).
|
||||
if (!("durationMs" in data)) {
|
||||
return data.ok;
|
||||
}
|
||||
// Bug 874425 demonstrates that two successive calls to Date.now()
|
||||
// can actually produce an interval with negative duration.
|
||||
// We assume that this is due to an operation that is so short
|
||||
// that Date.now() is not monotonic, so we round this up to 0.
|
||||
let durationMs = Math.max(0, data.durationMs);
|
||||
// Accumulate (or initialize) outExecutionDuration
|
||||
if (typeof options.outExecutionDuration == "number") {
|
||||
options.outExecutionDuration += durationMs;
|
||||
} else {
|
||||
options.outExecutionDuration = durationMs;
|
||||
}
|
||||
return data.ok;
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1258,8 +1298,18 @@ this.OS.Path = Path;
|
|||
// clients should register using AsyncShutdown.addBlocker.
|
||||
AsyncShutdown.profileBeforeChange.addBlocker(
|
||||
"OS.File: flush I/O queued before profile-before-change",
|
||||
() =>
|
||||
// Wait until the latest currently enqueued promise is satisfied/rejected
|
||||
Scheduler.latestPromise.then(null,
|
||||
function onError() { /* ignore error */})
|
||||
// Wait until the latest currently enqueued promise is satisfied/rejected
|
||||
(() => Scheduler.queue),
|
||||
function getDetails() {
|
||||
let result = {
|
||||
launched: Scheduler.launched,
|
||||
shutdown: Scheduler.shutdown,
|
||||
pendingReset: !!Scheduler.resetTimer,
|
||||
};
|
||||
if (OS.Constants.Sys.DEBUG) {
|
||||
result.latestSent = Scheduler.latestSent;
|
||||
result.latestReceived - Scheduler.latestReceived;
|
||||
};
|
||||
return result;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -23,13 +23,14 @@ Cu.import("resource://gre/modules/Promise.jsm", this);
|
|||
Cu.import("resource://gre/modules/TelemetryFile.jsm", this);
|
||||
Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
|
||||
Cu.import("resource://gre/modules/Task.jsm", this);
|
||||
Cu.import("resource://gre/modules/osfile.jsm", this);
|
||||
let {OS: {File, Path, Constants}} = Cu.import("resource://gre/modules/osfile.jsm", {});
|
||||
|
||||
// We increment TelemetryFile's MAX_PING_FILE_AGE and
|
||||
// OVERDUE_PING_FILE_AGE by 1ms so that our test pings exceed
|
||||
// those points in time.
|
||||
const EXPIRED_PING_FILE_AGE = TelemetryFile.MAX_PING_FILE_AGE + 1;
|
||||
const OVERDUE_PING_FILE_AGE = TelemetryFile.OVERDUE_PING_FILE_AGE + 1;
|
||||
// OVERDUE_PING_FILE_AGE by 1 minute so that our test pings exceed
|
||||
// those points in time, even taking into account file system imprecision.
|
||||
const ONE_MINUTE_MS = 60 * 1000;
|
||||
const EXPIRED_PING_FILE_AGE = TelemetryFile.MAX_PING_FILE_AGE + ONE_MINUTE_MS;
|
||||
const OVERDUE_PING_FILE_AGE = TelemetryFile.OVERDUE_PING_FILE_AGE + ONE_MINUTE_MS;
|
||||
|
||||
const PING_SAVE_FOLDER = "saved-telemetry-pings";
|
||||
const PING_TIMEOUT_LENGTH = 5000;
|
||||
|
@ -52,7 +53,8 @@ let gSeenPings = 0;
|
|||
* @param aAge the age in milliseconds to offset from now. A value
|
||||
* of 10 would make the ping 10ms older than now, for
|
||||
* example.
|
||||
* @returns an Array with the created pings.
|
||||
* @returns Promise
|
||||
* @resolve an Array with the created pings.
|
||||
*/
|
||||
function createSavedPings(aNum, aAge) {
|
||||
return Task.spawn(function*(){
|
||||
|
@ -71,8 +73,8 @@ function createSavedPings(aNum, aAge) {
|
|||
if (aAge) {
|
||||
// savePing writes to the file synchronously, so we're good to
|
||||
// modify the lastModifedTime now.
|
||||
let file = getSaveFileForPing(ping);
|
||||
file.lastModifiedTime = age;
|
||||
let file = getSavePathForPing(ping);
|
||||
yield File.setDates(file, null, age);
|
||||
}
|
||||
gCreatedPings++;
|
||||
pings.push(ping);
|
||||
|
@ -86,31 +88,29 @@ function createSavedPings(aNum, aAge) {
|
|||
* exist.
|
||||
*
|
||||
* @param aPings an Array of pings to delete.
|
||||
* @returns Promise
|
||||
*/
|
||||
function clearPings(aPings) {
|
||||
for (let ping of aPings) {
|
||||
let file = getSaveFileForPing(ping);
|
||||
if (file.exists()) {
|
||||
file.remove(false);
|
||||
return Task.spawn(function*() {
|
||||
for (let ping of aPings) {
|
||||
let path = getSavePathForPing(ping);
|
||||
yield File.remove(path);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a handle for the file that aPing should be
|
||||
* stored in locally.
|
||||
*
|
||||
* @returns nsILocalFile
|
||||
* @returns path
|
||||
*/
|
||||
function getSaveFileForPing(aPing) {
|
||||
let file = Services.dirsvc.get("ProfD", Ci.nsILocalFile).clone();
|
||||
file.append(PING_SAVE_FOLDER);
|
||||
file.append(aPing.slug);
|
||||
return file;
|
||||
function getSavePathForPing(aPing) {
|
||||
return Path.join(Constants.Path.profileDir, PING_SAVE_FOLDER, aPing.slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the number of TelemetryPings received by the
|
||||
* Check if the number of TelemetryPings received by the
|
||||
* HttpServer is not equal to aExpectedNum.
|
||||
*
|
||||
* @param aExpectedNum the number of pings we expect to receive.
|
||||
|
@ -123,18 +123,21 @@ function assertReceivedPings(aExpectedNum) {
|
|||
* Throws if any pings in aPings is saved locally.
|
||||
*
|
||||
* @param aPings an Array of pings to check.
|
||||
* @returns Promise
|
||||
*/
|
||||
function assertNotSaved(aPings) {
|
||||
let saved = 0;
|
||||
for (let ping of aPings) {
|
||||
let file = getSaveFileForPing(ping);
|
||||
if (file.exists()) {
|
||||
saved++;
|
||||
return Task.spawn(function*() {
|
||||
let saved = 0;
|
||||
for (let ping of aPings) {
|
||||
let file = getSavePathForPing(ping);
|
||||
if (yield File.exists()) {
|
||||
saved++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (saved > 0) {
|
||||
do_throw("Found " + saved + " unexpected saved pings.");
|
||||
}
|
||||
if (saved > 0) {
|
||||
do_throw("Found " + saved + " unexpected saved pings.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -200,7 +203,7 @@ add_task(function test_expired_pings_are_deleted() {
|
|||
let expiredPings = yield createSavedPings(EXPIRED_PINGS, EXPIRED_PING_FILE_AGE);
|
||||
yield startTelemetry();
|
||||
assertReceivedPings(0);
|
||||
assertNotSaved(expiredPings);
|
||||
yield assertNotSaved(expiredPings);
|
||||
yield resetTelemetry();
|
||||
});
|
||||
|
||||
|
@ -212,7 +215,7 @@ add_task(function test_recent_pings_not_sent() {
|
|||
yield startTelemetry();
|
||||
assertReceivedPings(0);
|
||||
yield resetTelemetry();
|
||||
clearPings(recentPings);
|
||||
yield clearPings(recentPings);
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -228,9 +231,9 @@ add_task(function test_overdue_pings_trigger_send() {
|
|||
yield startTelemetry();
|
||||
assertReceivedPings(TOTAL_EXPECTED_PINGS);
|
||||
|
||||
assertNotSaved(recentPings);
|
||||
assertNotSaved(expiredPings);
|
||||
assertNotSaved(overduePings);
|
||||
yield assertNotSaved(recentPings);
|
||||
yield assertNotSaved(expiredPings);
|
||||
yield assertNotSaved(overduePings);
|
||||
yield resetTelemetry();
|
||||
});
|
||||
|
||||
|
|
|
@ -22,9 +22,11 @@ function setup_crash() {
|
|||
}
|
||||
|
||||
function after_crash(mdump, extra) {
|
||||
do_print("after crash: " + extra.AsyncShutdownTimeout);
|
||||
let info = JSON.parse(extra.AsyncShutdownTimeout);
|
||||
do_check_true(info.phase == "testing-async-shutdown-crash");
|
||||
do_check_true(info.conditions.indexOf("A blocker that is never satisfied") != -1);
|
||||
do_check_eq(info.phase, "testing-async-shutdown-crash");
|
||||
do_print("Condition: " + JSON.stringify(info.conditions));
|
||||
do_check_true(JSON.stringify(info.conditions).indexOf("A blocker that is never satisfied") != -1);
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
|
|
|
@ -357,6 +357,10 @@ var PageStyleActor = protocol.ActorClass({
|
|||
*/
|
||||
addElementRules: function(element, inherited, options, rules)
|
||||
{
|
||||
if (!element.style) {
|
||||
return;
|
||||
}
|
||||
|
||||
let elementStyle = this._styleRef(element);
|
||||
|
||||
if (!inherited || this._hasInheritedProps(element.style)) {
|
||||
|
|
|
@ -617,7 +617,7 @@ CssLogic.prototype = {
|
|||
|
||||
|
||||
// Add element.style information.
|
||||
if (element.style.length > 0) {
|
||||
if (element.style && element.style.length > 0) {
|
||||
let rule = new CssRule(null, { style: element.style }, element);
|
||||
rule._matchId = this._matchId;
|
||||
rule._passId = this._passId;
|
||||
|
|
|
@ -108,6 +108,34 @@ function err(msg, error = null) {
|
|||
return log(msg, "ERROR: ", error);
|
||||
}
|
||||
|
||||
// Utility function designed to get the current state of execution
|
||||
// of a blocker.
|
||||
// We are a little paranoid here to ensure that in case of evaluation
|
||||
// error we do not block the AsyncShutdown.
|
||||
function safeGetState(state) {
|
||||
if (!state) {
|
||||
return "(none)";
|
||||
}
|
||||
try {
|
||||
// Evaluate state(), normalize the result into something that we can
|
||||
// safely stringify or upload.
|
||||
let string = JSON.stringify(state());
|
||||
let data = JSON.parse(string);
|
||||
// Simplify the rest of the code by ensuring that we can simply
|
||||
// concatenate the result to a message.
|
||||
data.toString = function() {
|
||||
return string;
|
||||
};
|
||||
return data;
|
||||
} catch (ex) {
|
||||
try {
|
||||
return "Error getting state: " + ex;
|
||||
} catch (ex2) {
|
||||
return "Could not display error";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Countdown for a given duration, skipping beats if the computer is too busy,
|
||||
* sleeping or otherwise unavailable.
|
||||
|
@ -186,6 +214,10 @@ function getPhase(topic) {
|
|||
* resulting promise is either resolved or rejected. If
|
||||
* |condition| is not a function but another value |v|, it behaves
|
||||
* as if it were a function returning |v|.
|
||||
* @param {function*} state Optionally, a function returning
|
||||
* information about the current state of the blocker as an
|
||||
* object. Used for providing more details when logging errors or
|
||||
* crashing.
|
||||
*
|
||||
* Examples:
|
||||
* AsyncShutdown.profileBeforeChange.addBlocker("Module: just a promise",
|
||||
|
@ -209,11 +241,14 @@ function getPhase(topic) {
|
|||
* });
|
||||
*
|
||||
*/
|
||||
addBlocker: function(name, condition) {
|
||||
addBlocker: function(name, condition, state = null) {
|
||||
if (typeof name != "string") {
|
||||
throw new TypeError("Expected a human-readable name as first argument");
|
||||
}
|
||||
spinner.addBlocker({name: name, condition: condition});
|
||||
if (state && typeof state != "function") {
|
||||
throw new TypeError("Expected nothing or a function as third argument");
|
||||
}
|
||||
spinner.addBlocker({name: name, condition: condition, state: state});
|
||||
}
|
||||
});
|
||||
gPhases.set(topic, phase);
|
||||
|
@ -274,7 +309,7 @@ Spinner.prototype = {
|
|||
// are not satisfied yet.
|
||||
let allMonitors = [];
|
||||
|
||||
for (let {condition, name} of conditions) {
|
||||
for (let {condition, name, state} of conditions) {
|
||||
// Gather all completion conditions
|
||||
|
||||
try {
|
||||
|
@ -303,13 +338,15 @@ Spinner.prototype = {
|
|||
let msg = "A phase completion condition is" +
|
||||
" taking too long to complete." +
|
||||
" Condition: " + monitor.name +
|
||||
" Phase: " + topic;
|
||||
" Phase: " + topic +
|
||||
" State: " + safeGetState(state);
|
||||
warn(msg);
|
||||
}, DELAY_WARNING_MS, Ci.nsITimer.TYPE_ONE_SHOT);
|
||||
|
||||
let monitor = {
|
||||
isFrozen: true,
|
||||
name: name
|
||||
name: name,
|
||||
state: state
|
||||
};
|
||||
condition = condition.then(function onSuccess() {
|
||||
timer.cancel(); // As a side-effect, this prevents |timer| from
|
||||
|
@ -320,7 +357,8 @@ Spinner.prototype = {
|
|||
let msg = "A completion condition encountered an error" +
|
||||
" while we were spinning the event loop." +
|
||||
" Condition: " + name +
|
||||
" Phase: " + topic;
|
||||
" Phase: " + topic +
|
||||
" State: " + safeGetState(state);
|
||||
warn(msg, error);
|
||||
monitor.isFrozen = false;
|
||||
});
|
||||
|
@ -331,7 +369,8 @@ Spinner.prototype = {
|
|||
let msg = "A completion condition encountered an error" +
|
||||
" while we were initializing the phase." +
|
||||
" Condition: " + name +
|
||||
" Phase: " + topic;
|
||||
" Phase: " + topic +
|
||||
" State: " + safeGetState(state);
|
||||
warn(msg, error);
|
||||
}
|
||||
|
||||
|
@ -362,9 +401,10 @@ Spinner.prototype = {
|
|||
function onTimeout() {
|
||||
// Report the problem as best as we can, then crash.
|
||||
let frozen = [];
|
||||
for (let {name, isFrozen} of allMonitors) {
|
||||
let states = [];
|
||||
for (let {name, isFrozen, state} of allMonitors) {
|
||||
if (isFrozen) {
|
||||
frozen.push(name);
|
||||
frozen.push({name: name, state: safeGetState(state)});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -372,7 +412,7 @@ Spinner.prototype = {
|
|||
" within a reasonable amount of time. Causing a crash to" +
|
||||
" ensure that we do not leave the user with an unresponsive" +
|
||||
" process draining resources." +
|
||||
" Conditions: " + frozen.join(", ") +
|
||||
" Conditions: " + JSON.stringify(frozen) +
|
||||
" Phase: " + topic;
|
||||
err(msg);
|
||||
if (gCrashReporter && gCrashReporter.enabled) {
|
||||
|
|
|
@ -54,23 +54,37 @@ add_task(function test_simple_async() {
|
|||
for (let arg of [undefined, null, "foo", 100, new Error("BOOM")]) {
|
||||
for (let resolution of [arg, Promise.reject(arg)]) {
|
||||
for (let success of [false, true]) {
|
||||
// Asynchronous phase
|
||||
do_print("Asynchronous test with " + arg + ", " + resolution);
|
||||
let topic = getUniqueTopic();
|
||||
let outParam = { isFinished: false };
|
||||
AsyncShutdown._getPhase(topic).addBlocker(
|
||||
"Async test",
|
||||
function() {
|
||||
if (success) {
|
||||
return longRunningAsyncTask(resolution, outParam);
|
||||
} else {
|
||||
throw resolution;
|
||||
}
|
||||
}
|
||||
);
|
||||
do_check_false(outParam.isFinished);
|
||||
Services.obs.notifyObservers(null, topic, null);
|
||||
do_check_eq(outParam.isFinished, success);
|
||||
for (let state of [[null],
|
||||
[],
|
||||
[() => "some state"],
|
||||
[function() {
|
||||
throw new Error("State BOOM"); }],
|
||||
[function() {
|
||||
return {
|
||||
toJSON: function() {
|
||||
throw new Error("State.toJSON BOOM");
|
||||
}
|
||||
};
|
||||
}]]) {
|
||||
// Asynchronous phase
|
||||
do_print("Asynchronous test with " + arg + ", " + resolution);
|
||||
let topic = getUniqueTopic();
|
||||
let outParam = { isFinished: false };
|
||||
AsyncShutdown._getPhase(topic).addBlocker(
|
||||
"Async test",
|
||||
function() {
|
||||
if (success) {
|
||||
return longRunningAsyncTask(resolution, outParam);
|
||||
} else {
|
||||
throw resolution;
|
||||
}
|
||||
},
|
||||
...state
|
||||
);
|
||||
do_check_false(outParam.isFinished);
|
||||
Services.obs.notifyObservers(null, topic, null);
|
||||
do_check_eq(outParam.isFinished, success);
|
||||
}
|
||||
}
|
||||
|
||||
// Synchronous phase - just test that we don't throw/freeze
|
||||
|
@ -128,6 +142,9 @@ add_task(function test_various_failures() {
|
|||
|
||||
exn = get_exn(() => phase.addBlocker(null, true));
|
||||
do_check_eq(exn.name, "TypeError");
|
||||
|
||||
exn = get_exn(() => phase.addBlocker("Test 2", () => true, "not a function"));
|
||||
do_check_eq(exn.name, "TypeError");
|
||||
});
|
||||
|
||||
add_task(function() {
|
||||
|
|
|
@ -96,6 +96,9 @@ const INTEGER_KEY_MAP = {
|
|||
daily_users: "dailyUsers"
|
||||
};
|
||||
|
||||
// Wrap the XHR factory so that tests can override with a mock
|
||||
let XHRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1",
|
||||
"nsIXMLHttpRequest");
|
||||
|
||||
function convertHTMLToPlainText(html) {
|
||||
if (!html)
|
||||
|
@ -602,12 +605,15 @@ this.AddonRepository = {
|
|||
* The array of add-on ids to repopulate the cache with
|
||||
* @param aCallback
|
||||
* The optional callback to call once complete
|
||||
* @param aTimeout
|
||||
* (Optional) timeout in milliseconds to abandon the XHR request
|
||||
* if we have not received a response from the server.
|
||||
*/
|
||||
repopulateCache: function AddonRepo_repopulateCache(aIds, aCallback) {
|
||||
this._repopulateCacheInternal(aIds, aCallback, false);
|
||||
repopulateCache: function(aIds, aCallback, aTimeout) {
|
||||
this._repopulateCacheInternal(aIds, aCallback, false, aTimeout);
|
||||
},
|
||||
|
||||
_repopulateCacheInternal: function AddonRepo_repopulateCacheInternal(aIds, aCallback, aSendPerformance) {
|
||||
_repopulateCacheInternal: function(aIds, aCallback, aSendPerformance, aTimeout) {
|
||||
// Completely remove cache if caching is not enabled
|
||||
if (!this.cacheEnabled) {
|
||||
this._addons = null;
|
||||
|
@ -637,7 +643,7 @@ this.AddonRepository = {
|
|||
if (aCallback)
|
||||
aCallback();
|
||||
}
|
||||
}, aSendPerformance);
|
||||
}, aSendPerformance, aTimeout);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -758,8 +764,11 @@ this.AddonRepository = {
|
|||
* @param aSendPerformance
|
||||
* Boolean indicating whether to send performance data with the
|
||||
* request.
|
||||
* @param aTimeout
|
||||
* (Optional) timeout in milliseconds to abandon the XHR request
|
||||
* if we have not received a response from the server.
|
||||
*/
|
||||
_beginGetAddons: function AddonRepo_beginGetAddons(aIDs, aCallback, aSendPerformance) {
|
||||
_beginGetAddons: function(aIDs, aCallback, aSendPerformance, aTimeout) {
|
||||
let ids = aIDs.slice(0);
|
||||
|
||||
let params = {
|
||||
|
@ -841,7 +850,7 @@ this.AddonRepository = {
|
|||
self._reportSuccess(results, -1);
|
||||
}
|
||||
|
||||
this._beginSearch(url, ids.length, aCallback, handleResults);
|
||||
this._beginSearch(url, ids.length, aCallback, handleResults, aTimeout);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1388,7 +1397,7 @@ this.AddonRepository = {
|
|||
},
|
||||
|
||||
// Begins a new search if one isn't currently executing
|
||||
_beginSearch: function AddonRepo_beginSearch(aURI, aMaxResults, aCallback, aHandleResults) {
|
||||
_beginSearch: function(aURI, aMaxResults, aCallback, aHandleResults, aTimeout) {
|
||||
if (this._searching || aURI == null || aMaxResults <= 0) {
|
||||
aCallback.searchFailed();
|
||||
return;
|
||||
|
@ -1400,23 +1409,23 @@ this.AddonRepository = {
|
|||
|
||||
LOG("Requesting " + aURI);
|
||||
|
||||
this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
|
||||
createInstance(Ci.nsIXMLHttpRequest);
|
||||
this._request = new XHRequest();
|
||||
this._request.mozBackgroundRequest = true;
|
||||
this._request.open("GET", aURI, true);
|
||||
this._request.overrideMimeType("text/xml");
|
||||
if (aTimeout) {
|
||||
this._request.timeout = aTimeout;
|
||||
}
|
||||
|
||||
let self = this;
|
||||
this._request.addEventListener("error", function beginSearch_errorListener(aEvent) {
|
||||
self._reportFailure();
|
||||
}, false);
|
||||
this._request.addEventListener("load", function beginSearch_loadListener(aEvent) {
|
||||
this._request.addEventListener("error", aEvent => this._reportFailure(), false);
|
||||
this._request.addEventListener("timeout", aEvent => this._reportFailure(), false);
|
||||
this._request.addEventListener("load", aEvent => {
|
||||
let request = aEvent.target;
|
||||
let responseXML = request.responseXML;
|
||||
|
||||
if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR ||
|
||||
(request.status != 200 && request.status != 0)) {
|
||||
self._reportFailure();
|
||||
this._reportFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1429,7 +1438,7 @@ this.AddonRepository = {
|
|||
totalResults = parsedTotalResults;
|
||||
|
||||
let compatElements = documentElement.getElementsByTagName("addon_compatibility");
|
||||
let compatData = self._parseAddonCompatData(compatElements);
|
||||
let compatData = this._parseAddonCompatData(compatElements);
|
||||
|
||||
aHandleResults(elements, totalResults, compatData);
|
||||
}, false);
|
||||
|
|
|
@ -394,10 +394,6 @@ function UpdateParser(aId, aUpdateKey, aUrl, aObserver) {
|
|||
this.observer = aObserver;
|
||||
this.url = aUrl;
|
||||
|
||||
this.timer = Cc["@mozilla.org/timer;1"].
|
||||
createInstance(Ci.nsITimer);
|
||||
this.timer.initWithCallback(this, TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
|
||||
|
||||
let requireBuiltIn = true;
|
||||
try {
|
||||
requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS);
|
||||
|
@ -415,9 +411,11 @@ function UpdateParser(aId, aUpdateKey, aUrl, aObserver) {
|
|||
// Prevent the request from writing to cache.
|
||||
this.request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
|
||||
this.request.overrideMimeType("text/xml");
|
||||
this.request.timeout = TIMEOUT;
|
||||
var self = this;
|
||||
this.request.addEventListener("load", function loadEventListener(event) { self.onLoad() }, false);
|
||||
this.request.addEventListener("error", function errorEventListener(event) { self.onError() }, false);
|
||||
this.request.addEventListener("timeout", function timeoutEventListener(event) { self.onTimeout() }, false);
|
||||
this.request.send(null);
|
||||
}
|
||||
catch (e) {
|
||||
|
@ -430,15 +428,12 @@ UpdateParser.prototype = {
|
|||
updateKey: null,
|
||||
observer: null,
|
||||
request: null,
|
||||
timer: null,
|
||||
url: null,
|
||||
|
||||
/**
|
||||
* Called when the manifest has been successfully loaded.
|
||||
*/
|
||||
onLoad: function UP_onLoad() {
|
||||
this.timer.cancel();
|
||||
this.timer = null;
|
||||
let request = this.request;
|
||||
this.request = null;
|
||||
|
||||
|
@ -505,13 +500,19 @@ UpdateParser.prototype = {
|
|||
this.notifyError(AddonUpdateChecker.ERROR_UNKNOWN_FORMAT);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the request times out
|
||||
*/
|
||||
onTimeout: function() {
|
||||
this.request = null;
|
||||
WARN("Request for " + this.url + " timed out");
|
||||
this.notifyError(AddonUpdateChecker.ERROR_TIMEOUT);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the manifest failed to load.
|
||||
*/
|
||||
onError: function UP_onError() {
|
||||
this.timer.cancel();
|
||||
this.timer = null;
|
||||
|
||||
if (!Components.isSuccessCode(this.request.status)) {
|
||||
WARN("Request failed: " + this.url + " - " + this.request.status);
|
||||
}
|
||||
|
@ -550,25 +551,10 @@ UpdateParser.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the request has timed out and should be canceled.
|
||||
*/
|
||||
notify: function UP_notify(aTimer) {
|
||||
this.timer = null;
|
||||
this.request.abort();
|
||||
this.request = null;
|
||||
|
||||
WARN("Request timed out");
|
||||
|
||||
this.notifyError(AddonUpdateChecker.ERROR_TIMEOUT);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called to cancel an in-progress update check.
|
||||
*/
|
||||
cancel: function UP_cancel() {
|
||||
this.timer.cancel();
|
||||
this.timer = null;
|
||||
this.request.abort();
|
||||
this.request = null;
|
||||
this.notifyError(AddonUpdateChecker.ERROR_CANCELLED);
|
||||
|
|
|
@ -12,6 +12,9 @@ const PREF_UPDATE_EXTENSIONS_ENABLED = "extensions.update.enabled";
|
|||
const PREF_XPINSTALL_ENABLED = "xpinstall.enabled";
|
||||
const PREF_EM_HOTFIX_ID = "extensions.hotfix.id";
|
||||
|
||||
// timeout (in milliseconds) to wait for response to the metadata ping
|
||||
const METADATA_TIMEOUT = 30000;
|
||||
|
||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
Components.utils.import("resource://gre/modules/AddonManager.jsm");
|
||||
Components.utils.import("resource://gre/modules/AddonRepository.jsm");
|
||||
|
@ -175,7 +178,7 @@ var gVersionInfoPage = {
|
|||
for (let addon of gUpdateWizard.addons)
|
||||
addon.findUpdates(gVersionInfoPage, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
|
||||
});
|
||||
});
|
||||
}, METADATA_TIMEOUT);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ MOCHITEST_BROWSER_MAIN_FILES = \
|
|||
browser_discovery.js \
|
||||
browser_dragdrop.js \
|
||||
browser_list.js \
|
||||
browser_metadataTimeout.js \
|
||||
browser_searching.js \
|
||||
browser_sorting.js \
|
||||
browser_uninstalling.js \
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
// Test how update window behaves when metadata ping times out
|
||||
// bug 965788
|
||||
|
||||
const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul";
|
||||
|
||||
const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url";
|
||||
const PREF_MIN_PLATFORM_COMPAT = "extensions.minCompatiblePlatformVersion";
|
||||
|
||||
Components.utils.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
let repo = {};
|
||||
let ARContext = Components.utils.import("resource://gre/modules/AddonRepository.jsm", repo);
|
||||
info("ARContext: " + Object.keys(ARContext).join(", "));
|
||||
|
||||
// Mock out the XMLHttpRequest factory for AddonRepository so
|
||||
// we can reply with a timeout
|
||||
let pXHRStarted = Promise.defer();
|
||||
let oldXHRConstructor = ARContext.XHRequest;
|
||||
ARContext.XHRequest = function() {
|
||||
this._handlers = new Map();
|
||||
this.mozBackgroundRequest = false;
|
||||
this.timeout = undefined;
|
||||
this.open = function(aMethod, aURI, aAsync) {
|
||||
this.method = aMethod;
|
||||
this.uri = aURI;
|
||||
this.async = aAsync;
|
||||
info("Opened XHR for " + aMethod + " " + aURI);
|
||||
};
|
||||
this.overrideMimeType = function(aMimeType) {
|
||||
this.mimeType = aMimeType;
|
||||
};
|
||||
this.addEventListener = function(aEvent, aHandler, aCapture) {
|
||||
this._handlers.set(aEvent, aHandler);
|
||||
};
|
||||
this.send = function(aBody) {
|
||||
info("Send XHR for " + this.method + " " + this.uri + " handlers: " + [this._handlers.keys()].join(", "));
|
||||
pXHRStarted.resolve(this);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Returns promise{window}, resolves with a handle to the compatibility
|
||||
// check window
|
||||
function promise_open_compatibility_window(aInactiveAddonIds) {
|
||||
let deferred = Promise.defer();
|
||||
// This will reset the longer timeout multiplier to 2 which will give each
|
||||
// test that calls open_compatibility_window a minimum of 60 seconds to
|
||||
// complete.
|
||||
requestLongerTimeout(100 /* XXX was 2 */);
|
||||
|
||||
var variant = Cc["@mozilla.org/variant;1"].
|
||||
createInstance(Ci.nsIWritableVariant);
|
||||
variant.setFromVariant(aInactiveAddonIds);
|
||||
|
||||
// Cannot be modal as we want to interract with it, shouldn't cause problems
|
||||
// with testing though.
|
||||
var features = "chrome,centerscreen,dialog,titlebar";
|
||||
var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
|
||||
getService(Ci.nsIWindowWatcher);
|
||||
var win = ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant);
|
||||
|
||||
win.addEventListener("load", function() {
|
||||
function page_shown(aEvent) {
|
||||
if (aEvent.target.pageid)
|
||||
info("Page " + aEvent.target.pageid + " shown");
|
||||
}
|
||||
|
||||
win.removeEventListener("load", arguments.callee, false);
|
||||
|
||||
info("Compatibility dialog opened");
|
||||
|
||||
win.addEventListener("pageshow", page_shown, false);
|
||||
win.addEventListener("unload", function() {
|
||||
win.removeEventListener("unload", arguments.callee, false);
|
||||
win.removeEventListener("pageshow", page_shown, false);
|
||||
dump("Compatibility dialog closed\n");
|
||||
}, false);
|
||||
|
||||
deferred.resolve(win);
|
||||
}, false);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function promise_window_close(aWindow) {
|
||||
let deferred = Promise.defer();
|
||||
aWindow.addEventListener("unload", function() {
|
||||
aWindow.removeEventListener("unload", arguments.callee, false);
|
||||
deferred.resolve(aWindow);
|
||||
}, false);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Start the compatibility update dialog, but use the mock XHR to respond with
|
||||
// a timeout
|
||||
add_task(function* amo_ping_timeout() {
|
||||
Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
|
||||
let compatWindow = yield promise_open_compatibility_window([]);
|
||||
|
||||
let xhr = yield pXHRStarted.promise;
|
||||
is(xhr.timeout, 30000, "XHR request should have 30 second timeout");
|
||||
ok(xhr._handlers.has("timeout"), "Timeout handler set on XHR");
|
||||
// call back the timeout handler
|
||||
xhr._handlers.get("timeout")();
|
||||
|
||||
// Put the old XHR constructor back
|
||||
ARContext.XHRequest = oldXHRConstructor;
|
||||
// The window should close without further interaction
|
||||
yield promise_window_close(compatWindow);
|
||||
});
|
|
@ -23,6 +23,11 @@ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|||
Components.utils.import("resource://gre/modules/FileUtils.jsm");
|
||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
Components.utils.import("resource://gre/modules/NetUtil.jsm");
|
||||
Components.utils.import("resource://gre/modules/Promise.jsm");
|
||||
Components.utils.import("resource://gre/modules/Task.jsm");
|
||||
Components.utils.import("resource://gre/modules/osfile.jsm");
|
||||
|
||||
Services.prefs.setBoolPref("toolkit.osfile.log", true);
|
||||
|
||||
// We need some internal bits of AddonManager
|
||||
let AMscope = Components.utils.import("resource://gre/modules/AddonManager.jsm");
|
||||
|
@ -444,6 +449,7 @@ function shutdownManager() {
|
|||
// This would be cleaner if I could get it as the rejection reason from
|
||||
// the AddonManagerInternal.shutdown() promise
|
||||
gXPISaveError = XPIscope.XPIProvider._shutdownError;
|
||||
do_print("gXPISaveError set to: " + gXPISaveError);
|
||||
AddonManagerPrivate.unregisterProvider(XPIscope.XPIProvider);
|
||||
Components.utils.unload("resource://gre/modules/XPIProvider.jsm");
|
||||
}
|
||||
|
@ -691,6 +697,8 @@ function writeInstallRDFForExtension(aData, aDir, aId, aExtraFile) {
|
|||
*
|
||||
* @param aExt a file pointing to either the packed extension or its unpacked directory.
|
||||
* @param aTime the time to which we set the lastModifiedTime of the extension
|
||||
*
|
||||
* @deprecated Please use promiseSetExtensionModifiedTime instead
|
||||
*/
|
||||
function setExtensionModifiedTime(aExt, aTime) {
|
||||
aExt.lastModifiedTime = aTime;
|
||||
|
@ -702,6 +710,25 @@ function setExtensionModifiedTime(aExt, aTime) {
|
|||
entries.close();
|
||||
}
|
||||
}
|
||||
function promiseSetExtensionModifiedTime(aPath, aTime) {
|
||||
return Task.spawn(function* () {
|
||||
yield OS.File.setDates(aPath, aTime, aTime);
|
||||
let entries, iterator;
|
||||
try {
|
||||
let iterator = new OS.File.DirectoryIterator(aPath);
|
||||
entries = yield iterator.nextBatch();
|
||||
} catch (ex if ex instanceof OS.File.Error) {
|
||||
return;
|
||||
} finally {
|
||||
if (iterator) {
|
||||
iterator.close();
|
||||
}
|
||||
}
|
||||
for (let entry of entries) {
|
||||
yield promiseSetExtensionModifiedTime(entry.path, aTime);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually installs an XPI file into an install location by either copying the
|
||||
|
@ -1082,7 +1109,11 @@ function completeAllInstalls(aInstalls, aCallback) {
|
|||
function installAllFiles(aFiles, aCallback, aIgnoreIncompatible) {
|
||||
let count = aFiles.length;
|
||||
let installs = [];
|
||||
|
||||
function callback() {
|
||||
if (aCallback) {
|
||||
aCallback();
|
||||
}
|
||||
}
|
||||
aFiles.forEach(function(aFile) {
|
||||
AddonManager.getInstallForFile(aFile, function(aInstall) {
|
||||
if (!aInstall)
|
||||
|
@ -1093,11 +1124,18 @@ function installAllFiles(aFiles, aCallback, aIgnoreIncompatible) {
|
|||
installs.push(aInstall);
|
||||
|
||||
if (--count == 0)
|
||||
completeAllInstalls(installs, aCallback);
|
||||
completeAllInstalls(installs, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function promiseInstallAllFiles(aFiles, aIgnoreIncompatible) {
|
||||
let deferred = Promise.defer();
|
||||
installAllFiles(aFiles, deferred.resolve, aIgnoreIncompatible);
|
||||
return deferred.promise;
|
||||
|
||||
}
|
||||
|
||||
if ("nsIWindowsRegKey" in AM_Ci) {
|
||||
var MockRegistry = {
|
||||
LOCAL_MACHINE: {},
|
||||
|
@ -1473,3 +1511,17 @@ function callback_soon(aFunction) {
|
|||
}, aFunction.name ? "delayed callback " + aFunction.name : "delayed callback");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A promise-based variant of AddonManager.getAddonsByIDs.
|
||||
*
|
||||
* @param {array} list As the first argument of AddonManager.getAddonsByIDs
|
||||
* @return {promise}
|
||||
* @resolve {array} The list of add-ons sent by AddonManaget.getAddonsByIDs to
|
||||
* its callback.
|
||||
*/
|
||||
function promiseAddonsByIDs(list) {
|
||||
let deferred = Promise.defer();
|
||||
AddonManager.getAddonsByIDs(list, deferred.resolve);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
|
|
@ -415,7 +415,20 @@ function run_test() {
|
|||
}
|
||||
|
||||
function end_test() {
|
||||
gServer.stop(do_test_finished);
|
||||
let testDir = gProfD.clone();
|
||||
testDir.append("extensions");
|
||||
testDir.append("staged");
|
||||
gServer.stop(function() {
|
||||
function loop() {
|
||||
if (!testDir.exists()) {
|
||||
do_print("Staged directory has been cleaned up");
|
||||
do_test_finished();
|
||||
}
|
||||
do_print("Waiting 1 second until cleanup is complete");
|
||||
do_timeout(1000, loop);
|
||||
}
|
||||
loop();
|
||||
});
|
||||
}
|
||||
|
||||
// Tests homepageURL, getRecommendedURL() and getSearchURL()
|
||||
|
|
|
@ -134,8 +134,7 @@ var theme2 = {
|
|||
const profileDir = gProfD.clone();
|
||||
profileDir.append("extensions");
|
||||
|
||||
function run_test() {
|
||||
do_test_pending();
|
||||
add_task(function* init() {
|
||||
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
|
||||
|
||||
writeInstallRDFForExtension(addon1, profileDir);
|
||||
|
@ -154,37 +153,221 @@ function run_test() {
|
|||
// New profile so new add-ons are ignored
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
|
||||
AddonManager.getAddonsByIDs(["addon2@tests.mozilla.org",
|
||||
let [a2, a3, a4, a7, t2] =
|
||||
yield promiseAddonsByIDs(["addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"], function([a2, a3, a4,
|
||||
a7, t2]) {
|
||||
// Set up the initial state
|
||||
a2.userDisabled = true;
|
||||
a4.userDisabled = true;
|
||||
a7.userDisabled = true;
|
||||
t2.userDisabled = false;
|
||||
a3.findUpdates({
|
||||
onUpdateFinished: function() {
|
||||
a4.findUpdates({
|
||||
onUpdateFinished: function() {
|
||||
// Let the updates finish before restarting the manager
|
||||
do_execute_soon(run_test_1);
|
||||
}
|
||||
}, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
|
||||
}
|
||||
}, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
|
||||
});
|
||||
}
|
||||
"theme2@tests.mozilla.org"]);
|
||||
let deferredUpdateFinished = Promise.defer();
|
||||
// Set up the initial state
|
||||
a2.userDisabled = true;
|
||||
a4.userDisabled = true;
|
||||
a7.userDisabled = true;
|
||||
t2.userDisabled = false;
|
||||
a3.findUpdates({
|
||||
onUpdateFinished: function() {
|
||||
a4.findUpdates({
|
||||
onUpdateFinished: function() {
|
||||
// Let the updates finish before restarting the manager
|
||||
deferredUpdateFinished.resolve();
|
||||
}
|
||||
}, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
|
||||
}
|
||||
}, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
|
||||
|
||||
function end_test() {
|
||||
testserver.stop(do_test_finished);
|
||||
}
|
||||
yield deferredUpdateFinished.promise;
|
||||
});
|
||||
|
||||
function run_test_1() {
|
||||
|
||||
add_task(function* run_test_1() {
|
||||
restartManager();
|
||||
AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
let [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
|
||||
yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org",
|
||||
"addon6@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme1@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"]);
|
||||
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_true(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
|
||||
do_check_neq(a3, null);
|
||||
do_check_true(a3.isActive);
|
||||
do_check_false(a3.userDisabled);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a3.id));
|
||||
|
||||
do_check_neq(a4, null);
|
||||
do_check_false(a4.isActive);
|
||||
do_check_true(a4.userDisabled);
|
||||
do_check_false(a4.appDisabled);
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a4.id));
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_true(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_false(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a5.id));
|
||||
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(a7, null);
|
||||
do_check_false(a7.isActive);
|
||||
do_check_true(a7.userDisabled);
|
||||
do_check_false(a7.appDisabled);
|
||||
do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(t1, null);
|
||||
do_check_false(t1.isActive);
|
||||
do_check_true(t1.userDisabled);
|
||||
do_check_false(t1.appDisabled);
|
||||
do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isThemeInAddonsList(profileDir, t1.id));
|
||||
|
||||
do_check_neq(t2, null);
|
||||
do_check_true(t2.isActive);
|
||||
do_check_false(t2.userDisabled);
|
||||
do_check_false(t2.appDisabled);
|
||||
do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isThemeInAddonsList(profileDir, t2.id));
|
||||
|
||||
// Open another handle on the JSON DB with as much Unix and Windows locking
|
||||
// as we can to simulate some other process interfering with it
|
||||
shutdownManager();
|
||||
do_print("Locking " + gExtensionsJSON.path);
|
||||
let options = {
|
||||
winShare: 0
|
||||
};
|
||||
if (OS.Constants.libc.O_EXLOCK)
|
||||
options.unixFlags = OS.Constants.libc.O_EXLOCK;
|
||||
|
||||
let file = yield OS.File.open(gExtensionsJSON.path, {read:true, write:true, existing:true}, options);
|
||||
|
||||
let filePermissions = gExtensionsJSON.permissions;
|
||||
if (!OS.Constants.Win) {
|
||||
gExtensionsJSON.permissions = 0;
|
||||
}
|
||||
startupManager(false);
|
||||
|
||||
// Shouldn't have seen any startup changes
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
|
||||
// Accessing the add-ons should open and recover the database
|
||||
[a1, a2, a3, a4, a5, a6, a7, t1, t2] =
|
||||
yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org",
|
||||
"addon6@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme1@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"]);
|
||||
|
||||
// Should be correctly recovered
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
|
||||
// Should be correctly recovered
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_true(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
|
||||
// The compatibility update won't be recovered but it should still be
|
||||
// active for this session
|
||||
do_check_neq(a3, null);
|
||||
do_check_true(a3.isActive);
|
||||
do_check_false(a3.userDisabled);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a3.id));
|
||||
|
||||
// The compatibility update won't be recovered and with strict
|
||||
// compatibility it would not have been able to tell that it was
|
||||
// previously userDisabled. However, without strict compat, it wasn't
|
||||
// appDisabled, so it knows it must have been userDisabled.
|
||||
do_check_neq(a4, null);
|
||||
do_check_false(a4.isActive);
|
||||
do_check_true(a4.userDisabled);
|
||||
do_check_false(a4.appDisabled);
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a4.id));
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_true(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_false(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a5.id));
|
||||
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(a7, null);
|
||||
do_check_false(a7.isActive);
|
||||
do_check_true(a7.userDisabled);
|
||||
do_check_false(a7.appDisabled);
|
||||
do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
// Should be correctly recovered
|
||||
do_check_neq(t1, null);
|
||||
do_check_false(t1.isActive);
|
||||
do_check_true(t1.userDisabled);
|
||||
do_check_false(t1.appDisabled);
|
||||
do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isThemeInAddonsList(profileDir, t1.id));
|
||||
|
||||
// Should be correctly recovered
|
||||
do_check_neq(t2, null);
|
||||
do_check_true(t2.isActive);
|
||||
do_check_false(t2.userDisabled);
|
||||
do_check_false(t2.appDisabled);
|
||||
do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isThemeInAddonsList(profileDir, t2.id));
|
||||
|
||||
// Restarting will actually apply changes to extensions.ini which will
|
||||
// then be put into the in-memory database when we next fail to load the
|
||||
// real thing
|
||||
restartManager();
|
||||
|
||||
// Shouldn't have seen any startup changes
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
|
||||
[a1, a2, a3, a4, a5, a6, a7, t1, t2] =
|
||||
yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
|
@ -192,340 +375,155 @@ function run_test_1() {
|
|||
"addon6@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme1@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"],
|
||||
callback_soon(function([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
"theme2@tests.mozilla.org"]);
|
||||
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_true(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
|
||||
do_check_neq(a3, null);
|
||||
do_check_true(a3.isActive);
|
||||
do_check_false(a3.userDisabled);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a3.id));
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_true(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
|
||||
do_check_neq(a4, null);
|
||||
do_check_false(a4.isActive);
|
||||
do_check_true(a4.userDisabled);
|
||||
do_check_false(a4.appDisabled);
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a4.id));
|
||||
do_check_neq(a3, null);
|
||||
do_check_true(a3.isActive);
|
||||
do_check_false(a3.userDisabled);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a3.id));
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_true(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_false(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a5.id));
|
||||
do_check_neq(a4, null);
|
||||
do_check_false(a4.isActive);
|
||||
do_check_true(a4.userDisabled);
|
||||
do_check_false(a4.appDisabled);
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a4.id));
|
||||
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_neq(a5, null);
|
||||
do_check_true(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_false(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a5.id));
|
||||
|
||||
do_check_neq(a7, null);
|
||||
do_check_false(a7.isActive);
|
||||
do_check_true(a7.userDisabled);
|
||||
do_check_false(a7.appDisabled);
|
||||
do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(t1, null);
|
||||
do_check_false(t1.isActive);
|
||||
do_check_true(t1.userDisabled);
|
||||
do_check_false(t1.appDisabled);
|
||||
do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isThemeInAddonsList(profileDir, t1.id));
|
||||
do_check_neq(a7, null);
|
||||
do_check_false(a7.isActive);
|
||||
do_check_true(a7.userDisabled);
|
||||
do_check_false(a7.appDisabled);
|
||||
do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(t2, null);
|
||||
do_check_true(t2.isActive);
|
||||
do_check_false(t2.userDisabled);
|
||||
do_check_false(t2.appDisabled);
|
||||
do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isThemeInAddonsList(profileDir, t2.id));
|
||||
do_check_neq(t1, null);
|
||||
do_check_false(t1.isActive);
|
||||
do_check_true(t1.userDisabled);
|
||||
do_check_false(t1.appDisabled);
|
||||
do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isThemeInAddonsList(profileDir, t1.id));
|
||||
|
||||
// Open another handle on the JSON DB with as much Unix and Windows locking
|
||||
// as we can to simulate some other process interfering with it
|
||||
shutdownManager();
|
||||
do_print("Locking " + gExtensionsJSON.path);
|
||||
let options = {
|
||||
winShare: 0
|
||||
};
|
||||
if (OS.Constants.libc.O_EXLOCK)
|
||||
options.unixFlags = OS.Constants.libc.O_EXLOCK;
|
||||
do_check_neq(t2, null);
|
||||
do_check_true(t2.isActive);
|
||||
do_check_false(t2.userDisabled);
|
||||
do_check_false(t2.appDisabled);
|
||||
do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isThemeInAddonsList(profileDir, t2.id));
|
||||
|
||||
OS.File.open(gExtensionsJSON.path, {read:true, write:true, existing:true}, options).then(
|
||||
file => {
|
||||
filePermissions = gExtensionsJSON.permissions;
|
||||
if (!OS.Constants.Win) {
|
||||
gExtensionsJSON.permissions = 0;
|
||||
}
|
||||
startupManager(false);
|
||||
|
||||
// Shouldn't have seen any startup changes
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
|
||||
// Accessing the add-ons should open and recover the database
|
||||
AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org",
|
||||
"addon6@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme1@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"],
|
||||
callback_soon(function get_after_lock([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
|
||||
// Should be correctly recovered
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
|
||||
// Should be correctly recovered
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_true(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
|
||||
// The compatibility update won't be recovered but it should still be
|
||||
// active for this session
|
||||
do_check_neq(a3, null);
|
||||
do_check_true(a3.isActive);
|
||||
do_check_false(a3.userDisabled);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a3.id));
|
||||
|
||||
// The compatibility update won't be recovered and with strict
|
||||
// compatibility it would not have been able to tell that it was
|
||||
// previously userDisabled. However, without strict compat, it wasn't
|
||||
// appDisabled, so it knows it must have been userDisabled.
|
||||
do_check_neq(a4, null);
|
||||
do_check_false(a4.isActive);
|
||||
do_check_true(a4.userDisabled);
|
||||
do_check_false(a4.appDisabled);
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a4.id));
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_true(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_false(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a5.id));
|
||||
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(a7, null);
|
||||
do_check_false(a7.isActive);
|
||||
do_check_true(a7.userDisabled);
|
||||
do_check_false(a7.appDisabled);
|
||||
do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
// Should be correctly recovered
|
||||
do_check_neq(t1, null);
|
||||
do_check_false(t1.isActive);
|
||||
do_check_true(t1.userDisabled);
|
||||
do_check_false(t1.appDisabled);
|
||||
do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isThemeInAddonsList(profileDir, t1.id));
|
||||
|
||||
// Should be correctly recovered
|
||||
do_check_neq(t2, null);
|
||||
do_check_true(t2.isActive);
|
||||
do_check_false(t2.userDisabled);
|
||||
do_check_false(t2.appDisabled);
|
||||
do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isThemeInAddonsList(profileDir, t2.id));
|
||||
|
||||
// Restarting will actually apply changes to extensions.ini which will
|
||||
// then be put into the in-memory database when we next fail to load the
|
||||
// real thing
|
||||
restartManager();
|
||||
|
||||
// Shouldn't have seen any startup changes
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
|
||||
AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org",
|
||||
"addon6@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme1@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"],
|
||||
callback_soon(function([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_true(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
|
||||
do_check_neq(a3, null);
|
||||
do_check_true(a3.isActive);
|
||||
do_check_false(a3.userDisabled);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a3.id));
|
||||
|
||||
do_check_neq(a4, null);
|
||||
do_check_false(a4.isActive);
|
||||
do_check_true(a4.userDisabled);
|
||||
do_check_false(a4.appDisabled);
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a4.id));
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_true(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_false(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a5.id));
|
||||
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(a7, null);
|
||||
do_check_false(a7.isActive);
|
||||
do_check_true(a7.userDisabled);
|
||||
do_check_false(a7.appDisabled);
|
||||
do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(t1, null);
|
||||
do_check_false(t1.isActive);
|
||||
do_check_true(t1.userDisabled);
|
||||
do_check_false(t1.appDisabled);
|
||||
do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isThemeInAddonsList(profileDir, t1.id));
|
||||
|
||||
do_check_neq(t2, null);
|
||||
do_check_true(t2.isActive);
|
||||
do_check_false(t2.userDisabled);
|
||||
do_check_false(t2.appDisabled);
|
||||
do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isThemeInAddonsList(profileDir, t2.id));
|
||||
|
||||
// After allowing access to the original DB things should go back to as
|
||||
// they were previously
|
||||
shutdownManager();
|
||||
do_print("Unlocking " + gExtensionsJSON.path);
|
||||
file.close();
|
||||
gExtensionsJSON.permissions = filePermissions;
|
||||
startupManager();
|
||||
// After allowing access to the original DB things should go back to as
|
||||
// they were previously
|
||||
shutdownManager();
|
||||
do_print("Unlocking " + gExtensionsJSON.path);
|
||||
yield file.close();
|
||||
gExtensionsJSON.permissions = filePermissions;
|
||||
startupManager();
|
||||
|
||||
|
||||
// Shouldn't have seen any startup changes
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
// Shouldn't have seen any startup changes
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
|
||||
AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org",
|
||||
"addon6@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme1@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"],
|
||||
callback_soon(function([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
[a1, a2, a3, a4, a5, a6, a7, t1, t2] =
|
||||
yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org",
|
||||
"addon6@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme1@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"]);
|
||||
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_true(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
|
||||
do_check_neq(a3, null);
|
||||
do_check_true(a3.isActive);
|
||||
do_check_false(a3.userDisabled);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a3.id));
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_true(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
|
||||
do_check_neq(a4, null);
|
||||
do_check_false(a4.isActive);
|
||||
do_check_true(a4.userDisabled);
|
||||
do_check_false(a4.appDisabled);
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a4.id));
|
||||
do_check_neq(a3, null);
|
||||
do_check_true(a3.isActive);
|
||||
do_check_false(a3.userDisabled);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a3.id));
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_true(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_false(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a5.id));
|
||||
do_check_neq(a4, null);
|
||||
do_check_false(a4.isActive);
|
||||
do_check_true(a4.userDisabled);
|
||||
do_check_false(a4.appDisabled);
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a4.id));
|
||||
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_neq(a5, null);
|
||||
do_check_true(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_false(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a5.id));
|
||||
|
||||
do_check_neq(a7, null);
|
||||
do_check_false(a7.isActive);
|
||||
do_check_true(a7.userDisabled);
|
||||
do_check_false(a7.appDisabled);
|
||||
do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(t1, null);
|
||||
do_check_false(t1.isActive);
|
||||
do_check_true(t1.userDisabled);
|
||||
do_check_false(t1.appDisabled);
|
||||
do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isThemeInAddonsList(profileDir, t1.id));
|
||||
do_check_neq(a7, null);
|
||||
do_check_false(a7.isActive);
|
||||
do_check_true(a7.userDisabled);
|
||||
do_check_false(a7.appDisabled);
|
||||
do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(t2, null);
|
||||
do_check_true(t2.isActive);
|
||||
do_check_false(t2.userDisabled);
|
||||
do_check_false(t2.appDisabled);
|
||||
do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isThemeInAddonsList(profileDir, t2.id));
|
||||
do_check_neq(t1, null);
|
||||
do_check_false(t1.isActive);
|
||||
do_check_true(t1.userDisabled);
|
||||
do_check_false(t1.appDisabled);
|
||||
do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isThemeInAddonsList(profileDir, t1.id));
|
||||
|
||||
end_test();
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
},
|
||||
do_report_unexpected_exception
|
||||
);
|
||||
}));
|
||||
do_check_neq(t2, null);
|
||||
do_check_true(t2.isActive);
|
||||
do_check_false(t2.userDisabled);
|
||||
do_check_false(t2.appDisabled);
|
||||
do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isThemeInAddonsList(profileDir, t2.id));
|
||||
});
|
||||
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
|
|
@ -71,8 +71,8 @@ var addon5 = {
|
|||
const profileDir = gProfD.clone();
|
||||
profileDir.append("extensions");
|
||||
|
||||
function run_test() {
|
||||
do_test_pending();
|
||||
add_task(function() {
|
||||
|
||||
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
|
||||
|
||||
writeInstallRDFForExtension(addon1, profileDir);
|
||||
|
@ -83,7 +83,8 @@ function run_test() {
|
|||
|
||||
// Make it look like add-on 5 was installed some time in the past so the update is
|
||||
// detected
|
||||
setExtensionModifiedTime(getFileForAddon(profileDir, addon5.id), Date.now() - (60000));
|
||||
let path = getFileForAddon(profileDir, addon5.id).path;
|
||||
yield promiseSetExtensionModifiedTime(path, Date.now() - (60000));
|
||||
|
||||
// Startup the profile and setup the initial state
|
||||
startupManager();
|
||||
|
@ -91,205 +92,201 @@ function run_test() {
|
|||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
|
||||
|
||||
AddonManager.getAddonByID("addon2@tests.mozilla.org", callback_soon(function(a2) {
|
||||
a2.userDisabled = true;
|
||||
let a1, a2, a3, a4, a5, a6;
|
||||
|
||||
restartManager();
|
||||
[a2] = yield promiseAddonsByIDs(["addon2@tests.mozilla.org"]);
|
||||
a2.userDisabled = true;
|
||||
|
||||
AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org"],
|
||||
function([a1, a2, a3, a4, a5]) {
|
||||
a2.userDisabled = false;
|
||||
a3.userDisabled = true;
|
||||
a4.uninstall();
|
||||
restartManager();
|
||||
|
||||
installAllFiles([do_get_addon("test_locked2_5"),
|
||||
do_get_addon("test_locked2_6")], function locked_installed() {
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
[a1, a2, a3, a4, a5] =
|
||||
yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org"]);
|
||||
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_false(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_ENABLE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
a2.userDisabled = false;
|
||||
a3.userDisabled = true;
|
||||
a4.uninstall();
|
||||
|
||||
do_check_neq(a3, null);
|
||||
do_check_true(a3.isActive);
|
||||
do_check_true(a3.userDisabled);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_DISABLE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a3.id));
|
||||
yield promiseInstallAllFiles([do_get_addon("test_locked2_5"),
|
||||
do_get_addon("test_locked2_6")]);
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
|
||||
do_check_neq(a4, null);
|
||||
do_check_true(a4.isActive);
|
||||
do_check_false(a4.userDisabled);
|
||||
do_check_false(a4.appDisabled);
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_UNINSTALL);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a4.id));
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_false(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_ENABLE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_eq(a5.version, "1.0");
|
||||
do_check_true(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_false(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_UPGRADE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a5.id));
|
||||
do_check_neq(a3, null);
|
||||
do_check_true(a3.isActive);
|
||||
do_check_true(a3.userDisabled);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_DISABLE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a3.id));
|
||||
|
||||
// Open another handle on the JSON DB with as much Unix and Windows locking
|
||||
// as we can to simulate some other process interfering with it
|
||||
shutdownManager();
|
||||
do_print("Locking " + gExtensionsJSON.path);
|
||||
let options = {
|
||||
winShare: 0
|
||||
};
|
||||
if (OS.Constants.libc.O_EXLOCK)
|
||||
options.unixFlags = OS.Constants.libc.O_EXLOCK;
|
||||
do_check_neq(a4, null);
|
||||
do_check_true(a4.isActive);
|
||||
do_check_false(a4.userDisabled);
|
||||
do_check_false(a4.appDisabled);
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_UNINSTALL);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a4.id));
|
||||
|
||||
OS.File.open(gExtensionsJSON.path, {read:true, write:true, existing:true}, options).then(
|
||||
file => {
|
||||
filePermissions = gExtensionsJSON.permissions;
|
||||
if (!OS.Constants.Win) {
|
||||
gExtensionsJSON.permissions = 0;
|
||||
}
|
||||
startupManager(false);
|
||||
do_check_neq(a5, null);
|
||||
do_check_eq(a5.version, "1.0");
|
||||
do_check_true(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_false(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_UPGRADE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a5.id));
|
||||
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
|
||||
// Open another handle on the JSON DB with as much Unix and Windows locking
|
||||
// as we can to simulate some other process interfering with it
|
||||
shutdownManager();
|
||||
do_print("Locking " + gExtensionsJSON.path);
|
||||
let options = {
|
||||
winShare: 0
|
||||
};
|
||||
if (OS.Constants.libc.O_EXLOCK)
|
||||
options.unixFlags = OS.Constants.libc.O_EXLOCK;
|
||||
|
||||
AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org",
|
||||
"addon6@tests.mozilla.org"],
|
||||
callback_soon(function([a1, a2, a3, a4, a5, a6]) {
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
let file = yield OS.File.open(gExtensionsJSON.path, {read:true, write:true, existing:true}, options);
|
||||
|
||||
do_check_neq(a2, null);
|
||||
do_check_true(a2.isActive);
|
||||
do_check_false(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a2.id));
|
||||
let filePermissions = gExtensionsJSON.permissions;
|
||||
if (!OS.Constants.Win) {
|
||||
gExtensionsJSON.permissions = 0;
|
||||
}
|
||||
startupManager(false);
|
||||
|
||||
do_check_neq(a3, null);
|
||||
do_check_false(a3.isActive);
|
||||
do_check_true(a3.userDisabled);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a3.id));
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
|
||||
|
||||
do_check_eq(a4, null);
|
||||
[a1, a2, a3, a4, a5, a6] =
|
||||
yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org",
|
||||
"addon6@tests.mozilla.org"]);
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_eq(a5.version, "2.0");
|
||||
do_check_true(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_false(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a5.id));
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a6.id));
|
||||
do_check_neq(a2, null);
|
||||
do_check_true(a2.isActive);
|
||||
do_check_false(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a2.id));
|
||||
|
||||
// After allowing access to the original DB things should still be
|
||||
// back how they were before the lock
|
||||
shutdownManager();
|
||||
file.close();
|
||||
gExtensionsJSON.permissions = filePermissions;
|
||||
startupManager();
|
||||
do_check_neq(a3, null);
|
||||
do_check_false(a3.isActive);
|
||||
do_check_true(a3.userDisabled);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a3.id));
|
||||
|
||||
// On Unix, we can save the DB even when the original file wasn't
|
||||
// readable, so our changes were saved. On Windows,
|
||||
// these things happened when we had no access to the database so
|
||||
// they are seen as external changes when we get the database back
|
||||
if (gXPISaveError) {
|
||||
do_print("Previous XPI save failed");
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED,
|
||||
["addon6@tests.mozilla.org"]);
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED,
|
||||
["addon4@tests.mozilla.org"]);
|
||||
}
|
||||
else {
|
||||
do_print("Previous XPI save succeeded");
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
|
||||
}
|
||||
do_check_eq(a4, null);
|
||||
|
||||
AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org",
|
||||
"addon6@tests.mozilla.org"],
|
||||
function([a1, a2, a3, a4, a5, a6]) {
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
do_check_neq(a5, null);
|
||||
do_check_eq(a5.version, "2.0");
|
||||
do_check_true(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_false(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a5.id));
|
||||
|
||||
do_check_neq(a2, null);
|
||||
do_check_true(a2.isActive);
|
||||
do_check_false(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a2.id));
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a6.id));
|
||||
|
||||
do_check_neq(a3, null);
|
||||
do_check_false(a3.isActive);
|
||||
do_check_true(a3.userDisabled);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a3.id));
|
||||
// After allowing access to the original DB things should still be
|
||||
// back how they were before the lock
|
||||
shutdownManager();
|
||||
yield file.close();
|
||||
gExtensionsJSON.permissions = filePermissions;
|
||||
startupManager();
|
||||
|
||||
do_check_eq(a4, null);
|
||||
// On Unix, we can save the DB even when the original file wasn't
|
||||
// readable, so our changes were saved. On Windows,
|
||||
// these things happened when we had no access to the database so
|
||||
// they are seen as external changes when we get the database back
|
||||
if (gXPISaveError) {
|
||||
do_print("Previous XPI save failed");
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED,
|
||||
["addon6@tests.mozilla.org"]);
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED,
|
||||
["addon4@tests.mozilla.org"]);
|
||||
}
|
||||
else {
|
||||
do_print("Previous XPI save succeeded");
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
|
||||
}
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_eq(a5.version, "2.0");
|
||||
do_check_true(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_false(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a5.id));
|
||||
[a1, a2, a3, a4, a5, a6] =
|
||||
yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org",
|
||||
"addon6@tests.mozilla.org"]);
|
||||
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a6.id));
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
|
||||
end_test();
|
||||
});
|
||||
}));
|
||||
},
|
||||
do_report_unexpected_exception
|
||||
);
|
||||
});
|
||||
});
|
||||
}));
|
||||
do_check_neq(a2, null);
|
||||
do_check_true(a2.isActive);
|
||||
do_check_false(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a2.id));
|
||||
|
||||
do_check_neq(a3, null);
|
||||
do_check_false(a3.isActive);
|
||||
do_check_true(a3.userDisabled);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a3.id));
|
||||
|
||||
do_check_eq(a4, null);
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_eq(a5.version, "2.0");
|
||||
do_check_true(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_false(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a5.id));
|
||||
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a6.id));
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
function end_test() {
|
||||
do_execute_soon(do_test_finished);
|
||||
}
|
||||
|
|
|
@ -134,8 +134,7 @@ var theme2 = {
|
|||
const profileDir = gProfD.clone();
|
||||
profileDir.append("extensions");
|
||||
|
||||
function run_test() {
|
||||
do_test_pending();
|
||||
add_task(function* init() {
|
||||
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
|
||||
|
||||
writeInstallRDFForExtension(addon1, profileDir);
|
||||
|
@ -154,37 +153,222 @@ function run_test() {
|
|||
// New profile so new add-ons are ignored
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
|
||||
AddonManager.getAddonsByIDs(["addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"], function([a2, a3, a4,
|
||||
a7, t2]) {
|
||||
// Set up the initial state
|
||||
a2.userDisabled = true;
|
||||
a4.userDisabled = true;
|
||||
a7.userDisabled = true;
|
||||
t2.userDisabled = false;
|
||||
a3.findUpdates({
|
||||
onUpdateFinished: function() {
|
||||
a4.findUpdates({
|
||||
onUpdateFinished: function() {
|
||||
do_execute_soon(run_test_1);
|
||||
}
|
||||
}, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
|
||||
}
|
||||
}, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
|
||||
});
|
||||
}
|
||||
let a1, a2, a3, a4, a5, a6, a7, t1, t2;
|
||||
|
||||
function end_test() {
|
||||
testserver.stop(do_test_finished);
|
||||
}
|
||||
[a2, a3, a4, a7, t2] =
|
||||
yield promiseAddonsByIDs(["addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"]);
|
||||
|
||||
function run_test_1() {
|
||||
// Set up the initial state
|
||||
let deferredUpdateFinished = Promise.defer();
|
||||
|
||||
a2.userDisabled = true;
|
||||
a4.userDisabled = true;
|
||||
a7.userDisabled = true;
|
||||
t2.userDisabled = false;
|
||||
a3.findUpdates({
|
||||
onUpdateFinished: function() {
|
||||
a4.findUpdates({
|
||||
onUpdateFinished: function() {
|
||||
deferredUpdateFinished.resolve();
|
||||
}
|
||||
}, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
|
||||
}
|
||||
}, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
|
||||
yield deferredUpdateFinished.promise;
|
||||
});
|
||||
|
||||
add_task(function* run_test_1() {
|
||||
let a1, a2, a3, a4, a5, a6, a7, t1, t2;
|
||||
|
||||
restartManager();
|
||||
[a1, a2, a3, a4, a5, a6, a7, t1, t2] =
|
||||
yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org",
|
||||
"addon6@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme1@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"]);
|
||||
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_true(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
|
||||
do_check_neq(a3, null);
|
||||
do_check_true(a3.isActive);
|
||||
do_check_false(a3.userDisabled);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a3.id));
|
||||
|
||||
do_check_neq(a4, null);
|
||||
do_check_false(a4.isActive);
|
||||
do_check_true(a4.userDisabled);
|
||||
do_check_false(a4.appDisabled);
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a4.id));
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_false(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_true(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a5.id));
|
||||
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(a7, null);
|
||||
do_check_false(a7.isActive);
|
||||
do_check_true(a7.userDisabled);
|
||||
do_check_false(a7.appDisabled);
|
||||
do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(t1, null);
|
||||
do_check_false(t1.isActive);
|
||||
do_check_true(t1.userDisabled);
|
||||
do_check_false(t1.appDisabled);
|
||||
do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isThemeInAddonsList(profileDir, t1.id));
|
||||
|
||||
do_check_neq(t2, null);
|
||||
do_check_true(t2.isActive);
|
||||
do_check_false(t2.userDisabled);
|
||||
do_check_false(t2.appDisabled);
|
||||
do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isThemeInAddonsList(profileDir, t2.id));
|
||||
|
||||
// Open another handle on the JSON DB with as much Unix and Windows locking
|
||||
// as we can to simulate some other process interfering with it
|
||||
shutdownManager();
|
||||
do_print("Locking " + gExtensionsJSON.path);
|
||||
let options = {
|
||||
winShare: 0
|
||||
};
|
||||
if (OS.Constants.libc.O_EXLOCK)
|
||||
options.unixFlags = OS.Constants.libc.O_EXLOCK;
|
||||
|
||||
let file = yield OS.File.open(gExtensionsJSON.path, {read:true, write:true, existing:true}, options);
|
||||
|
||||
let filePermissions = gExtensionsJSON.permissions;
|
||||
if (!OS.Constants.Win) {
|
||||
gExtensionsJSON.permissions = 0;
|
||||
}
|
||||
startupManager(false);
|
||||
|
||||
// Shouldn't have seen any startup changes
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
|
||||
// Accessing the add-ons should open and recover the database
|
||||
[a1, a2, a3, a4, a5, a6, a7, t1, t2] =
|
||||
yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org",
|
||||
"addon6@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme1@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"]);
|
||||
|
||||
// Should be correctly recovered
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
|
||||
// Should be correctly recovered
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_true(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
|
||||
// The compatibility update won't be recovered but it should still be
|
||||
// active for this session
|
||||
do_check_neq(a3, null);
|
||||
do_check_true(a3.isActive);
|
||||
do_check_false(a3.userDisabled);
|
||||
do_check_true(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_DISABLE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a3.id));
|
||||
|
||||
// The compatibility update won't be recovered and it will not have been
|
||||
// able to tell that it was previously userDisabled
|
||||
do_check_neq(a4, null);
|
||||
do_check_false(a4.isActive);
|
||||
do_check_false(a4.userDisabled);
|
||||
do_check_true(a4.appDisabled);
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a4.id));
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_false(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_true(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a5.id));
|
||||
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(a7, null);
|
||||
do_check_false(a7.isActive);
|
||||
do_check_true(a7.userDisabled);
|
||||
do_check_false(a7.appDisabled);
|
||||
do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
// Should be correctly recovered
|
||||
do_check_neq(t1, null);
|
||||
do_check_false(t1.isActive);
|
||||
do_check_true(t1.userDisabled);
|
||||
do_check_false(t1.appDisabled);
|
||||
do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isThemeInAddonsList(profileDir, t1.id));
|
||||
|
||||
// Should be correctly recovered
|
||||
do_check_neq(t2, null);
|
||||
do_check_true(t2.isActive);
|
||||
do_check_false(t2.userDisabled);
|
||||
do_check_false(t2.appDisabled);
|
||||
do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isThemeInAddonsList(profileDir, t2.id));
|
||||
|
||||
// Restarting will actually apply changes to extensions.ini which will
|
||||
// then be put into the in-memory database when we next fail to load the
|
||||
// real thing
|
||||
restartManager();
|
||||
|
||||
AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
// Shouldn't have seen any startup changes
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
|
||||
[a1, a2, a3, a4, a5, a6, a7, t1, t2] =
|
||||
yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
|
@ -192,359 +376,176 @@ function run_test_1() {
|
|||
"addon6@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme1@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"],
|
||||
callback_soon(function([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
"theme2@tests.mozilla.org"]);
|
||||
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_true(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
|
||||
do_check_neq(a3, null);
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_true(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
|
||||
do_check_neq(a3, null);
|
||||
do_check_false(a3.isActive);
|
||||
do_check_false(a3.userDisabled);
|
||||
do_check_true(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a3.id));
|
||||
|
||||
do_check_neq(a4, null);
|
||||
do_check_false(a4.isActive);
|
||||
do_check_false(a4.userDisabled);
|
||||
do_check_true(a4.appDisabled);
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a4.id));
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_false(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_true(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a5.id));
|
||||
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(a7, null);
|
||||
do_check_false(a7.isActive);
|
||||
do_check_true(a7.userDisabled);
|
||||
do_check_false(a7.appDisabled);
|
||||
do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(t1, null);
|
||||
do_check_false(t1.isActive);
|
||||
do_check_true(t1.userDisabled);
|
||||
do_check_false(t1.appDisabled);
|
||||
do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isThemeInAddonsList(profileDir, t1.id));
|
||||
|
||||
do_check_neq(t2, null);
|
||||
do_check_true(t2.isActive);
|
||||
do_check_false(t2.userDisabled);
|
||||
do_check_false(t2.appDisabled);
|
||||
do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isThemeInAddonsList(profileDir, t2.id));
|
||||
|
||||
// After allowing access to the original DB things should go back to as
|
||||
// back how they were before the lock
|
||||
shutdownManager();
|
||||
do_print("Unlocking " + gExtensionsJSON.path);
|
||||
yield file.close();
|
||||
gExtensionsJSON.permissions = filePermissions;
|
||||
startupManager(false);
|
||||
|
||||
// Shouldn't have seen any startup changes
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
|
||||
[a1, a2, a3, a4, a5, a6, a7, t1, t2] =
|
||||
yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org",
|
||||
"addon6@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme1@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"]);
|
||||
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_true(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
|
||||
do_check_neq(a3, null);
|
||||
do_check_false(a3.userDisabled);
|
||||
// On Unix, we may be able to save our changes over the locked DB so we
|
||||
// remember that this extension was changed to disabled. On Windows we
|
||||
// couldn't replace the old DB so we read the older version of the DB
|
||||
// where the extension is enabled
|
||||
if (gXPISaveError) {
|
||||
do_print("XPI save failed");
|
||||
do_check_true(a3.isActive);
|
||||
do_check_false(a3.userDisabled);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a3.id));
|
||||
}
|
||||
else {
|
||||
do_print("XPI save succeeded");
|
||||
do_check_false(a3.isActive);
|
||||
do_check_true(a3.appDisabled);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a3.id));
|
||||
}
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(a4, null);
|
||||
do_check_false(a4.isActive);
|
||||
do_check_neq(a4, null);
|
||||
do_check_false(a4.isActive);
|
||||
// The reverse of the platform difference for a3 - Unix should
|
||||
// stay the same as the last iteration because the save succeeded,
|
||||
// Windows should still say userDisabled
|
||||
if (OS.Constants.Win) {
|
||||
do_check_true(a4.userDisabled);
|
||||
do_check_false(a4.appDisabled);
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a4.id));
|
||||
}
|
||||
else {
|
||||
do_check_false(a4.userDisabled);
|
||||
do_check_true(a4.appDisabled);
|
||||
}
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a4.id));
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_false(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_true(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a5.id));
|
||||
do_check_neq(a5, null);
|
||||
do_check_false(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_true(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a5.id));
|
||||
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(a7, null);
|
||||
do_check_false(a7.isActive);
|
||||
do_check_true(a7.userDisabled);
|
||||
do_check_false(a7.appDisabled);
|
||||
do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_neq(a7, null);
|
||||
do_check_false(a7.isActive);
|
||||
do_check_true(a7.userDisabled);
|
||||
do_check_false(a7.appDisabled);
|
||||
do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(t1, null);
|
||||
do_check_false(t1.isActive);
|
||||
do_check_true(t1.userDisabled);
|
||||
do_check_false(t1.appDisabled);
|
||||
do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isThemeInAddonsList(profileDir, t1.id));
|
||||
do_check_neq(t1, null);
|
||||
do_check_false(t1.isActive);
|
||||
do_check_true(t1.userDisabled);
|
||||
do_check_false(t1.appDisabled);
|
||||
do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isThemeInAddonsList(profileDir, t1.id));
|
||||
|
||||
do_check_neq(t2, null);
|
||||
do_check_true(t2.isActive);
|
||||
do_check_false(t2.userDisabled);
|
||||
do_check_false(t2.appDisabled);
|
||||
do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isThemeInAddonsList(profileDir, t2.id));
|
||||
do_check_neq(t2, null);
|
||||
do_check_true(t2.isActive);
|
||||
do_check_false(t2.userDisabled);
|
||||
do_check_false(t2.appDisabled);
|
||||
do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isThemeInAddonsList(profileDir, t2.id));
|
||||
});
|
||||
|
||||
// Open another handle on the JSON DB with as much Unix and Windows locking
|
||||
// as we can to simulate some other process interfering with it
|
||||
shutdownManager();
|
||||
do_print("Locking " + gExtensionsJSON.path);
|
||||
let options = {
|
||||
winShare: 0
|
||||
};
|
||||
if (OS.Constants.libc.O_EXLOCK)
|
||||
options.unixFlags = OS.Constants.libc.O_EXLOCK;
|
||||
|
||||
OS.File.open(gExtensionsJSON.path, {read:true, write:true, existing:true}, options).then(
|
||||
file => {
|
||||
filePermissions = gExtensionsJSON.permissions;
|
||||
if (!OS.Constants.Win) {
|
||||
gExtensionsJSON.permissions = 0;
|
||||
}
|
||||
startupManager(false);
|
||||
|
||||
// Shouldn't have seen any startup changes
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
|
||||
// Accessing the add-ons should open and recover the database
|
||||
AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org",
|
||||
"addon6@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme1@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"],
|
||||
callback_soon(function([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
|
||||
// Should be correctly recovered
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
|
||||
// Should be correctly recovered
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_true(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
|
||||
// The compatibility update won't be recovered but it should still be
|
||||
// active for this session
|
||||
do_check_neq(a3, null);
|
||||
do_check_true(a3.isActive);
|
||||
do_check_false(a3.userDisabled);
|
||||
do_check_true(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_DISABLE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a3.id));
|
||||
|
||||
// The compatibility update won't be recovered and it will not have been
|
||||
// able to tell that it was previously userDisabled
|
||||
do_check_neq(a4, null);
|
||||
do_check_false(a4.isActive);
|
||||
do_check_false(a4.userDisabled);
|
||||
do_check_true(a4.appDisabled);
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a4.id));
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_false(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_true(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a5.id));
|
||||
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(a7, null);
|
||||
do_check_false(a7.isActive);
|
||||
do_check_true(a7.userDisabled);
|
||||
do_check_false(a7.appDisabled);
|
||||
do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
// Should be correctly recovered
|
||||
do_check_neq(t1, null);
|
||||
do_check_false(t1.isActive);
|
||||
do_check_true(t1.userDisabled);
|
||||
do_check_false(t1.appDisabled);
|
||||
do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isThemeInAddonsList(profileDir, t1.id));
|
||||
|
||||
// Should be correctly recovered
|
||||
do_check_neq(t2, null);
|
||||
do_check_true(t2.isActive);
|
||||
do_check_false(t2.userDisabled);
|
||||
do_check_false(t2.appDisabled);
|
||||
do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isThemeInAddonsList(profileDir, t2.id));
|
||||
|
||||
// Restarting will actually apply changes to extensions.ini which will
|
||||
// then be put into the in-memory database when we next fail to load the
|
||||
// real thing
|
||||
restartManager();
|
||||
|
||||
// Shouldn't have seen any startup changes
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
|
||||
AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org",
|
||||
"addon6@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme1@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"],
|
||||
callback_soon(function([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_true(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
|
||||
do_check_neq(a3, null);
|
||||
do_check_false(a3.isActive);
|
||||
do_check_false(a3.userDisabled);
|
||||
do_check_true(a3.appDisabled);
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a3.id));
|
||||
|
||||
do_check_neq(a4, null);
|
||||
do_check_false(a4.isActive);
|
||||
do_check_false(a4.userDisabled);
|
||||
do_check_true(a4.appDisabled);
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a4.id));
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_false(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_true(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a5.id));
|
||||
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(a7, null);
|
||||
do_check_false(a7.isActive);
|
||||
do_check_true(a7.userDisabled);
|
||||
do_check_false(a7.appDisabled);
|
||||
do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(t1, null);
|
||||
do_check_false(t1.isActive);
|
||||
do_check_true(t1.userDisabled);
|
||||
do_check_false(t1.appDisabled);
|
||||
do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isThemeInAddonsList(profileDir, t1.id));
|
||||
|
||||
do_check_neq(t2, null);
|
||||
do_check_true(t2.isActive);
|
||||
do_check_false(t2.userDisabled);
|
||||
do_check_false(t2.appDisabled);
|
||||
do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isThemeInAddonsList(profileDir, t2.id));
|
||||
|
||||
// After allowing access to the original DB things should go back to as
|
||||
// back how they were before the lock
|
||||
shutdownManager();
|
||||
do_print("Unlocking " + gExtensionsJSON.path);
|
||||
file.close();
|
||||
gExtensionsJSON.permissions = filePermissions;
|
||||
startupManager(false);
|
||||
|
||||
// Shouldn't have seen any startup changes
|
||||
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
|
||||
|
||||
AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
|
||||
"addon2@tests.mozilla.org",
|
||||
"addon3@tests.mozilla.org",
|
||||
"addon4@tests.mozilla.org",
|
||||
"addon5@tests.mozilla.org",
|
||||
"addon6@tests.mozilla.org",
|
||||
"addon7@tests.mozilla.org",
|
||||
"theme1@tests.mozilla.org",
|
||||
"theme2@tests.mozilla.org"],
|
||||
callback_soon(function([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
|
||||
do_check_neq(a1, null);
|
||||
do_check_true(a1.isActive);
|
||||
do_check_false(a1.userDisabled);
|
||||
do_check_false(a1.appDisabled);
|
||||
do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a1.id));
|
||||
|
||||
do_check_neq(a2, null);
|
||||
do_check_false(a2.isActive);
|
||||
do_check_true(a2.userDisabled);
|
||||
do_check_false(a2.appDisabled);
|
||||
do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a2.id));
|
||||
|
||||
do_check_neq(a3, null);
|
||||
do_check_false(a3.userDisabled);
|
||||
// On Unix, we may be able to save our changes over the locked DB so we
|
||||
// remember that this extension was changed to disabled. On Windows we
|
||||
// couldn't replace the old DB so we read the older version of the DB
|
||||
// where the extension is enabled
|
||||
if (gXPISaveError) {
|
||||
do_print("XPI save failed");
|
||||
do_check_true(a3.isActive);
|
||||
do_check_false(a3.appDisabled);
|
||||
do_check_true(isExtensionInAddonsList(profileDir, a3.id));
|
||||
}
|
||||
else {
|
||||
do_print("XPI save succeeded");
|
||||
do_check_false(a3.isActive);
|
||||
do_check_true(a3.appDisabled);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a3.id));
|
||||
}
|
||||
do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(a4, null);
|
||||
do_check_false(a4.isActive);
|
||||
// The reverse of the platform difference for a3 - Unix should
|
||||
// stay the same as the last iteration because the save succeeded,
|
||||
// Windows should still say userDisabled
|
||||
if (OS.Constants.Win) {
|
||||
do_check_true(a4.userDisabled);
|
||||
do_check_false(a4.appDisabled);
|
||||
}
|
||||
else {
|
||||
do_check_false(a4.userDisabled);
|
||||
do_check_true(a4.appDisabled);
|
||||
}
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a4.id));
|
||||
do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(a5, null);
|
||||
do_check_false(a5.isActive);
|
||||
do_check_false(a5.userDisabled);
|
||||
do_check_true(a5.appDisabled);
|
||||
do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isExtensionInAddonsList(profileDir, a5.id));
|
||||
|
||||
do_check_neq(a6, null);
|
||||
do_check_true(a6.isActive);
|
||||
do_check_false(a6.userDisabled);
|
||||
do_check_false(a6.appDisabled);
|
||||
do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(a7, null);
|
||||
do_check_false(a7.isActive);
|
||||
do_check_true(a7.userDisabled);
|
||||
do_check_false(a7.appDisabled);
|
||||
do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
|
||||
|
||||
do_check_neq(t1, null);
|
||||
do_check_false(t1.isActive);
|
||||
do_check_true(t1.userDisabled);
|
||||
do_check_false(t1.appDisabled);
|
||||
do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_false(isThemeInAddonsList(profileDir, t1.id));
|
||||
|
||||
do_check_neq(t2, null);
|
||||
do_check_true(t2.isActive);
|
||||
do_check_false(t2.userDisabled);
|
||||
do_check_false(t2.appDisabled);
|
||||
do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
|
||||
do_check_true(isThemeInAddonsList(profileDir, t2.id));
|
||||
|
||||
end_test();
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
},
|
||||
do_report_unexpected_exception
|
||||
);
|
||||
}));
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче