Merge m-c to inbound, a=merge CLOSED TREE

This commit is contained in:
Wes Kocher 2016-07-12 17:16:47 -07:00
commit 0dcab503c5
101 changed files with 10260 additions and 281 deletions

View File

@ -12,10 +12,11 @@ const { Ci } = require("chrome");
var { emit } = require("./core");
var { when: unload } = require("../system/unload");
var listeners = new Map();
var listeners = new WeakMap();
const { Cu } = require("chrome");
const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm");
const { ThreadSafeChromeUtils } = Cu.import("resource://gre/modules/Services.jsm", {});
var getWindowFrom = x =>
x instanceof Ci.nsIDOMWindow ? x :
@ -69,7 +70,8 @@ function open(target, type, options) {
}
unload(() => {
for (let window of listeners.keys())
let keys = ThreadSafeChromeUtils.nondeterministicGetWeakMapKeys(listeners)
for (let window of keys)
removeFromListeners.call(window);
});

View File

@ -542,6 +542,13 @@ Section "-Application" APP_IDX
${EndIf}
${EndIf}
; Update lastwritetime of the Start Menu shortcut to clear the tile cache.
${If} ${AtLeastWin8}
${AndIf} ${FileExists} "$SMPROGRAMS\${BrandFullName}.lnk"
FileOpen $0 "$SMPROGRAMS\${BrandFullName}.lnk" a
FileClose $0
${EndIf}
${If} $AddDesktopSC == 1
CreateShortCut "$DESKTOP\${BrandFullName}.lnk" "$INSTDIR\${FileMainEXE}"
${If} ${FileExists} "$DESKTOP\${BrandFullName}.lnk"

View File

@ -120,6 +120,13 @@
; root of the Start Menu Programs directory.
${MigrateStartMenuShortcut}
; Update lastwritetime of the Start Menu shortcut to clear the tile cache.
${If} ${AtLeastWin8}
${AndIf} ${FileExists} "$SMPROGRAMS\${BrandFullName}.lnk"
FileOpen $0 "$SMPROGRAMS\${BrandFullName}.lnk" a
FileClose $0
${EndIf}
; Adds a pinned Task Bar shortcut (see MigrateTaskBarShortcut for details).
${MigrateTaskBarShortcut}

View File

@ -11,3 +11,9 @@ description,
caption > label {
font-size: 1.2em;
}
/* Create a separate rule to unset these styles on .tree-input instead of
using :not(.tree-input) so the selector specifity doesn't change. */
textbox.tree-input {
font-size: unset;
}

View File

@ -17,6 +17,12 @@ caption > label {
font-size: 1.3em;
}
/* Create a separate rule to unset these styles on .tree-input instead of
using :not(.tree-input) so the selector specifity doesn't change. */
textbox.tree-input {
font-size: unset;
}
button {
font-size: 1em;
}

View File

@ -11,3 +11,9 @@ description,
caption > label {
font-size: 1.2em;
}
/* Create a separate rule to unset these styles on .tree-input instead of
using :not(.tree-input) so the selector specifity doesn't change. */
textbox.tree-input {
font-size: unset;
}

View File

@ -48,6 +48,7 @@ function onRequestProperties(state, action) {
function onReceiveProperties(cache, action) {
let response = action.response;
let from = response.from;
let className = action.grip.class;
// Properly deal with getters.
mergeProperties(response);
@ -55,7 +56,13 @@ function onReceiveProperties(cache, action) {
// Compute list of requested children.
let previewProps = response.preview ? response.preview.ownProperties : null;
let ownProps = response.ownProperties || previewProps || [];
let props = Object.keys(ownProps).map(key => {
// Array indexes as a special case. We convert any keys that are string
// representations of integers to integers.
if (className === "Array" && isInteger(key)) {
key = parseInt(key, 10);
}
return new Property(key, ownProps[key], key);
});
@ -100,6 +107,11 @@ function sortName(a, b) {
return a.name > b.name ? 1 : -1;
}
function isInteger(n) {
// We use parseInt(n, 10) == n to disregard scientific notation e.g. "3e24"
return isFinite(n) && parseInt(n, 10) == n;
}
function Property(name, value, key) {
this.name = name;
this.value = value;

View File

@ -890,7 +890,7 @@ InspectorPanel.prototype = {
menu.append(new MenuItem({
id: "node-menu-expand",
label: strings.GetStringFromName("inspectorExpandNode.label"),
disabled: !isNodeWithChildren || markupContainer.expanded,
disabled: !isNodeWithChildren,
click: () => this.expandNode(),
}));
menu.append(new MenuItem({

View File

@ -51,8 +51,11 @@ add_task(function* () {
});
nodeMenuCollapseElement =
allMenuItems.find(item => item.id === "node-menu-collapse");
nodeMenuExpandElement =
allMenuItems.find(item => item.id === "node-menu-expand");
ok(!nodeMenuCollapseElement.disabled, "Collapse option is enabled");
ok(!nodeMenuExpandElement.disabled, "ExpandAll option is enabled");
nodeMenuCollapseElement.click();
info("Waiting for collapse to occur");

View File

@ -2,8 +2,18 @@
# 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/.
# LOCALIZATION NOTE (debug):
# This string is displayed as a label of the button that starts
# debugging a service worker.
debug = Debug
# LOCALIZATION NOTE (push):
# This string is displayed as a label of the button that pushes a test payload
# to a service worker.
push = Push
# LOCALIZATION NOTE (start):
# This string is displayed as a label of the button that starts a service worker.
start = Start
scope = Scope
@ -11,25 +21,71 @@ unregister = unregister
pushService = Push Service
# LOCALIZATION NOTE (addons):
# This string is displayed as a header of the about:debugging#addons page.
addons = Add-ons
# LOCALIZATION NOTE (addonDebugging.label):
# This string is displayed next to a check box that enables the user to switch
# addon debugging on/off.
addonDebugging.label = Enable add-on debugging
# LOCALIZATION NOTE (addonDebugging.tooltip):
# This string is displayed in a tooltip that appears when hovering over a check
# box that switches addon debugging on/off.
addonDebugging.tooltip = Turning this on will allow you to debug add-ons and various other parts of the browser chrome
# LOCALIZATION NOTE (moreInfo):
# This string is displayed next to addonDebugging.label as a link to a page
# with documentation.
moreInfo = more info
# LOCALIZATION NOTE (loadTemporaryAddon):
# This string is displayed as a label of a button that allows the user to
# load additional add-ons.
loadTemporaryAddon = Load Temporary Add-on
# LOCALIZATION NOTE (extensions):
# This string is displayed as a header above the list of loaded add-ons.
extensions = Extensions
# LOCALIZATION NOTE (selectAddonFromFile2):
# This string is displayed as the title of the file picker that appears when
# the user clicks the 'Load Temporary Add-on' button
selectAddonFromFile2 = Select Manifest File or Package (.xpi)
# LOCALIZATION NOTE (reload):
# This string is displayed as a label of the button that reloads a given addon.
reload = Reload
# LOCALIZATION NOTE (reloadDisabledTooltip):
# This string is displayed in a tooltip that appears when hovering over a
# disabled 'reload' button.
reloadDisabledTooltip = Only temporarily installed add-ons can be reloaded
# LOCALIZATION NOTE (workers):
# This string is displayed as a header of the about:debugging#workers page.
workers = Workers
serviceWorkers = Service Workers
sharedWorkers = Shared Workers
otherWorkers = Other Workers
# LOCALIZATION NOTE (tabs):
# This string is displayed as a header of the about:debugging#tabs page.
tabs = Tabs
# LOCALIZATION NOTE (pageNotFound):
# This string is displayed as the main message at any error/invalid page.
pageNotFound = Page not found
# LOCALIZATION NOTE (doesNotExist):
# This string is displayed as an error message when navigating to an invalid page
# %S will be replaced by the name of the page at run-time.
doesNotExist = #%S does not exist!
# LOCALIZATION NOTE (nothing):
# This string is displayed when the list of workers is empty.
nothing = Nothing yet.
configurationIsNotCompatible = Your browser configuration is not compatible with Service Workers

View File

@ -118,6 +118,14 @@ var PerformanceView = {
* "empty", "recording", "console-recording", "recorded".
*/
setState: function (state) {
// Make sure that the focus isn't captured on a hidden iframe. This fixes a
// XUL bug where shortcuts stop working.
const iframes = window.document.querySelectorAll('iframe');
for (let iframe of iframes) {
iframe.blur();
}
window.focus();
let viewConfig = this.states[state];
if (!viewConfig) {
throw new Error(`Invalid state for PerformanceView: ${state}`);

View File

@ -296,6 +296,9 @@ pref("devtools.webconsole.autoMultiline", true);
// Enable the experimental webconsole frontend (work in progress)
pref("devtools.webconsole.new-frontend-enabled", false);
// The number of lines that are displayed in the web console.
pref("devtools.hud.loglimit", 1000);
// The number of lines that are displayed in the web console for the Net,
// CSS, JS and Web Developer categories. These defaults should be kept in sync
// with DEFAULT_LOG_LIMIT in the webconsole frontend.

View File

@ -62,9 +62,13 @@ define(function (require, exports, module) {
if (array.length > max + 1) {
items.pop();
let objectLink = this.props.objectLink || DOM.span;
items.push(Caption({
key: "more",
object: "more...",
object: objectLink({
object: this.props.object
}, "more...")
}));
}
@ -129,29 +133,22 @@ define(function (require, exports, module) {
items = this.arrayIterator(object, max);
}
let objectLink = this.props.objectLink || DOM.span;
return (
ObjectBox({
className: "array",
onClick: this.onToggleProperties},
DOM.a({
className: "objectLink",
onclick: this.onClickBracket},
DOM.span({
className: "arrayLeftBracket",
role: "presentation"},
"["
)
),
className: "array"},
objectLink({
className: "arrayLeftBracket",
role: "presentation",
object: object
}, "["),
items,
DOM.a({
className: "objectLink",
onclick: this.onClickBracket},
DOM.span({
className: "arrayRightBracket",
role: "presentation"},
"]"
)
),
objectLink({
className: "arrayRightBracket",
role: "presentation",
object: object
}, "]"),
DOM.span({
className: "arrayProperties",
role: "group"}

View File

@ -12,7 +12,7 @@ define(function (require, exports, module) {
// Reps
const { createFactories, isGrip } = require("./rep-utils");
const { ObjectLink } = createFactories(require("./object-link"));
const { ObjectBox } = createFactories(require("./object-box"));
// Shortcuts
const { span } = React.DOM;
@ -28,15 +28,21 @@ define(function (require, exports, module) {
},
getTitle: function (grip) {
return new Date(grip.preview.timestamp).toISOString();
if (this.props.objectLink) {
return this.props.objectLink({
object: grip
}, grip.class);
}
return "";
},
render: function () {
let grip = this.props.object;
return (
ObjectLink({className: "Date"},
span({className: "objectTitle"},
this.getTitle(grip)
ObjectBox({},
this.getTitle(grip),
span({className: "Date"},
new Date(grip.preview.timestamp).toISOString()
)
)
);

View File

@ -33,8 +33,15 @@ define(function (require, exports, module) {
return location ? getFileName(location) : "";
},
getTitle: function (win, context) {
return "document";
getTitle: function (grip) {
if (this.props.objectLink) {
return ObjectBox({},
this.props.objectLink({
object: grip
}, grip.class)
);
}
return "";
},
getTooltip: function (doc) {
@ -46,6 +53,7 @@ define(function (require, exports, module) {
return (
ObjectBox({className: "object"},
this.getTitle(grip),
span({className: "objectPropValue"},
this.getLocation(grip)
)

View File

@ -12,7 +12,7 @@ define(function (require, exports, module) {
// Reps
const { createFactories, isGrip } = require("./rep-utils");
const { ObjectLink } = createFactories(require("./object-link"));
const { ObjectBox } = createFactories(require("./object-box"));
/**
* Renders DOM event objects.
@ -24,8 +24,17 @@ define(function (require, exports, module) {
object: React.PropTypes.object.isRequired
},
getTitle: function (grip) {
if (this.props.objectLink) {
return this.props.objectLink({
object: grip
}, grip.preview.type);
}
return grip.preview.type;
},
summarizeEvent: function (grip) {
let info = [grip.preview.type, " "];
let info = [];
let eventFamily = grip.class;
let props = grip.preview.properties;
@ -44,7 +53,8 @@ define(function (require, exports, module) {
render: function () {
let grip = this.props.object;
return (
ObjectLink({className: "event"},
ObjectBox({className: "event"},
this.getTitle(grip),
this.summarizeEvent(grip)
)
);

View File

@ -12,7 +12,7 @@ define(function (require, exports, module) {
// Reps
const { createFactories, isGrip, cropString } = require("./rep-utils");
const { ObjectLink } = createFactories(require("./object-link"));
const { ObjectBox } = createFactories(require("./object-box"));
/**
* This component represents a template for Function objects.
@ -24,6 +24,15 @@ define(function (require, exports, module) {
object: React.PropTypes.object.isRequired
},
getTitle: function (grip) {
if (this.props.objectLink) {
return this.props.objectLink({
object: grip
}, "function");
}
return "";
},
summarizeFunction: function (grip) {
let name = grip.displayName || grip.name || "function";
return cropString(name + "()", 100);
@ -33,7 +42,8 @@ define(function (require, exports, module) {
let grip = this.props.object;
return (
ObjectLink({className: "function"},
ObjectBox({className: "function"},
this.getTitle(grip),
this.summarizeFunction(grip)
)
);

View File

@ -15,7 +15,7 @@ define(function (require, exports, module) {
const { Caption } = createFactories(require("./caption"));
// Shortcuts
const { a, span } = React.DOM;
const { span } = React.DOM;
/**
* Renders an array. The array is enclosed by left and right bracket
@ -35,7 +35,12 @@ define(function (require, exports, module) {
},
getTitle: function (object, context) {
return "[" + object.length + "]";
if (this.props.objectLink) {
return this.props.objectLink({
object: object
}, object.class);
}
return "";
},
arrayIterator: function (grip, max) {
@ -84,10 +89,13 @@ define(function (require, exports, module) {
if (array.length > max + 1) {
items.pop();
let objectLink = this.props.objectLink || span;
items.push(Caption({
key: "more",
object: "more..."}
));
object: objectLink({
object: this.props.object
}, "more...")
}));
}
return items;
@ -106,23 +114,23 @@ define(function (require, exports, module) {
items = this.arrayIterator(object, max);
}
let objectLink = this.props.objectLink || span;
return (
ObjectBox({className: "array"},
a({className: "objectLink"},
span({
className: "arrayLeftBracket",
role: "presentation"},
"["
)
),
ObjectBox({
className: "array"},
this.getTitle(object),
objectLink({
className: "arrayLeftBracket",
role: "presentation",
object: object
}, "["),
items,
a({className: "objectLink"},
span({
className: "arrayRightBracket",
role: "presentation"},
"]"
)
),
objectLink({
className: "arrayRightBracket",
role: "presentation",
object: object
}, "]"),
span({
className: "arrayProperties",
role: "group"}

View File

@ -30,8 +30,13 @@ define(function (require, exports, module) {
mode: React.PropTypes.string,
},
getTitle: function () {
return this.props.object.class || "Object";
getTitle: function (object) {
if (this.props.objectLink) {
return this.props.objectLink({
object: object
}, object.class);
}
return object.class || "Object";
},
longPropIterator: function (object) {
@ -81,9 +86,14 @@ define(function (require, exports, module) {
// one and append 'more...' postfix in such case.
if (props.length > max) {
props.pop();
let objectLink = this.props.objectLink || span;
props.push(Caption({
key: "more",
object: "more...",
object: objectLink({
object: object
}, "more...")
}));
} else if (props.length > 0) {
// Remove the last comma.
@ -146,20 +156,34 @@ define(function (require, exports, module) {
this.longPropIterator(object) :
this.shortPropIterator(object);
let objectLink = this.props.objectLink || span;
if (this.props.mode == "tiny" || !props.length) {
return (
ObjectBox({className: "object"},
span({className: "objectTitle"}, this.getTitle(object))
this.getTitle(object),
objectLink({
className: "objectLeftBrace",
role: "presentation",
object: object
}, "")
)
);
}
return (
ObjectBox({className: "object"},
span({className: "objectTitle"}, this.getTitle(object)),
span({className: "objectLeftBrace", role: "presentation"}, " {"),
this.getTitle(object),
objectLink({
className: "objectLeftBrace",
role: "presentation",
object: object
}, " {"),
props,
span({className: "objectRightBrace"}, "}")
objectLink({
className: "objectRightBrace",
role: "presentation",
object: object
}, "}")
)
);
},

View File

@ -12,7 +12,7 @@ define(function (require, exports, module) {
// Reps
const { createFactories, isGrip } = require("./rep-utils");
const { ObjectLink } = createFactories(require("./object-link"));
const { ObjectBox } = createFactories(require("./object-box"));
const { Caption } = createFactories(require("./caption"));
// Shortcuts
@ -22,6 +22,7 @@ define(function (require, exports, module) {
* Used to render a map of values provided as a grip.
*/
let NamedNodeMap = React.createClass({
displayName: "NamedNodeMap",
propTypes: {
object: React.PropTypes.object.isRequired,
@ -29,13 +30,16 @@ define(function (require, exports, module) {
provider: React.PropTypes.object,
},
className: "NamedNodeMap",
getLength: function (object) {
return object.preview.length;
},
getTitle: function (object) {
if (this.props.objectLink && object.class) {
return this.props.objectLink({
object: object
}, object.class);
}
return object.class ? object.class : "";
},
@ -46,9 +50,12 @@ define(function (require, exports, module) {
if (items.length > max + 1) {
items.pop();
let objectLink = this.props.objectLink || span;
items.push(Caption({
key: "more",
object: "more...",
object: objectLink({
object: this.props.object
}, "more...")
}));
}
@ -98,22 +105,22 @@ define(function (require, exports, module) {
items = this.getItems(grip, max);
}
let objectLink = this.props.objectLink || span;
return (
ObjectLink({className: "NamedNodeMap"},
span({className: "objectTitle"},
this.getTitle(grip)
),
span({
ObjectBox({className: "NamedNodeMap"},
this.getTitle(grip),
objectLink({
className: "arrayLeftBracket",
role: "presentation"},
"["
),
role: "presentation",
object: grip
}, "["),
items,
span({
objectLink({
className: "arrayRightBracket",
role: "presentation"},
"]"
)
role: "presentation",
object: grip
}, "]")
)
);
},

View File

@ -12,7 +12,7 @@ define(function (require, exports, module) {
// Reps
const { createFactories, isGrip } = require("./rep-utils");
const { ObjectLink } = createFactories(require("./object-link"));
const { ObjectBox } = createFactories(require("./object-box"));
// Shortcuts
const { span } = React.DOM;
@ -27,6 +27,17 @@ define(function (require, exports, module) {
object: React.PropTypes.object.isRequired,
},
getTitle: function (grip) {
if (this.props.objectLink) {
return ObjectBox({},
this.props.objectLink({
object: grip
}, this.getType(grip))
);
}
return "";
},
getType: function (grip) {
return grip.class;
},
@ -38,7 +49,8 @@ define(function (require, exports, module) {
render: function () {
let grip = this.props.object;
return (
ObjectLink({className: this.getType(grip)},
ObjectBox({className: this.getType(grip)},
this.getTitle(grip),
span({className: "objectPropValue"},
this.getDescription(grip)
)

View File

@ -12,7 +12,7 @@ define(function (require, exports, module) {
// Reps
const { createFactories, isGrip } = require("./rep-utils");
const { ObjectLink } = createFactories(require("./object-link"));
const { ObjectBox } = createFactories(require("./object-box"));
// Shortcuts
const { span } = React.DOM;
@ -27,6 +27,17 @@ define(function (require, exports, module) {
object: React.PropTypes.object.isRequired,
},
getTitle: function (grip) {
if (this.props.objectLink) {
return ObjectBox({},
this.props.objectLink({
object: grip
}, this.getType(grip))
);
}
return "";
},
getType: function (grip) {
return grip.class;
},
@ -38,7 +49,8 @@ define(function (require, exports, module) {
render: function () {
let grip = this.props.object;
return (
ObjectLink({className: this.getType(grip)},
ObjectBox({className: this.getType(grip)},
this.getTitle(grip),
span({className: "objectPropValue"},
this.getDescription(grip)
)

View File

@ -27,7 +27,12 @@ define(function (require, exports, module) {
mode: React.PropTypes.string,
},
getTitle: function () {
getTitle: function (object) {
if (this.props.objectLink) {
return this.props.objectLink({
object: object
}, object.class);
}
return "Object";
},
@ -75,9 +80,13 @@ define(function (require, exports, module) {
if (props.length > max) {
props.pop();
let objectLink = this.props.objectLink || span;
props.push(Caption({
key: "more",
object: "more...",
object: objectLink({
object: object
}, "more...")
}));
} else if (props.length > 0) {
// Remove the last comma.
@ -133,21 +142,30 @@ define(function (require, exports, module) {
render: function () {
let object = this.props.object;
let props = this.shortPropIterator(object);
let objectLink = this.props.objectLink || span;
if (this.props.mode == "tiny" || !props.length) {
return (
ObjectBox({className: "object"},
span({className: "objectTitle"}, this.getTitle())
objectLink({className: "objectTitle"}, this.getTitle())
)
);
}
return (
ObjectBox({className: "object"},
span({className: "objectTitle"}, this.getTitle()),
span({className: "objectLeftBrace", role: "presentation"}, "{"),
this.getTitle(object),
objectLink({
className: "objectLeftBrace",
role: "presentation",
object: object
}, "{"),
props,
span({className: "objectRightBrace"}, "}")
objectLink({
className: "objectRightBrace",
role: "presentation",
object: object
}, "}")
)
);
},
@ -155,6 +173,7 @@ define(function (require, exports, module) {
function supportsObject(object, type) {
return true;
}
// Exports from this module
exports.Obj = {
rep: Obj,

View File

@ -12,7 +12,7 @@ define(function (require, exports, module) {
// Reps
const { createFactories, isGrip } = require("./rep-utils");
const { ObjectLink } = createFactories(require("./object-link"));
const { ObjectBox } = createFactories(require("./object-box"));
// Shortcuts
const { span } = React.DOM;
@ -27,25 +27,20 @@ define(function (require, exports, module) {
object: React.PropTypes.object.isRequired,
},
getTitle: function (grip) {
return grip.class;
},
getSource: function (grip) {
return grip.displayString;
},
render: function () {
let grip = this.props.object;
let objectLink = this.props.objectLink || span;
return (
ObjectLink({className: "regexp"},
span({className: "objectTitle"},
this.getTitle(grip)
),
span(" "),
span({className: "regexpSource"},
this.getSource(grip)
)
ObjectBox({className: "regexp"},
objectLink({
object: grip,
className: "regexpSource"
}, this.getSource(grip))
)
);
},

View File

@ -30,9 +30,11 @@ define(function (require, exports, module) {
);
}
let croppedString = this.props.cropLimit ?
cropMultipleLines(text, this.props.cropLimit) : cropMultipleLines(text);
return (
ObjectBox({className: "string"},
"\"" + cropMultipleLines(text) + "\""
ObjectBox({className: "string"}, "\"" + croppedString + "\""
)
);
},

View File

@ -28,6 +28,18 @@ define(function (require, exports, module) {
object: React.PropTypes.object.isRequired,
},
getTitle: function (grip) {
let title = "StyleSheet ";
if (this.props.objectLink) {
return ObjectBox({},
this.props.objectLink({
object: grip
}, title)
);
}
return title;
},
getLocation: function (grip) {
// Embedded stylesheets don't have URL and so, no preview.
let url = grip.preview ? grip.preview.url : "";
@ -39,7 +51,7 @@ define(function (require, exports, module) {
return (
ObjectBox({className: "object"},
"StyleSheet ",
this.getTitle(grip),
DOM.span({className: "objectPropValue"},
this.getLocation(grip)
)

View File

@ -11,8 +11,9 @@ define(function (require, exports, module) {
const React = require("devtools/client/shared/vendor/react");
// Reps
const { createFactories, isGrip, cropMultipleLines } = require("./rep-utils");
const { ObjectLink } = createFactories(require("./object-link"));
const { createFactories, isGrip } = require("./rep-utils");
const { ObjectBox } = createFactories(require("./object-box"));
const { cropMultipleLines } = require("./string");
// Shortcuts
const DOM = React.DOM;
@ -32,8 +33,13 @@ define(function (require, exports, module) {
return cropMultipleLines(grip.preview.textContent);
},
getTitle: function (win, context) {
return "textNode";
getTitle: function (grip) {
if (this.props.objectLink) {
return this.props.objectLink({
object: grip
}, "#text");
}
return "";
},
render: function () {
@ -42,22 +48,29 @@ define(function (require, exports, module) {
if (mode == "short" || mode == "tiny") {
return (
ObjectLink({className: "textNode"},
ObjectBox({className: "textNode"},
this.getTitle(grip),
"\"" + this.getTextContent(grip) + "\""
)
);
}
let objectLink = this.props.objectLink || DOM.span;
return (
ObjectLink({className: "textNode"},
"<",
ObjectBox({className: "textNode"},
this.getTitle(grip),
objectLink({
object: grip
}, "<"),
DOM.span({className: "nodeTag"}, "TextNode"),
" textContent=\"",
DOM.span({className: "nodeValue"},
this.getTextContent(grip)
),
"\"",
">;"
objectLink({
object: grip
}, ">;")
)
);
},

View File

@ -27,6 +27,17 @@ define(function (require, exports, module) {
object: React.PropTypes.object.isRequired,
},
getTitle: function (grip) {
if (this.props.objectLink) {
return ObjectBox({},
this.props.objectLink({
object: grip
}, grip.class)
);
}
return "";
},
getLocation: function (grip) {
return cropString(grip.preview.url);
},
@ -36,6 +47,7 @@ define(function (require, exports, module) {
return (
ObjectBox({className: "Window"},
this.getTitle(grip),
DOM.span({className: "objectPropValue"},
this.getLocation(grip)
)

View File

@ -7,6 +7,7 @@ support-files =
[test_notification_box_01.html]
[test_notification_box_02.html]
[test_notification_box_03.html]
[test_reps_array.html]
[test_reps_attribute.html]
[test_reps_date-time.html]
[test_reps_function.html]

View File

@ -0,0 +1,185 @@
<!DOCTYPE HTML>
<html>
<!--
Test ArrayRep rep
-->
<head>
<meta charset="utf-8">
<title>Rep test - ArrayRep</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<pre id="test">
<script src="head.js" type="application/javascript;version=1.8"></script>
<script type="application/javascript;version=1.8">
window.onload = Task.async(function* () {
let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
let { ArrayRep } = browserRequire("devtools/client/shared/components/reps/array");
let componentUnderTest = ArrayRep;
try {
yield testBasic();
// Test property iterator
yield testMaxProps();
yield testMoreThanMaxProps();
yield testRecursiveArray();
// Test that properties are rendered as expected by ItemRep
yield testNested();
} catch(e) {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
} finally {
SimpleTest.finish();
}
function testBasic() {
// Test that correct rep is chosen
const stub = [];
const renderedRep = shallowRenderComponent(Rep, { object: stub });
is(renderedRep.type, ArrayRep.rep, `Rep correctly selects ${ArrayRep.rep.displayName}`);
// Test rendering
const defaultOutput = `[]`;
const modeTests = [
{
mode: undefined,
expectedOutput: defaultOutput,
},
{
mode: "tiny",
expectedOutput: `[0]`,
},
{
mode: "short",
expectedOutput: defaultOutput,
},
{
mode: "long",
expectedOutput: defaultOutput,
}
];
testRepRenderModes(modeTests, "testBasic", componentUnderTest, stub);
}
function testMaxProps() {
const stub = [1, "foo", {}];
const defaultOutput = `[1, "foo", Object]`;
const modeTests = [
{
mode: undefined,
expectedOutput: defaultOutput,
},
{
mode: "tiny",
expectedOutput: `[3]`,
},
{
mode: "short",
expectedOutput: defaultOutput,
},
{
mode: "long",
expectedOutput: defaultOutput,
}
];
testRepRenderModes(modeTests, "testMaxProps", componentUnderTest, stub);
}
function testMoreThanMaxProps() {
const stub = Array(302).fill("foo");
const defaultOutput = `["foo", "foo", "foo", more...]`;
const modeTests = [
{
mode: undefined,
expectedOutput: defaultOutput,
},
{
mode: "tiny",
expectedOutput: `[302]`,
},
{
mode: "short",
expectedOutput: defaultOutput,
},
{
mode: "long",
expectedOutput: `[${Array(300).fill("\"foo\"").join(", ")}, more...]`,
}
];
testRepRenderModes(modeTests, "testMoreThanMaxProps", componentUnderTest, stub);
}
function testRecursiveArray() {
let stub = [1];
stub.push(stub);
const defaultOutput = `[1, [...]]`;
const modeTests = [
{
mode: undefined,
expectedOutput: defaultOutput,
},
{
mode: "tiny",
expectedOutput: `[2]`,
},
{
mode: "short",
expectedOutput: defaultOutput,
},
{
mode: "long",
expectedOutput: defaultOutput,
}
];
testRepRenderModes(modeTests, "testRecursiveArray", componentUnderTest, stub);
}
function testNested() {
let stub = [
{
p1: "s1",
p2: ["a1", "a2", "a3"],
p3: "s3",
p4: "s4"
}
];
const defaultOutput = `[Object{p1: "s1", p3: "s3", p4: "s4", more...}]`;
const modeTests = [
{
mode: undefined,
expectedOutput: defaultOutput,
},
{
mode: "tiny",
expectedOutput: `[1]`,
},
{
mode: "short",
expectedOutput: defaultOutput,
},
{
mode: "long",
expectedOutput: defaultOutput,
}
];
testRepRenderModes(modeTests, "testNested", componentUnderTest, stub);
}
});
</script>
</pre>
</body>
</html>

View File

@ -41,7 +41,7 @@ window.onload = Task.async(function* () {
// Test rendering
const renderedComponent = renderComponent(ObjectWithURL.rep, { object: gripStub });
ok(renderedComponent.className.includes("objectLink-Location"), "ObjectWithURL rep has expected class name");
ok(renderedComponent.className.includes("objectBox-Location"), "ObjectWithURL rep has expected class name");
const innerNode = renderedComponent.querySelector(".objectPropValue");
is(innerNode.textContent, "https://www.mozilla.org/en-US/", "ObjectWithURL rep has expected inner HTML structure and text content");

View File

@ -26,6 +26,7 @@ window.onload = Task.async(function* () {
// Test rendering
yield testMultiline();
yield testMultilineOpen();
yield testMultilineLimit();
} catch(e) {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
} finally {
@ -37,6 +38,11 @@ window.onload = Task.async(function* () {
is(renderedComponent.textContent, "\"aaaaaaaaaaaaaaaaaaaaa\\nbbb…bbbbbb\\ncccccccccccccccc\\n\"", "String rep has expected text content for multiline string");
}
function testMultilineLimit() {
const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testMultiline"), cropLimit: 20 });
is(renderedComponent.textContent, "\"aaaaaaaaaa…cccccccc\\n\"", "String rep has expected text content for multiline string with specified number of characters");
}
function testMultilineOpen() {
const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testMultiline"), member: {open: true} });
is(renderedComponent.textContent, "\"aaaaaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbb\ncccccccccccccccc\n\"", "String rep has expected text content for multiline string when open");

View File

@ -247,3 +247,7 @@
.theme-firebug #command-button-frames {
min-width: 32px;
}
.theme-firebug #element-picker {
min-height: 21px;
}

View File

@ -16,19 +16,20 @@ const {openVariablesView} = require("devtools/client/webconsole/new-console-outp
VariablesViewLink.displayName = "VariablesViewLink";
VariablesViewLink.propTypes = {
objectActor: PropTypes.object.required,
label: PropTypes.string.label,
object: PropTypes.object.required
};
function VariablesViewLink(props) {
const { objectActor, label } = props;
const { object, children } = props;
return dom.a({
onClick: openVariablesView.bind(null, objectActor),
className: "cm-variable",
draggable: false,
href: "#"
}, label);
return (
dom.a({
onClick: openVariablesView.bind(null, object),
className: "cm-variable",
draggable: false,
href: "#"
}, children)
);
}
module.exports.VariablesViewLink = VariablesViewLink;

View File

@ -6,7 +6,9 @@
"use strict";
const { messages } = require("./messages");
const { prefs } = require("./prefs");
exports.reducers = {
messages
messages,
prefs,
};

View File

@ -6,4 +6,5 @@
DevToolsModules(
'index.js',
'messages.js',
'prefs.js',
)

View File

@ -0,0 +1,12 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript 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";
function prefs(state = {}, action) {
return state;
}
exports.prefs = prefs;

View File

@ -5,8 +5,18 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { getLogLimit } = require("devtools/client/webconsole/new-console-output/selectors/prefs");
function getAllMessages(state) {
return state.messages;
let messages = state.messages;
let messageCount = messages.count();
let logLimit = getLogLimit(state);
if (messageCount > logLimit) {
return messages.splice(0, messageCount - logLimit);
}
return messages;
}
exports.getAllMessages = getAllMessages;

View File

@ -5,4 +5,5 @@
DevToolsModules(
'messages.js',
'prefs.js',
)

View File

@ -0,0 +1,12 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript 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";
function getLogLimit(state) {
return state.prefs.logLimit;
}
exports.getLogLimit = getLogLimit;

View File

@ -4,9 +4,18 @@
"use strict";
const { combineReducers, createStore } = require("devtools/client/shared/vendor/redux");
const Immutable = require("devtools/client/shared/vendor/immutable");
const { reducers } = require("./reducers/index");
const Services = require("Services");
function storeFactory() {
const initialState = {
messages: Immutable.List(),
prefs: {
logLimit: Math.max(Services.prefs.getIntPref("devtools.hud.loglimit"), 1)
}
};
function storeFactory(initialState = {}) {
return createStore(combineReducers(reducers), initialState);
}

View File

@ -34,7 +34,7 @@ window.onload = Task.async(function* () {
};
const rendered = renderComponent(EvaluationResult, props);
const queryPathBase = "div.message.cm-s-mozilla span.message-body-wrapper.message-body.devtools-monospace span .objectLink";
const queryPathBase = "div.message.cm-s-mozilla span.message-body-wrapper.message-body.devtools-monospace span .objectBox";
const preview = rendered.querySelectorAll(queryPathBase);
is(preview[0].textContent, testCommand.expectedText, "EvaluationResult outputs expected text");

View File

@ -7,6 +7,7 @@
var { utils: Cu } = Components;
var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
const Services = require("Services");
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
DevToolsUtils.testing = true;

View File

@ -82,3 +82,43 @@ add_task(function*() {
deepEqual(messages.toArray(), [prepareMessage(clearPacket)],
"console.clear clears existing messages and add a new one");
});
/**
* Test message limit on the store.
*/
add_task(function* () {
const { getState, dispatch } = storeFactory();
const logLimit = 1000;
const messageNumber = logLimit + 1;
let newPacket = Object.assign({}, packet);
for (let i = 1; i <= messageNumber; i++) {
newPacket.message.arguments = [i];
dispatch(actions.messageAdd(newPacket));
}
let messages = getAllMessages(getState());
equal(messages.count(), logLimit, "Messages are pruned up to the log limit");
deepEqual(messages.last().data.arguments, [messageNumber],
"The last message is the expected one");
});
/**
* Test message limit on the store with user set prefs.
*/
add_task(function* () {
const userSetLimit = 10;
Services.prefs.setIntPref("devtools.hud.loglimit", userSetLimit);
const { getState, dispatch } = storeFactory();
let newPacket = Object.assign({}, packet);
for (let i = 1; i <= userSetLimit + 1; i++) {
newPacket.message.arguments = [i];
dispatch(actions.messageAdd(newPacket));
}
let messages = getAllMessages(getState());
equal(messages.count(), userSetLimit,
"Messages are pruned up to the user set log limit");
});

View File

@ -12,6 +12,6 @@
*
* Once JSTerm is also written in React/Redux, these will be actions.
*/
exports.openVariablesView = (objectActor) => {
window.jsterm.openVariablesView({objectActor});
exports.openVariablesView = (object) => {
window.jsterm.openVariablesView({objectActor: object});
};

View File

@ -72,6 +72,14 @@ ComputedTimingFunction::GetValue(
return aPortion;
}
// Ensure that we return 0 or 1 on both edges.
if (aPortion == 0.0) {
return 0.0;
}
if (aPortion == 1.0) {
return 1.0;
}
// For negative values, try to extrapolate with tangent (p1 - p0) or,
// if p1 is coincident with p0, with (p2 - p0).
if (aPortion < 0.0) {

View File

@ -713,6 +713,7 @@ KeyframeEffectReadOnly::ComposeStyle(RefPtr<AnimValuesStyleRule>& aStyleRule,
positionInSegment,
computedTiming.mBeforeFlag);
MOZ_ASSERT(IsFinite(valuePosition), "Position value should be finite");
StyleAnimationValue val;
if (StyleAnimationValue::Interpolate(prop.mProperty,
segment->mFromValue,

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script>
function boom()
{
document.body.animate([],
{ duration: 6,
easing: "cubic-bezier(0, -1e+39, 0, 0)" });
document.body.animate([],
{ duration: 6,
easing: "cubic-bezier(0, 1e+39, 0, 0)" });
document.body.animate([],
{ duration: 6,
easing: "cubic-bezier(0, 0, 0, -1e+39)" });
document.body.animate([],
{ duration: 6,
easing: "cubic-bezier(0, 0, 0, 1e+39)" });
}
</script>
</head>
<body onload="boom();"></body>
</html>

View File

@ -6,4 +6,5 @@ pref(dom.animations-api.core.enabled,true) load 1216842-3.html
pref(dom.animations-api.core.enabled,true) load 1216842-4.html
pref(dom.animations-api.core.enabled,true) load 1216842-5.html
pref(dom.animations-api.core.enabled,true) load 1216842-6.html
pref(dom.animations-api.core.enabled,true) load 1278485-1.html
pref(dom.animations-api.core.enabled,true) load 1277272-1.html

View File

@ -35,6 +35,7 @@ support-files =
css-transitions/file_keyframeeffect-getkeyframes.html
css-transitions/file_pseudoElement-get-animations.html
document-timeline/file_document-timeline.html
mozilla/file_cubic_bezier_limits.html
mozilla/file_deferred_start.html
mozilla/file_disabled_properties.html
mozilla/file_document-timeline-origin-time-range.html
@ -80,6 +81,7 @@ skip-if = buildapp == 'mulet'
[document-timeline/test_document-timeline.html]
[document-timeline/test_request_animation_frame.html]
skip-if = buildapp == 'mulet'
[mozilla/test_cubic_bezier_limits.html]
[mozilla/test_deferred_start.html]
skip-if = (toolkit == 'gonk' && debug)
[mozilla/test_disabled_properties.html]

View File

@ -0,0 +1,167 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<body>
<style>
@keyframes anim {
to { margin-left: 100px; }
}
.transition-div {
margin-left: 100px;
}
</style>
<script>
'use strict';
// We clamp +infinity or -inifinity value in floating point to
// maximum floating point value or -maxinum floating point value.
const max_float = 3.40282e+38;
test(function(t) {
var div = addDiv(t);
var anim = div.animate({ }, 100 * MS_PER_SEC);
anim.effect.timing.easing = 'cubic-bezier(0, 1e+39, 0, 0)';
assert_equals(anim.effect.timing.easing,
'cubic-bezier(0, ' + max_float + ', 0, 0)',
'y1 control point for effect easing is out of upper boundary');
anim.effect.timing.easing = 'cubic-bezier(0, 0, 0, 1e+39)';
assert_equals(anim.effect.timing.easing,
'cubic-bezier(0, 0, 0, ' + max_float + ')',
'y2 control point for effect easing is out of upper boundary');
anim.effect.timing.easing = 'cubic-bezier(0, -1e+39, 0, 0)';
assert_equals(anim.effect.timing.easing,
'cubic-bezier(0, ' + -max_float + ', 0, 0)',
'y1 control point for effect easing is out of lower boundary');
anim.effect.timing.easing = 'cubic-bezier(0, 0, 0, -1e+39)';
assert_equals(anim.effect.timing.easing,
'cubic-bezier(0, 0, 0, ' + -max_float + ')',
'y2 control point for effect easing is out of lower boundary');
}, 'Clamp y1 and y2 control point out of boundaries for effect easing' );
test(function(t) {
var div = addDiv(t);
var anim = div.animate({ }, 100 * MS_PER_SEC);
anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 1e+39, 0, 0)' }]);
assert_equals(anim.effect.getKeyframes()[0].easing,
'cubic-bezier(0, ' + max_float + ', 0, 0)',
'y1 control point for keyframe easing is out of upper boundary');
anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, 1e+39)' }]);
assert_equals(anim.effect.getKeyframes()[0].easing,
'cubic-bezier(0, 0, 0, ' + max_float + ')',
'y2 control point for keyframe easing is out of upper boundary');
anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, -1e+39, 0, 0)' }]);
assert_equals(anim.effect.getKeyframes()[0].easing,
'cubic-bezier(0, ' + -max_float + ', 0, 0)',
'y1 control point for keyframe easing is out of lower boundary');
anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, -1e+39)' }]);
assert_equals(anim.effect.getKeyframes()[0].easing,
'cubic-bezier(0, 0, 0, ' + -max_float + ')',
'y2 control point for keyframe easing is out of lower boundary');
}, 'Clamp y1 and y2 control point out of boundaries for keyframe easing' );
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim 100s cubic-bezier(0, 1e+39, 0, 0)';
assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
'cubic-bezier(0, ' + max_float + ', 0, 0)',
'y1 control point for CSS animation is out of upper boundary');
div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, 1e+39)';
assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
'cubic-bezier(0, 0, 0, ' + max_float + ')',
'y2 control point for CSS animation is out of upper boundary');
div.style.animation = 'anim 100s cubic-bezier(0, -1e+39, 0, 0)';
assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
'cubic-bezier(0, ' + -max_float + ', 0, 0)',
'y1 control point for CSS animation is out of lower boundary');
div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, -1e+39)';
assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
'cubic-bezier(0, 0, 0, ' + -max_float + ')',
'y2 control point for CSS animation is out of lower boundary');
}, 'Clamp y1 and y2 control point out of boundaries for CSS animation' );
test(function(t) {
var div = addDiv(t, {'class': 'transition-div'});
div.style.transition = 'margin-left 100s cubic-bezier(0, 1e+39, 0, 0)';
flushComputedStyle(div);
div.style.marginLeft = '0px';
assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
'cubic-bezier(0, ' + max_float + ', 0, 0)',
'y1 control point for CSS transition on upper boundary');
div.style.transition = '';
div.style.marginLeft = '';
div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, 1e+39)';
flushComputedStyle(div);
div.style.marginLeft = '0px';
assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
'cubic-bezier(0, 0, 0, ' + max_float + ')',
'y2 control point for CSS transition on upper boundary');
div.style.transition = '';
div.style.marginLeft = '';
div.style.transition = 'margin-left 100s cubic-bezier(0, -1e+39, 0, 0)';
flushComputedStyle(div);
div.style.marginLeft = '0px';
assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
'cubic-bezier(0, ' + -max_float + ', 0, 0)',
'y1 control point for CSS transition on lower boundary');
div.style.transition = '';
div.style.marginLeft = '';
div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, -1e+39)';
flushComputedStyle(div);
div.style.marginLeft = '0px';
assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
'cubic-bezier(0, 0, 0, ' + -max_float + ')',
'y2 control point for CSS transition on lower boundary');
}, 'Clamp y1 and y2 control point out of boundaries for CSS transition' );
test(function(t) {
var div = addDiv(t);
var anim = div.animate({ }, { duration: 100 * MS_PER_SEC, fill: 'forwards' });
anim.pause();
// The positive steepest function on both edges.
anim.effect.timing.easing = 'cubic-bezier(0, 1e+39, 0, 1e+39)';
assert_equals(anim.effect.getComputedTiming().progress, 0.0,
'progress on lower edge for the highest value of y1 and y2 control points');
anim.finish();
assert_equals(anim.effect.getComputedTiming().progress, 1.0,
'progress on upper edge for the highest value of y1 and y2 control points');
// The negative steepest function on both edges.
anim.effect.timing.easing = 'cubic-bezier(0, -1e+39, 0, -1e+39)';
anim.currentTime = 0;
assert_equals(anim.effect.getComputedTiming().progress, 0.0,
'progress on lower edge for the lowest value of y1 and y2 control points');
anim.finish();
assert_equals(anim.effect.getComputedTiming().progress, 1.0,
'progress on lower edge for the lowest value of y1 and y2 control points');
}, 'Calculated values on both edges');
done();
</script>
</body>

View File

@ -0,0 +1,14 @@
<!doctype html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
'use strict';
setup({explicit_done: true});
SpecialPowers.pushPrefEnv(
{ "set": [["dom.animations-api.core.enabled", true]]},
function() {
window.open("file_cubic_bezier_limits.html");
});
</script>

View File

@ -947,8 +947,8 @@ GLContextGLX::MakeCurrentImpl(bool aForce)
// Many GLX implementations default to blocking until the next
// VBlank when calling glXSwapBuffers. We want to run unthrottled
// in ASAP mode. See bug 1280744.
int interval = gfxPlatform::IsInLayoutAsapMode() ? 0 : 1;
mGLX->xSwapInterval(mDisplay, mDrawable, interval);
const bool isASAP = (gfxPrefs::LayoutFrameRate() == 0);
mGLX->xSwapInterval(mDisplay, mDrawable, isASAP ? 0 : 1);
}
}

View File

@ -538,9 +538,9 @@ AccessibleCaretManager::SelectWordOrShortcut(const nsPoint& aPoint)
}
// Find the frame under point.
nsIFrame* ptFrame = nsLayoutUtils::GetFrameForPoint(rootFrame, aPoint,
nsWeakFrame ptFrame = nsLayoutUtils::GetFrameForPoint(rootFrame, aPoint,
nsLayoutUtils::IGNORE_PAINT_SUPPRESSION | nsLayoutUtils::IGNORE_CROSS_DOC);
if (!ptFrame) {
if (!ptFrame.IsAlive()) {
return NS_ERROR_FAILURE;
}
@ -553,6 +553,14 @@ AccessibleCaretManager::SelectWordOrShortcut(const nsPoint& aPoint)
focusableFrame ? focusableFrame->ListTag().get() : "no frame");
#endif
// Get ptInFrame here so that we don't need to check whether rootFrame is
// alive later. Note that if ptFrame is being moved by
// IMEStateManager::NotifyIME() or ChangeFocusToOrClearOldFocus() below,
// something under the original point will be selected, which may not be the
// original text the user wants to select.
nsPoint ptInFrame = aPoint;
nsLayoutUtils::TransformPoint(rootFrame, ptFrame, ptInFrame);
// Firstly check long press on an empty editable content.
Element* newFocusEditingHost = GetEditingHostForFrame(ptFrame);
if (focusableFrame && newFocusEditingHost &&
@ -585,9 +593,17 @@ AccessibleCaretManager::SelectWordOrShortcut(const nsPoint& aPoint)
// is any) before changing the focus.
IMEStateManager::NotifyIME(widget::REQUEST_TO_COMMIT_COMPOSITION,
mPresShell->GetPresContext());
if (!ptFrame.IsAlive()) {
// Cannot continue because ptFrame died.
return NS_ERROR_FAILURE;
}
// ptFrame is selectable. Now change the focus.
ChangeFocusToOrClearOldFocus(focusableFrame);
if (!ptFrame.IsAlive()) {
// Cannot continue because ptFrame died.
return NS_ERROR_FAILURE;
}
if (GetCaretMode() == CaretMode::Selection &&
!mFirstCaret->IsLogicallyVisible() && !mSecondCaret->IsLogicallyVisible()) {
@ -600,9 +616,6 @@ AccessibleCaretManager::SelectWordOrShortcut(const nsPoint& aPoint)
}
// Then try select a word under point.
nsPoint ptInFrame = aPoint;
nsLayoutUtils::TransformPoint(rootFrame, ptFrame, ptInFrame);
nsresult rv = SelectWord(ptFrame, ptInFrame);
UpdateCaretsWithHapticFeedback();

View File

@ -13,8 +13,10 @@
#include "mozilla/TypedEnumBits.h"
#include <algorithm> // for std::stable_sort
#include <limits> // for std::numeric_limits
#include "nsCSSParser.h"
#include "nsAlgorithm.h"
#include "nsCSSProps.h"
#include "nsCSSKeywords.h"
#include "nsCSSScanner.h"
@ -1067,7 +1069,7 @@ protected:
bool ParseTransitionTimingFunctionValues(nsCSSValue& aValue);
bool ParseTransitionTimingFunctionValueComponent(float& aComponent,
char aStop,
bool aCheckRange);
bool aIsXPoint);
bool ParseTransitionStepTimingFunctionValues(nsCSSValue& aValue);
enum ParseAnimationOrTransitionShorthandResult {
eParseAnimationOrTransitionShorthand_Values,
@ -16381,7 +16383,7 @@ CSSParserImpl::ParseTransitionTimingFunctionValues(nsCSSValue& aValue)
bool
CSSParserImpl::ParseTransitionTimingFunctionValueComponent(float& aComponent,
char aStop,
bool aCheckRange)
bool aIsXPoint)
{
if (!GetToken(true)) {
return false;
@ -16389,7 +16391,14 @@ CSSParserImpl::ParseTransitionTimingFunctionValueComponent(float& aComponent,
nsCSSToken* tk = &mToken;
if (tk->mType == eCSSToken_Number) {
float num = tk->mNumber;
if (aCheckRange && (num < 0.0 || num > 1.0)) {
// Clamp infinity or -infinity values to max float or -max float to avoid
// calculations with infinity.
num = mozilla::clamped(num, -std::numeric_limits<float>::max(),
std::numeric_limits<float>::max());
// X control point should be inside [0, 1] range.
if (aIsXPoint && (num < 0.0 || num > 1.0)) {
return false;
}
aComponent = num;

View File

@ -234,6 +234,12 @@ public class BrowserContract {
public static final int FIXED_SCREENSHOT_FOLDER_ID = -4;
public static final int FAKE_READINGLIST_SMARTFOLDER_ID = -5;
/**
* This ID and the following negative IDs are reserved for bookmarks from Android's partner
* bookmark provider.
*/
public static final long FAKE_PARTNER_BOOKMARKS_START = -1000;
public static final String MOBILE_FOLDER_GUID = "mobile";
public static final String PLACES_FOLDER_GUID = "places";
public static final String MENU_FOLDER_GUID = "menu";

View File

@ -0,0 +1,71 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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/. */
package org.mozilla.gecko.distribution;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import org.mozilla.gecko.db.BrowserContract;
/**
* Client for reading Android's PartnerBookmarksProvider.
*
* Note: This client is only invoked for distributions. Without a distribution the content provider
* will not be read and no bookmarks will be added to the UI.
*/
public class PartnerBookmarksProviderClient {
/**
* The contract between the partner bookmarks provider and applications. Contains the definition
* for the supported URIs and columns.
*/
private static class PartnerContract {
public static final Uri CONTENT_URI = Uri.parse("content://com.android.partnerbookmarks/bookmarks");
public static final int TYPE_BOOKMARK = 1;
public static final int TYPE_FOLDER = 2;
public static final int PARENT_ROOT_ID = 0;
public static final String ID = "_id";
public static final String TYPE = "type";
public static final String URL = "url";
public static final String TITLE = "title";
public static final String FAVICON = "favicon";
public static final String TOUCHICON = "touchicon";
public static final String PARENT = "parent";
}
public static Cursor getBookmarksInFolder(ContentResolver contentResolver, int folderId) {
// Use root folder id or transform negative id into actual (positive) folder id.
final long actualFolderId = folderId == BrowserContract.Bookmarks.FIXED_ROOT_ID
? PartnerContract.PARENT_ROOT_ID
: BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START - folderId;
return contentResolver.query(
PartnerContract.CONTENT_URI,
new String[] {
// Transform ids into negative values starting with FAKE_PARTNER_BOOKMARKS_START.
"(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.ID + ") as " + BrowserContract.Bookmarks._ID,
PartnerContract.TITLE + " as " + BrowserContract.Bookmarks.TITLE,
PartnerContract.URL + " as " + BrowserContract.Bookmarks.URL,
// Transform parent ids to negative ids as well
"(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.PARENT + ") as " + BrowserContract.Bookmarks.PARENT,
// Convert types (we use 0-1 and the partner provider 1-2)
"(2 - " + PartnerContract.TYPE + ") as " + BrowserContract.Bookmarks.TYPE,
// Use the ID of the entry as GUID
PartnerContract.ID + " as " + BrowserContract.Bookmarks.GUID
},
PartnerContract.PARENT + " = ?"
// Only select entries with valid type
+ " AND (" + BrowserContract.Bookmarks.TYPE + " = 1 OR " + BrowserContract.Bookmarks.TYPE + " = 2)"
// Only select entries with non empty title
+ " AND " + BrowserContract.Bookmarks.TITLE + " <> ''",
new String[] { String.valueOf(actualFolderId) },
// Same order we use in our content provider (without position)
BrowserContract.Bookmarks.TYPE + " ASC, " + BrowserContract.Bookmarks._ID + " ASC");
}
}

View File

@ -10,19 +10,25 @@ import java.util.LinkedList;
import java.util.List;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.distribution.PartnerBookmarksProviderClient;
import org.mozilla.gecko.home.BookmarksListAdapter.FolderInfo;
import org.mozilla.gecko.home.BookmarksListAdapter.OnRefreshFolderListener;
import org.mozilla.gecko.home.BookmarksListAdapter.RefreshType;
import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.preferences.GeckoPreferences;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Configuration;
import android.database.Cursor;
import android.database.MergeCursor;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
@ -220,7 +226,32 @@ public class BookmarksPanel extends HomeFragment {
@Override
public Cursor loadCursor() {
return mDB.getBookmarksInFolder(getContext().getContentResolver(), mFolderInfo.id);
final boolean isRootFolder = mFolderInfo.id == BrowserContract.Bookmarks.FIXED_ROOT_ID;
final ContentResolver contentResolver = getContext().getContentResolver();
Cursor partnerCursor = null;
Cursor userCursor = null;
if (GeckoSharedPrefs.forProfile(getContext()).getBoolean(GeckoPreferences.PREFS_READ_PARTNER_BOOKMARKS_PROVIDER, false)
&& (isRootFolder || mFolderInfo.id <= Bookmarks.FAKE_PARTNER_BOOKMARKS_START)) {
partnerCursor = PartnerBookmarksProviderClient.getBookmarksInFolder(contentResolver, mFolderInfo.id);
}
if (isRootFolder || mFolderInfo.id > Bookmarks.FAKE_PARTNER_BOOKMARKS_START) {
userCursor = mDB.getBookmarksInFolder(contentResolver, mFolderInfo.id);
}
if (partnerCursor == null && userCursor == null) {
return null;
} else if (partnerCursor == null) {
return userCursor;
} else if (userCursor == null) {
return partnerCursor;
} else {
return new MergeCursor(new Cursor[] { partnerCursor, userCursor });
}
}
@Override

View File

@ -163,6 +163,7 @@ OnSharedPreferenceChangeListener
public static final String PREFS_NOTIFICATIONS_WHATS_NEW = NON_PREF_PREFIX + "notifications.whats_new";
public static final String PREFS_APP_UPDATE_LAST_BUILD_ID = "app.update.last_build_id";
public static final String PREFS_READ_PARTNER_CUSTOMIZATIONS_PROVIDER = NON_PREF_PREFIX + "distribution.read_partner_customizations_provider";
public static final String PREFS_READ_PARTNER_BOOKMARKS_PROVIDER = NON_PREF_PREFIX + "distribution.read_partner_bookmarks_provider";
private static final String ACTION_STUMBLER_UPLOAD_PREF = "STUMBLER_PREF";

View File

@ -268,6 +268,7 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'DevToolsAuthHelper.java',
'distribution/Distribution.java',
'distribution/DistributionStoreCallback.java',
'distribution/PartnerBookmarksProviderClient.java',
'distribution/PartnerBrowserCustomizationsClient.java',
'distribution/ReferrerDescriptor.java',
'distribution/ReferrerReceiver.java',

View File

@ -145,7 +145,7 @@ this.makeIdentityConfig = function(overrides) {
kA: 'kA',
kB: 'kB',
sessionToken: 'sessionToken',
uid: 'user_uid',
uid: "a".repeat(32),
verified: true,
},
token: {

View File

@ -4,7 +4,7 @@
"use strict";
this.EXPORTED_SYMBOLS = ["BrowserIDManager"];
this.EXPORTED_SYMBOLS = ["BrowserIDManager", "AuthenticationError"];
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
@ -65,8 +65,9 @@ function deriveKeyBundle(kB) {
some other error object (which should do the right thing when toString() is
called on it)
*/
function AuthenticationError(details) {
function AuthenticationError(details, source) {
this.details = details;
this.source = source;
}
AuthenticationError.prototype = {
@ -112,6 +113,14 @@ this.BrowserIDManager.prototype = {
}
},
// Get the FxA UID. Throws if there is no signed in user
userUID() {
if (!this._signedInUser) {
throw new Error("userUID(): No signed in user");
}
return this._signedInUser.uid;
},
initialize: function() {
for (let topic of OBSERVER_TOPICS) {
Services.obs.addObserver(this, topic, false);
@ -625,13 +634,13 @@ this.BrowserIDManager.prototype = {
// both tokenserverclient and hawkclient.
// A tokenserver error thrown based on a bad response.
if (err.response && err.response.status === 401) {
err = new AuthenticationError(err);
err = new AuthenticationError(err, "tokenserver");
// A hawkclient error.
} else if (err.code && err.code === 401) {
err = new AuthenticationError(err);
err = new AuthenticationError(err, "hawkclient");
// An FxAccounts.jsm error.
} else if (err.message == fxAccountsCommon.ERROR_AUTH_ERROR) {
err = new AuthenticationError(err);
err = new AuthenticationError(err, "fxaccounts");
}
// TODO: write tests to make sure that different auth error cases are handled here

View File

@ -1453,6 +1453,8 @@ SyncEngine.prototype = {
this._log.trace("Preparing " + modifiedIDs.length +
" outgoing records");
let counts = { sent: modifiedIDs.length, failed: 0 };
// collection we'll upload
let up = new Collection(this.engineURL, null, this.service);
let handleResponse = resp => {
@ -1468,6 +1470,7 @@ SyncEngine.prototype = {
this.lastSync = modified;
let failed_ids = Object.keys(resp.obj.failed);
counts.failed += failed_ids.length;
if (failed_ids.length)
this._log.debug("Records that will be uploaded again because "
+ "the server couldn't store them: "
@ -1504,6 +1507,7 @@ SyncEngine.prototype = {
this._store._sleep(0);
}
postQueue.flush();
Observers.notify("weave:engine:sync:uploaded", counts, this.name);
}
},

View File

@ -32,6 +32,7 @@ Cu.import("resource://services-sync/rest.js");
Cu.import("resource://services-sync/stages/enginesync.js");
Cu.import("resource://services-sync/stages/declined.js");
Cu.import("resource://services-sync/status.js");
Cu.import("resource://services-sync/telemetry.js");
Cu.import("resource://services-sync/userapi.js");
Cu.import("resource://services-sync/util.js");
@ -548,7 +549,8 @@ Sync11Service.prototype = {
// Always check for errors; this is also where we look for X-Weave-Alert.
this.errorHandler.checkServerError(info);
if (!info.success) {
throw "Aborting sync: failed to get collections.";
this._log.error("Aborting sync: failed to get collections.")
throw info;
}
return info;
},

View File

@ -0,0 +1,432 @@
/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
this.EXPORTED_SYMBOLS = ["SyncTelemetry"];
Cu.import("resource://services-sync/browserid_identity.js");
Cu.import("resource://services-sync/main.js");
Cu.import("resource://services-sync/status.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-common/observers.js");
Cu.import("resource://services-common/async.js");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/TelemetryController.jsm");
Cu.import("resource://gre/modules/FxAccounts.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
let constants = {};
Cu.import("resource://services-sync/constants.js", constants);
var fxAccountsCommon = {};
Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
"@mozilla.org/base/telemetry;1",
"nsITelemetry");
const log = Log.repository.getLogger("Sync.Telemetry");
const TOPICS = [
"xpcom-shutdown",
"weave:service:sync:start",
"weave:service:sync:finish",
"weave:service:sync:error",
"weave:engine:sync:start",
"weave:engine:sync:finish",
"weave:engine:sync:error",
"weave:engine:sync:applied",
"weave:engine:sync:uploaded",
];
const PING_FORMAT_VERSION = 1;
// The set of engines we record telemetry for - any other engines are ignored.
const ENGINES = new Set(["addons", "bookmarks", "clients", "forms", "history",
"passwords", "prefs", "tabs"]);
// is it a wrapped auth error from browserid_identity?
function isBrowerIdAuthError(error) {
// I can't think of what could throw on String conversion
// but we have absolutely no clue about the type, and
// there's probably some things out there that would
try {
if (String(error).startsWith("AuthenticationError")) {
return true;
}
} catch (e) {}
return false;
}
function transformError(error, engineName) {
if (Async.isShutdownException(error)) {
return { name: "shutdownerror" };
}
if (typeof error === "string") {
if (error.startsWith("error.")) {
// This is hacky, but I can't imagine that it's not also accurate.
return { name: "othererror", error };
}
return { name: "unexpectederror", error };
}
if (error.failureCode) {
return { name: "othererror", error: error.failureCode };
}
if (error instanceof AuthenticationError) {
return { name: "autherror", from: error.source };
}
let httpCode = error.status ||
(error.response && error.response.status) ||
error.code;
if (httpCode) {
return { name: "httperror", code: httpCode };
}
if (error.result) {
return { name: "nserror", code: error.result };
}
return {
name: "unexpectederror",
error: String(error),
}
}
function tryGetMonotonicTimestamp() {
try {
return Telemetry.msSinceProcessStart();
} catch (e) {
log.warn("Unable to get a monotonic timestamp!");
return -1;
}
}
function timeDeltaFrom(monotonicStartTime) {
let now = tryGetMonotonicTimestamp();
if (monotonicStartTime !== -1 && now !== -1) {
return Math.round(now - monotonicStartTime);
}
return -1;
}
class EngineRecord {
constructor(name) {
// startTime is in ms from process start, but is monotonic (unlike Date.now())
// so we need to keep both it and when.
this.startTime = tryGetMonotonicTimestamp();
this.name = name;
}
toJSON() {
let result = Object.assign({}, this);
delete result.startTime;
return result;
}
finished(error) {
let took = timeDeltaFrom(this.startTime);
if (took > 0) {
this.took = took;
}
if (error) {
this.failureReason = transformError(error, this.name);
}
}
recordApplied(counts) {
if (this.incoming) {
log.error(`Incoming records applied multiple times for engine ${this.name}!`);
return;
}
if (this.name === "clients" && !counts.failed) {
// ignore successful application of client records
// since otherwise they show up every time and are meaningless.
return;
}
let incomingData = {};
let properties = ["applied", "failed", "newFailed", "reconciled"];
// Only record non-zero properties and only record incoming at all if
// there's at least one property we care about.
for (let property of properties) {
if (counts[property]) {
incomingData[property] = counts[property];
this.incoming = incomingData;
}
}
}
recordUploaded(counts) {
if (counts.sent || counts.failed) {
if (!this.outgoing) {
this.outgoing = [];
}
this.outgoing.push({
sent: counts.sent || undefined,
failed: counts.failed || undefined,
});
}
}
}
class TelemetryRecord {
constructor(allowedEngines) {
this.allowedEngines = allowedEngines;
// Our failure reason. This property only exists in the generated ping if an
// error actually occurred.
this.failureReason = undefined;
this.uid = "";
this.when = Date.now();
this.startTime = tryGetMonotonicTimestamp();
this.took = 0; // will be set later.
// All engines that have finished (ie, does not include the "current" one)
// We omit this from the ping if it's empty.
this.engines = [];
// The engine that has started but not yet stopped.
this.currentEngine = null;
}
// toJSON returns the actual payload we will submit.
toJSON() {
let result = {
when: this.when,
uid: this.uid,
took: this.took,
version: PING_FORMAT_VERSION,
failureReason: this.failureReason,
status: this.status,
};
let engines = [];
for (let engine of this.engines) {
engines.push(engine.toJSON());
}
if (engines.length > 0) {
result.engines = engines;
}
return result;
}
finished(error) {
this.took = timeDeltaFrom(this.startTime);
if (this.currentEngine != null) {
log.error("Finished called for the sync before the current engine finished");
this.currentEngine.finished(null);
this.onEngineStop(this.currentEngine.name);
}
if (error) {
this.failureReason = transformError(error);
}
try {
this.uid = Weave.Service.identity.userUID();
} catch (e) {
this.uid = "0".repeat(32);
}
// Check for engine statuses. -- We do this now, and not in engine.finished
// to make sure any statuses that get set "late" are recorded
for (let engine of this.engines) {
let status = Status.engines[engine.name];
if (status && status !== constants.ENGINE_SUCCEEDED) {
engine.status = status;
}
}
let statusObject = {};
let serviceStatus = Status.service;
if (serviceStatus && serviceStatus !== constants.STATUS_OK) {
statusObject.service = serviceStatus;
this.status = statusObject;
}
let syncStatus = Status.sync;
if (syncStatus && syncStatus !== constants.SYNC_SUCCEEDED) {
statusObject.sync = syncStatus;
this.status = statusObject;
}
}
onEngineStart(engineName) {
if (this._shouldIgnoreEngine(engineName, false)) {
return;
}
if (this.currentEngine) {
log.error(`Being told that engine ${engineName} has started, but current engine ${
this.currentEngine.name} hasn't stopped`);
// Just discard the current engine rather than making up data for it.
}
this.currentEngine = new EngineRecord(engineName);
}
onEngineStop(engineName, error) {
if (error && !this.currentEngine) {
log.error(`Error triggered on ${engineName} when no current engine exists: ${error}`);
// It's possible for us to get an error before the start message of an engine
// (somehow), in which case we still want to record that error.
this.currentEngine = new EngineRecord(engineName);
} else if (!this.currentEngine || (engineName && this._shouldIgnoreEngine(engineName, true))) {
return;
}
this.currentEngine.finished(error);
this.engines.push(this.currentEngine);
this.currentEngine = null;
}
onEngineApplied(engineName, counts) {
if (this._shouldIgnoreEngine(engineName)) {
return;
}
this.currentEngine.recordApplied(counts);
}
onEngineUploaded(engineName, counts) {
if (this._shouldIgnoreEngine(engineName)) {
return;
}
this.currentEngine.recordUploaded(counts);
}
_shouldIgnoreEngine(engineName, shouldBeCurrent = true) {
if (!this.allowedEngines.has(engineName)) {
log.info(`Notification for engine ${engineName}, but we aren't recording telemetry for it`);
return true;
}
if (shouldBeCurrent) {
if (!this.currentEngine || engineName != this.currentEngine.name) {
log.error(`Notification for engine ${engineName} but it isn't current`);
return true;
}
}
return false;
}
}
class SyncTelemetryImpl {
constructor(allowedEngines) {
log.level = Log.Level[Svc.Prefs.get("log.logger.telemetry", "Trace")];
// This is accessible so we can enable custom engines during tests.
this.allowedEngines = allowedEngines;
this.current = null;
this.setupObservers();
}
setupObservers() {
for (let topic of TOPICS) {
Observers.add(topic, this, this);
}
}
shutdown() {
for (let topic of TOPICS) {
Observers.remove(topic, this, this);
}
}
submit(record) {
TelemetryController.submitExternalPing("sync", record);
}
onSyncStarted() {
if (this.current) {
log.warn("Observed weave:service:sync:start, but we're already recording a sync!");
// Just discard the old record, consistent with our handling of engines, above.
}
this.current = new TelemetryRecord(this.allowedEngines);
}
_checkCurrent(topic) {
if (!this.current) {
log.warn(`Observed notification ${topic} but no current sync is being recorded.`);
return false;
}
return true;
}
onSyncFinished(error) {
if (!this.current) {
log.warn("onSyncFinished but we aren't recording");
return;
}
this.current.finished(error);
let current = this.current;
this.current = null;
this.submit(current.toJSON());
}
observe(subject, topic, data) {
log.trace(`observed ${topic} ${data}`);
switch (topic) {
case "xpcom-shutdown":
this.shutdown();
break;
/* sync itself state changes */
case "weave:service:sync:start":
this.onSyncStarted();
break;
case "weave:service:sync:finish":
if (this._checkCurrent(topic)) {
this.onSyncFinished(null);
}
break;
case "weave:service:sync:error":
// argument needs to be truthy (this should always be the case)
this.onSyncFinished(subject || "Unknown");
break;
/* engine sync state changes */
case "weave:engine:sync:start":
if (this._checkCurrent(topic)) {
this.current.onEngineStart(data);
}
break;
case "weave:engine:sync:finish":
if (this._checkCurrent(topic)) {
this.current.onEngineStop(data, null);
}
break;
case "weave:engine:sync:error":
if (this._checkCurrent(topic)) {
// argument needs to be truthy (this should always be the case)
this.current.onEngineStop(data, subject || "Unknown");
}
break;
/* engine counts */
case "weave:engine:sync:applied":
if (this._checkCurrent(topic)) {
this.current.onEngineApplied(data, subject);
}
break;
case "weave:engine:sync:uploaded":
if (this._checkCurrent(topic)) {
this.current.onEngineUploaded(data, subject);
}
break;
default:
log.warn(`unexpected observer topic ${topic}`);
break;
}
}
}
this.SyncTelemetry = new SyncTelemetryImpl(ENGINES);

View File

@ -35,6 +35,7 @@ EXTRA_JS_MODULES['services-sync'] += [
'modules/service.js',
'modules/status.js',
'modules/SyncedTabs.jsm',
'modules/telemetry.js',
'modules/userapi.js',
'modules/util.js',
]

View File

@ -4,6 +4,36 @@
Cu.import("resource://services-common/async.js");
Cu.import("resource://testing-common/services/common/utils.js");
Cu.import("resource://testing-common/PlacesTestUtils.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGetter(this, 'SyncPingSchema', function() {
let ns = {};
Cu.import("resource://gre/modules/FileUtils.jsm", ns);
let stream = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream);
let jsonReader = Cc["@mozilla.org/dom/json;1"]
.createInstance(Components.interfaces.nsIJSON);
let schema;
try {
let schemaFile = do_get_file("sync_ping_schema.json");
stream.init(schemaFile, ns.FileUtils.MODE_RDONLY, ns.FileUtils.PERMS_FILE, 0);
schema = jsonReader.decodeFromStream(stream, stream.available());
} finally {
stream.close();
}
// Allow tests to make whatever engines they want, this shouldn't cause
// validation failure.
schema.definitions.engine.properties.name = { type: "string" };
return schema;
});
XPCOMUtils.defineLazyGetter(this, 'SyncPingValidator', function() {
let ns = {};
Cu.import("resource://testing-common/ajv-4.1.1.js", ns);
let ajv = new ns.Ajv({ async: "co*" });
return ajv.compile(SyncPingSchema);
});
var provider = {
getFile: function(prop, persistent) {
@ -206,3 +236,157 @@ function do_check_array_eq(a1, a2) {
do_check_eq(a1[i], a2[i]);
}
}
// Helper function to get the sync telemetry and add the typically used test
// engine names to its list of allowed engines.
function get_sync_test_telemetry() {
let ns = {};
Cu.import("resource://services-sync/telemetry.js", ns);
let testEngines = ["rotary", "steam", "sterling", "catapult"];
for (let engineName of testEngines) {
ns.SyncTelemetry.allowedEngines.add(engineName);
}
return ns.SyncTelemetry;
}
function assert_valid_ping(record) {
if (record) {
if (!SyncPingValidator(record)) {
deepEqual([], SyncPingValidator.errors, "Sync telemetry ping validation failed");
}
equal(record.version, 1);
lessOrEqual(record.when, Date.now());
}
}
// Asserts that `ping` is a ping that doesn't contain any failure information
function assert_success_ping(ping) {
ok(!!ping);
assert_valid_ping(ping);
ok(!ping.failureReason);
equal(undefined, ping.status);
greater(ping.engines.length, 0);
for (let e of ping.engines) {
ok(!e.failureReason);
equal(undefined, e.status);
if (e.outgoing) {
for (let o of e.outgoing) {
equal(undefined, o.failed);
notEqual(undefined, o.sent);
}
}
if (e.incoming) {
equal(undefined, e.incoming.failed);
equal(undefined, e.incoming.newFailed);
notEqual(undefined, e.incoming.applied || e.incoming.reconciled);
}
}
}
// Hooks into telemetry to validate all pings after calling.
function validate_all_future_pings() {
let telem = get_sync_test_telemetry();
telem.submit = assert_valid_ping;
}
function wait_for_ping(callback, allowErrorPings) {
return new Promise(resolve => {
let telem = get_sync_test_telemetry();
let oldSubmit = telem.submit;
telem.submit = function(record) {
telem.submit = oldSubmit;
if (allowErrorPings) {
assert_valid_ping(record);
} else {
assert_success_ping(record);
}
resolve(record);
};
callback();
});
}
// Short helper for wait_for_ping
function sync_and_validate_telem(allowErrorPings) {
return wait_for_ping(() => Service.sync(), allowErrorPings);
}
// Used for the (many) cases where we do a 'partial' sync, where only a single
// engine is actually synced, but we still want to ensure we're generating a
// valid ping. Returns a promise that resolves to the ping, or rejects with the
// thrown error after calling an optional callback.
function sync_engine_and_validate_telem(engine, allowErrorPings, onError) {
return new Promise((resolve, reject) => {
let telem = get_sync_test_telemetry();
let caughtError = null;
// Clear out status, so failures from previous syncs won't show up in the
// telemetry ping.
let ns = {};
Cu.import("resource://services-sync/status.js", ns);
ns.Status._engines = {};
ns.Status.partial = false;
// Ideally we'd clear these out like we do with engines, (probably via
// Status.resetSync()), but this causes *numerous* tests to fail, so we just
// assume that if no failureReason or engine failures are set, and the
// status properties are the same as they were initially, that it's just
// a leftover.
// This is only an issue since we're triggering the sync of just one engine,
// without doing any other parts of the sync.
let initialServiceStatus = ns.Status._service;
let initialSyncStatus = ns.Status._sync;
let oldSubmit = telem.submit;
telem.submit = function(record) {
telem.submit = oldSubmit;
if (record && record.status) {
// did we see anything to lead us to believe that something bad actually happened
let realProblem = record.failureReason || record.engines.some(e => {
if (e.failureReason || e.status) {
return true;
}
if (e.outgoing && e.outgoing.some(o => o.failed > 0)) {
return true;
}
return e.incoming && e.incoming.failed;
});
if (!realProblem) {
// no, so if the status is the same as it was initially, just assume
// that its leftover and that we can ignore it.
if (record.status.sync && record.status.sync == initialSyncStatus) {
delete record.status.sync;
}
if (record.status.service && record.status.service == initialServiceStatus) {
delete record.status.service;
}
if (!record.status.sync && !record.status.service) {
delete record.status;
}
}
}
if (allowErrorPings) {
assert_valid_ping(record);
} else {
assert_success_ping(record);
}
if (caughtError) {
if (onError) {
onError(record);
}
reject(caughtError);
} else {
resolve(record);
}
};
Svc.Obs.notify("weave:service:sync:start");
try {
engine.sync();
} catch (e) {
caughtError = e;
}
if (caughtError) {
Svc.Obs.notify("weave:service:sync:error", caughtError);
} else {
Svc.Obs.notify("weave:service:sync:finish");
}
});
}

View File

@ -0,0 +1,147 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "schema for Sync pings, documentation avaliable in toolkit/components/telemetry/docs/sync-ping.rst",
"type": "object",
"additionalProperties": false,
"required": ["when", "version", "took", "uid"],
"properties": {
"version": { "type": "integer", "minimum": 0 },
"didLogin": { "type": "boolean" },
"when": { "type": "integer" },
"status": {
"type": "object",
"anyOf": [
{"required": ["sync"]},
{"required": ["service"]}
],
"additionalProperties": false,
"properties": {
"sync": { "type": "string" },
"service": { "type": "string" }
}
},
"why": { "enum": ["startup", "schedule", "score", "user", "tabs"] },
"took": { "type": "integer", "minimum": -1 },
"uid": {
"type": "string",
"oneOf": [
{ "pattern": "^[0-9a-f]{32}$" },
{ "maxLength": 0 }
]
},
"failureReason": { "$ref": "#/definitions/error" },
"engines": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/definitions/engine" }
}
},
"definitions": {
"engine": {
"required": ["name"],
"additionalProperties": false,
"properties": {
"failureReason": { "$ref": "#/definitions/error" },
"name": { "enum": ["addons", "bookmarks", "clients", "forms", "history", "passwords", "prefs", "tabs"] },
"took": { "type": "integer", "minimum": 1 },
"status": { "type": "string" },
"incoming": {
"type": "object",
"additionalProperties": false,
"anyOf": [
{"required": ["applied"]},
{"required": ["failed"]},
{"required": ["newFailed"]},
{"required": ["reconciled"]}
],
"properties": {
"applied": { "type": "integer", "minimum": 1 },
"failed": { "type": "integer", "minimum": 1 },
"newFailed": { "type": "integer", "minimum": 1 },
"reconciled": { "type": "integer", "minimum": 1 }
}
},
"outgoing": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/definitions/outgoingBatch" }
},
"validation": {
"type": "array",
"minItems": 1,
"$ref": "#/definitions/validationProblem"
}
}
},
"outgoingBatch": {
"type": "object",
"additionalProperties": false,
"anyOf": [
{"required": ["sent"]},
{"required": ["failed"]}
],
"properties": {
"sent": { "type": "integer", "minimum": 1 },
"failed": { "type": "integer", "minimum": 1 }
}
},
"error": {
"oneOf": [
{ "$ref": "#/definitions/httpError" },
{ "$ref": "#/definitions/nsError" },
{ "$ref": "#/definitions/shutdownError" },
{ "$ref": "#/definitions/authError" },
{ "$ref": "#/definitions/otherError" },
{ "$ref": "#/definitions/unexpectedError" }
]
},
"httpError": {
"required": ["name", "code"],
"properties": {
"name": { "enum": ["httperror"] },
"code": { "type": "integer" }
}
},
"nsError": {
"required": ["name", "code"],
"properties": {
"name": { "enum": ["nserror"] },
"code": { "type": "integer" }
}
},
"shutdownError": {
"required": ["name"],
"properties": {
"name": { "enum": ["shutdownerror"] }
}
},
"authError": {
"required": ["name"],
"properties": {
"name": { "enum": ["autherror"] },
"from": { "enum": ["tokenserver", "fxaccounts", "hawkclient"] }
}
},
"otherError": {
"required": ["name"],
"properties": {
"name": { "enum": ["othererror"] },
"error": { "type": "string" }
}
},
"unexpectedError": {
"required": ["name"],
"properties": {
"name": { "enum": ["unexpectederror"] },
"error": { "type": "string" }
}
},
"validationProblem": {
"required": ["name", "count"],
"properties": {
"name": { "type": "string" },
"count": { "type": "integer" }
}
}
}
}

View File

@ -99,7 +99,7 @@ function serverForFoo(engine) {
});
}
add_test(function test_processIncoming_error_orderChildren() {
add_task(function* test_processIncoming_error_orderChildren() {
_("Ensure that _orderChildren() is called even when _processIncoming() throws an error.");
let engine = new BookmarksEngine(Service);
@ -146,11 +146,11 @@ add_test(function test_processIncoming_error_orderChildren() {
let error;
try {
engine.sync();
yield sync_engine_and_validate_telem(engine, true)
} catch(ex) {
error = ex;
}
do_check_true(!!error);
ok(!!error);
// Verify that the bookmark order has been applied.
let new_children = store.createRecord(folder1_guid).children;
@ -165,7 +165,7 @@ add_test(function test_processIncoming_error_orderChildren() {
store.wipe();
Svc.Prefs.resetBranch("");
Service.recordManager.clearCache();
server.stop(run_next_test);
yield new Promise(resolve => server.stop(resolve));
}
});
@ -218,7 +218,7 @@ add_task(function* test_restorePromptsReupload() {
let error;
try {
engine.sync();
yield sync_engine_and_validate_telem(engine, false);
} catch(ex) {
error = ex;
_("Got error: " + Log.exceptionStr(ex));
@ -262,7 +262,7 @@ add_task(function* test_restorePromptsReupload() {
_("Sync again. This'll wipe bookmarks from the server.");
try {
engine.sync();
yield sync_engine_and_validate_telem(engine, false);
} catch(ex) {
error = ex;
_("Got error: " + Log.exceptionStr(ex));

View File

@ -57,7 +57,7 @@ function serverForFoo(engine) {
// Verify that Places smart bookmarks have their annotation uploaded and
// handled locally.
add_test(function test_annotation_uploaded() {
add_task(function *test_annotation_uploaded() {
let server = serverForFoo(engine);
new SyncTestingInfrastructure(server.server);
@ -110,7 +110,7 @@ add_test(function test_annotation_uploaded() {
let collection = server.user("foo").collection("bookmarks");
try {
engine.sync();
yield sync_engine_and_validate_telem(engine, false);
let wbos = collection.keys(function (id) {
return ["menu", "toolbar", "mobile", "unfiled"].indexOf(id) == -1;
});
@ -141,7 +141,7 @@ add_test(function test_annotation_uploaded() {
do_check_eq(smartBookmarkCount(), startCount);
_("Sync. Verify that the downloaded record carries the annotation.");
engine.sync();
yield sync_engine_and_validate_telem(engine, false);
_("Verify that the Places DB now has an annotated bookmark.");
_("Our count has increased again.");

View File

@ -33,6 +33,9 @@ add_identity_test(this, function* test_missing_crypto_collection() {
};
let collections = ["clients", "bookmarks", "forms", "history",
"passwords", "prefs", "tabs"];
// Disable addon sync because AddonManager won't be initialized here.
Service.engineManager.unregister("addons");
for (let coll of collections) {
handlers["/1.1/johndoe/storage/" + coll] =
johnU(coll, new ServerCollection({}, true).handler());
@ -50,7 +53,7 @@ add_identity_test(this, function* test_missing_crypto_collection() {
};
_("Startup, no meta/global: freshStart called once.");
Service.sync();
yield sync_and_validate_telem();
do_check_eq(fresh, 1);
fresh = 0;
@ -60,12 +63,12 @@ add_identity_test(this, function* test_missing_crypto_collection() {
_("Simulate a bad info/collections.");
delete johnColls.crypto;
Service.sync();
yield sync_and_validate_telem();
do_check_eq(fresh, 1);
fresh = 0;
_("Regular sync: no need to freshStart.");
Service.sync();
yield sync_and_validate_telem();
do_check_eq(fresh, 0);
} finally {

View File

@ -59,6 +59,7 @@ add_task(function* test_locally_changed_keys() {
Service.clusterURL = server.baseURI;
Service.engineManager.register(HistoryEngine);
Service.engineManager.unregister("addons");
function corrupt_local_keys() {
Service.collectionKeys._default.keyPair = [Svc.Crypto.generateRandomKey(),
@ -86,7 +87,7 @@ add_task(function* test_locally_changed_keys() {
do_check_true(Service.isLoggedIn);
// Sync should upload records.
Service.sync();
yield sync_and_validate_telem();
// Tabs exist.
_("Tabs modified: " + johndoe.modified("tabs"));
@ -139,7 +140,9 @@ add_task(function* test_locally_changed_keys() {
_("HMAC error count: " + hmacErrorCount);
// Now syncing should succeed, after one HMAC error.
Service.sync();
let ping = yield wait_for_ping(() => Service.sync(), true);
equal(ping.engines.find(e => e.name == "history").incoming.applied, 5);
do_check_eq(hmacErrorCount, 1);
_("Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair);
@ -183,7 +186,9 @@ add_task(function* test_locally_changed_keys() {
Service.lastHMACEvent = 0;
_("Syncing...");
Service.sync();
ping = yield sync_and_validate_telem(true);
do_check_eq(ping.engines.find(e => e.name == "history").incoming.failed, 5);
_("Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair);
_("Server keys have been updated, and we skipped over 5 more HMAC errors without adjusting history.");
do_check_true(johndoe.modified("crypto") > old_key_time);
@ -204,6 +209,7 @@ add_task(function* test_locally_changed_keys() {
function run_test() {
let logger = Log.repository.rootLogger;
Log.repository.rootLogger.addAppender(new Log.DumpAppender());
validate_all_future_pings();
ensureLegacyIdentityManager();

View File

@ -171,10 +171,12 @@ add_test(function test_sync() {
do_check_false(engine.enabled);
do_check_false(engine.wasSynced);
engine.sync();
do_check_false(engine.wasSynced);
_("Engine.sync calls _sync if it's enabled");
engine.enabled = true;
engine.sync();
do_check_true(engine.wasSynced);
do_check_eq(engineObserver.topics[0], "weave:engine:sync:start");

View File

@ -166,7 +166,7 @@ add_identity_test(this, function* test_401_logout() {
yield setUp(server);
// By calling sync, we ensure we're logged in.
Service.sync();
yield sync_and_validate_telem();
do_check_eq(Status.sync, SYNC_SUCCEEDED);
do_check_true(Service.isLoggedIn);
@ -201,7 +201,8 @@ add_identity_test(this, function* test_401_logout() {
Service._updateCachedURLs();
_("Starting first sync.");
Service.sync();
let ping = yield sync_and_validate_telem(true);
deepEqual(ping.failureReason, { name: "httperror", code: 401 });
_("First sync done.");
yield deferred.promise;
});
@ -211,12 +212,18 @@ add_identity_test(this, function* test_credentials_changed_logout() {
yield setUp(server);
// By calling sync, we ensure we're logged in.
Service.sync();
yield sync_and_validate_telem();
do_check_eq(Status.sync, SYNC_SUCCEEDED);
do_check_true(Service.isLoggedIn);
generateCredentialsChangedFailure();
Service.sync();
let ping = yield sync_and_validate_telem(true);
equal(ping.status.sync, CREDENTIALS_CHANGED);
deepEqual(ping.failureReason, {
name: "unexpectederror",
error: "Error: Aborting sync, remote setup failed"
});
do_check_eq(Status.sync, CREDENTIALS_CHANGED);
do_check_false(Service.isLoggedIn);
@ -539,13 +546,20 @@ add_identity_test(this, function* test_sync_syncAndReportErrors_non_network_erro
Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
do_check_eq(Status.sync, CREDENTIALS_CHANGED);
clean();
server.stop(deferred.resolve);
// If we clean this tick, telemetry won't get the right error
server.stop(() => {
clean();
deferred.resolve();
});
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
let ping = yield wait_for_ping(() => errorHandler.syncAndReportErrors(), true);
equal(ping.status.sync, CREDENTIALS_CHANGED);
deepEqual(ping.failureReason, {
name: "unexpectederror",
error: "Error: Aborting sync, remote setup failed"
});
yield deferred.promise;
});
@ -589,13 +603,20 @@ add_identity_test(this, function* test_sync_syncAndReportErrors_prolonged_non_ne
Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
do_check_eq(Status.sync, CREDENTIALS_CHANGED);
clean();
server.stop(deferred.resolve);
// If we clean this tick, telemetry won't get the right error
server.stop(() => {
clean();
deferred.resolve();
});
});
setLastSync(PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
let ping = yield wait_for_ping(() => errorHandler.syncAndReportErrors(), true);
equal(ping.status.sync, CREDENTIALS_CHANGED);
deepEqual(ping.failureReason, {
name: "unexpectederror",
error: "Error: Aborting sync, remote setup failed"
});
yield deferred.promise;
});
@ -715,13 +736,20 @@ add_task(function* test_sync_prolonged_non_network_error() {
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_true(errorHandler.didReportProlongedError);
clean();
server.stop(deferred.resolve);
server.stop(() => {
clean();
deferred.resolve();
});
});
setLastSync(PROLONGED_ERROR_DURATION);
Service.sync();
let ping = yield sync_and_validate_telem(true);
equal(ping.status.sync, PROLONGED_SYNC_FAILURE);
deepEqual(ping.failureReason, {
name: "unexpectederror",
error: "Error: Aborting sync, remote setup failed"
});
yield deferred.promise;
});
@ -880,12 +908,17 @@ add_identity_test(this, function* test_sync_server_maintenance_error() {
do_check_false(errorHandler.didReportProlongedError);
Svc.Obs.remove("weave:ui:sync:error", onSyncError);
clean();
server.stop(deferred.resolve);
server.stop(() => {
clean();
deferred.resolve();
})
});
setLastSync(NON_PROLONGED_ERROR_DURATION);
Service.sync();
let ping = yield sync_and_validate_telem(true);
equal(ping.status.sync, SERVER_MAINTENANCE);
deepEqual(ping.engines.find(e => e.failureReason).failureReason, { name: "httperror", code: 503 })
yield deferred.promise;
});
@ -1040,14 +1073,19 @@ add_task(function* test_sync_prolonged_server_maintenance_error() {
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_true(errorHandler.didReportProlongedError);
clean();
server.stop(deferred.resolve);
server.stop(() => {
clean();
deferred.resolve();
});
});
do_check_eq(Status.service, STATUS_OK);
setLastSync(PROLONGED_ERROR_DURATION);
Service.sync();
let ping = yield sync_and_validate_telem(true);
deepEqual(ping.status.sync, PROLONGED_SYNC_FAILURE);
deepEqual(ping.engines.find(e => e.failureReason).failureReason,
{ name: "httperror", code: 503 });
yield deferred.promise;
});
@ -1264,9 +1302,10 @@ add_identity_test(this, function* test_wipeRemote_prolonged_server_maintenance_e
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_eq(Svc.Prefs.get("firstSync"), "wipeRemote");
do_check_true(errorHandler.didReportProlongedError);
clean();
server.stop(deferred.resolve);
server.stop(() => {
clean();
deferred.resolve();
});
});
do_check_false(Status.enforceBackoff);
@ -1274,7 +1313,8 @@ add_identity_test(this, function* test_wipeRemote_prolonged_server_maintenance_e
Svc.Prefs.set("firstSync", "wipeRemote");
setLastSync(PROLONGED_ERROR_DURATION);
Service.sync();
let ping = yield sync_and_validate_telem(true);
deepEqual(ping.failureReason, { name: "httperror", code: 503 });
yield deferred.promise;
});
@ -1770,10 +1810,10 @@ add_identity_test(this, function* test_wipeServer_login_syncAndReportErrors_prol
add_task(function* test_sync_engine_generic_fail() {
let server = sync_httpd_setup();
let engine = engineManager.get("catapult");
let engine = engineManager.get("catapult");
engine.enabled = true;
engine.sync = function sync() {
Svc.Obs.notify("weave:engine:sync:error", "", "catapult");
Svc.Obs.notify("weave:engine:sync:error", ENGINE_UNKNOWN_FAIL, "catapult");
};
let log = Log.repository.getLogger("Sync.ErrorHandler");
@ -1808,12 +1848,18 @@ add_task(function* test_sync_engine_generic_fail() {
let syncErrors = sumHistogram("WEAVE_ENGINE_SYNC_ERRORS", { key: "catapult" });
do_check_true(syncErrors, 1);
server.stop(deferred.resolve);
server.stop(() => {
clean();
deferred.resolve();
});
});
});
do_check_true(yield setUp(server));
Service.sync();
let ping = yield sync_and_validate_telem(true);
deepEqual(ping.status.service, SYNC_FAILED_PARTIAL);
deepEqual(ping.engines.find(e => e.status).status, ENGINE_UNKNOWN_FAIL);
yield deferred.promise;
});

View File

@ -35,6 +35,8 @@ function run_test() {
Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
validate_all_future_pings();
run_next_test();
}

View File

@ -276,6 +276,7 @@ add_identity_test(this, function* test_resource_timeout() {
});
function run_test() {
validate_all_future_pings();
engineManager.register(CatapultEngine);
run_next_test();
}

View File

@ -49,7 +49,7 @@ function shared_setup() {
return [engine, rotaryColl, clientsColl, keysWBO, global];
}
add_test(function hmac_error_during_404() {
add_task(function *hmac_error_during_404() {
_("Attempt to replicate the HMAC error setup.");
let [engine, rotaryColl, clientsColl, keysWBO, global] = shared_setup();
@ -83,13 +83,14 @@ add_test(function hmac_error_during_404() {
try {
_("Syncing.");
Service.sync();
yield sync_and_validate_telem();
_("Partially resetting client, as if after a restart, and forcing redownload.");
Service.collectionKeys.clear();
engine.lastSync = 0; // So that we redownload records.
key404Counter = 1;
_("---------------------------");
Service.sync();
yield sync_and_validate_telem();
_("---------------------------");
// Two rotary items, one client record... no errors.
@ -97,7 +98,7 @@ add_test(function hmac_error_during_404() {
} finally {
Svc.Prefs.resetBranch("");
Service.recordManager.clearCache();
server.stop(run_next_test);
yield new Promise(resolve => server.stop(resolve));
}
});

View File

@ -23,7 +23,7 @@ function run_test() {
Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
initTestLogging();
validate_all_future_pings();
ensureLegacyIdentityManager();
Service.engineManager.register(RotaryEngine);

View File

@ -10,6 +10,7 @@ Svc.Prefs.set("registerEngines", "Tab,Bookmarks,Form,History");
Cu.import("resource://services-sync/service.js");
function run_test() {
validate_all_future_pings();
_("When imported, Service.onStartup is called");
initTestLogging("Trace");

View File

@ -5,6 +5,7 @@ Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
function run_test() {
validate_all_future_pings();
let debug = [];
let info = [];

View File

@ -10,6 +10,7 @@ Cu.import("resource://testing-common/services/sync/fakeservices.js");
Cu.import("resource://testing-common/services/sync/utils.js");
function run_test() {
validate_all_future_pings();
let logger = Log.repository.rootLogger;
Log.repository.rootLogger.addAppender(new Log.DumpAppender());

View File

@ -77,6 +77,7 @@ function setUp() {
function run_test() {
initTestLogging("Trace");
validate_all_future_pings();
Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;

View File

@ -86,6 +86,7 @@ function run_test() {
initTestLogging("Trace");
Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
validate_all_future_pings();
run_next_test();
}

View File

@ -15,13 +15,22 @@ function makeRotaryEngine() {
return new RotaryEngine(Service);
}
function cleanAndGo(server) {
function clean() {
Svc.Prefs.resetBranch("");
Svc.Prefs.set("log.logger.engine.rotary", "Trace");
Service.recordManager.clearCache();
}
function cleanAndGo(server) {
clean();
server.stop(run_next_test);
}
function promiseClean(server) {
clean();
return new Promise(resolve => server.stop(resolve));
}
function configureService(server, username, password) {
Service.clusterURL = server.baseURI;
@ -667,7 +676,7 @@ add_test(function test_processIncoming_mobile_batchSize() {
});
add_test(function test_processIncoming_store_toFetch() {
add_task(function *test_processIncoming_store_toFetch() {
_("If processIncoming fails in the middle of a batch on mobile, state is saved in toFetch and lastSync.");
Service.identity.username = "foo";
Svc.Prefs.set("client.type", "mobile");
@ -714,11 +723,10 @@ add_test(function test_processIncoming_store_toFetch() {
let error;
try {
engine.sync();
yield sync_engine_and_validate_telem(engine, true);
} catch (ex) {
error = ex;
}
do_check_true(!!error);
// Only the first two batches have been applied.
do_check_eq(Object.keys(engine._store.items).length,
@ -730,7 +738,7 @@ add_test(function test_processIncoming_store_toFetch() {
do_check_eq(engine.lastSync, collection.wbo("record-no-99").modified);
} finally {
cleanAndGo(server);
yield promiseClean(server);
}
});
@ -1221,7 +1229,7 @@ add_test(function test_processIncoming_failed_records() {
});
add_test(function test_processIncoming_decrypt_failed() {
add_task(function *test_processIncoming_decrypt_failed() {
_("Ensure that records failing to decrypt are either replaced or refetched.");
Service.identity.username = "foo";
@ -1280,7 +1288,10 @@ add_test(function test_processIncoming_decrypt_failed() {
});
engine.lastSync = collection.wbo("nojson").modified - 1;
engine.sync();
let ping = yield sync_engine_and_validate_telem(engine, true);
do_check_eq(ping.engines[0].incoming.applied, 2);
do_check_eq(ping.engines[0].incoming.failed, 4);
do_check_eq(ping.engines[0].incoming.newFailed, 4);
do_check_eq(engine.previousFailed.length, 4);
do_check_eq(engine.previousFailed[0], "nojson");
@ -1294,7 +1305,7 @@ add_test(function test_processIncoming_decrypt_failed() {
do_check_eq(observerSubject.failed, 4);
} finally {
cleanAndGo(server);
yield promiseClean(server);
}
});
@ -1358,7 +1369,7 @@ add_test(function test_uploadOutgoing_toEmptyServer() {
});
add_test(function test_uploadOutgoing_failed() {
add_task(function *test_uploadOutgoing_failed() {
_("SyncEngine._uploadOutgoing doesn't clear the tracker of objects that failed to upload.");
Service.identity.username = "foo";
@ -1401,7 +1412,7 @@ add_test(function test_uploadOutgoing_failed() {
do_check_eq(engine._tracker.changedIDs['peppercorn'], PEPPERCORN_CHANGED);
engine.enabled = true;
engine.sync();
yield sync_engine_and_validate_telem(engine, true);
// Local timestamp has been set.
do_check_true(engine.lastSyncLocal > 0);
@ -1416,7 +1427,7 @@ add_test(function test_uploadOutgoing_failed() {
do_check_eq(engine._tracker.changedIDs['peppercorn'], PEPPERCORN_CHANGED);
} finally {
cleanAndGo(server);
yield promiseClean(server);
}
});
@ -1699,7 +1710,7 @@ add_test(function test_syncFinish_deleteLotsInBatches() {
});
add_test(function test_sync_partialUpload() {
add_task(function *test_sync_partialUpload() {
_("SyncEngine.sync() keeps changedIDs that couldn't be uploaded.");
Service.identity.username = "foo";
@ -1747,11 +1758,12 @@ add_test(function test_sync_partialUpload() {
engine.enabled = true;
let error;
try {
engine.sync();
yield sync_engine_and_validate_telem(engine, true);
} catch (ex) {
error = ex;
}
do_check_true(!!error);
ok(!!error);
// The timestamp has been updated.
do_check_true(engine.lastSyncLocal > 456);
@ -1769,7 +1781,7 @@ add_test(function test_sync_partialUpload() {
}
} finally {
cleanAndGo(server);
yield promiseClean(server);
}
});

View File

@ -90,6 +90,7 @@ function run_test() {
Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
Log.repository.getLogger("Sync.scheduler").level = Log.Level.Trace;
validate_all_future_pings();
// The scheduler checks Weave.fxaEnabled to determine whether to use
// FxA defaults or legacy defaults. As .fxaEnabled checks the username, we

View File

@ -0,0 +1,409 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://services-common/observers.js");
Cu.import("resource://services-sync/telemetry.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/resource.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/engines/clients.js");
Cu.import("resource://testing-common/services/sync/utils.js");
Cu.import("resource://testing-common/services/sync/fxa_utils.js");
Cu.import("resource://testing-common/services/sync/rotaryengine.js");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://services-sync/util.js");
initTestLogging("Trace");
function SteamStore(engine) {
Store.call(this, "Steam", engine);
}
SteamStore.prototype = {
__proto__: Store.prototype,
};
function SteamTracker(name, engine) {
Tracker.call(this, name || "Steam", engine);
}
SteamTracker.prototype = {
__proto__: Tracker.prototype
};
function SteamEngine(service) {
Engine.call(this, "steam", service);
}
SteamEngine.prototype = {
__proto__: Engine.prototype,
_storeObj: SteamStore,
_trackerObj: SteamTracker,
_errToThrow: null,
_sync() {
if (this._errToThrow) {
throw this._errToThrow;
}
}
};
function cleanAndGo(server) {
Svc.Prefs.resetBranch("");
Svc.Prefs.set("log.logger.engine.rotary", "Trace");
Service.recordManager.clearCache();
return new Promise(resolve => server.stop(resolve));
}
// Avoid addon manager complaining about not being initialized
Service.engineManager.unregister("addons");
add_identity_test(this, function *test_basic() {
let helper = track_collections_helper();
let upd = helper.with_updated_collection;
yield configureIdentity({ username: "johndoe" });
let handlers = {
"/1.1/johndoe/info/collections": helper.handler,
"/1.1/johndoe/storage/crypto/keys": upd("crypto", new ServerWBO("keys").handler()),
"/1.1/johndoe/storage/meta/global": upd("meta", new ServerWBO("global").handler())
};
let collections = ["clients", "bookmarks", "forms", "history", "passwords", "prefs", "tabs"];
for (let coll of collections) {
handlers["/1.1/johndoe/storage/" + coll] = upd(coll, new ServerCollection({}, true).handler());
}
let server = httpd_setup(handlers);
Service.serverURL = server.baseURI;
yield sync_and_validate_telem(true);
yield new Promise(resolve => server.stop(resolve));
});
add_task(function* test_processIncoming_error() {
let engine = new BookmarksEngine(Service);
let store = engine._store;
let server = serverForUsers({"foo": "password"}, {
meta: {global: {engines: {bookmarks: {version: engine.version,
syncID: engine.syncID}}}},
bookmarks: {}
});
new SyncTestingInfrastructure(server.server);
let collection = server.user("foo").collection("bookmarks");
try {
// Create a bogus record that when synced down will provoke a
// network error which in turn provokes an exception in _processIncoming.
const BOGUS_GUID = "zzzzzzzzzzzz";
let bogus_record = collection.insert(BOGUS_GUID, "I'm a bogus record!");
bogus_record.get = function get() {
throw "Sync this!";
};
// Make the 10 minutes old so it will only be synced in the toFetch phase.
bogus_record.modified = Date.now() / 1000 - 60 * 10;
engine.lastSync = Date.now() / 1000 - 60;
engine.toFetch = [BOGUS_GUID];
let error, ping;
try {
yield sync_engine_and_validate_telem(engine, true, errPing => ping = errPing);
} catch(ex) {
error = ex;
}
ok(!!error);
ok(!!ping);
equal(ping.uid, "0".repeat(32));
deepEqual(ping.failureReason, {
name: "othererror",
error: "error.engine.reason.record_download_fail"
});
equal(ping.engines.length, 1);
equal(ping.engines[0].name, "bookmarks");
deepEqual(ping.engines[0].failureReason, {
name: "othererror",
error: "error.engine.reason.record_download_fail"
});
} finally {
store.wipe();
yield cleanAndGo(server);
}
});
add_task(function *test_uploading() {
let engine = new BookmarksEngine(Service);
let store = engine._store;
let server = serverForUsers({"foo": "password"}, {
meta: {global: {engines: {bookmarks: {version: engine.version,
syncID: engine.syncID}}}},
bookmarks: {}
});
new SyncTestingInfrastructure(server.server);
let parent = PlacesUtils.toolbarFolderId;
let uri = Utils.makeURI("http://getfirefox.com/");
let title = "Get Firefox";
let bmk_id = PlacesUtils.bookmarks.insertBookmark(parent, uri,
PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!");
let guid = store.GUIDForId(bmk_id);
let record = store.createRecord(guid);
let collection = server.user("foo").collection("bookmarks");
try {
let ping = yield sync_engine_and_validate_telem(engine, false);
ok(!!ping);
equal(ping.engines.length, 1);
equal(ping.engines[0].name, "bookmarks");
ok(!!ping.engines[0].outgoing);
greater(ping.engines[0].outgoing[0].sent, 0)
ok(!ping.engines[0].incoming);
PlacesUtils.bookmarks.setItemTitle(bmk_id, "New Title");
store.wipe();
engine.resetClient();
ping = yield sync_engine_and_validate_telem(engine, false);
equal(ping.engines.length, 1);
equal(ping.engines[0].name, "bookmarks");
equal(ping.engines[0].outgoing.length, 1);
ok(!!ping.engines[0].incoming);
} finally {
// Clean up.
store.wipe();
yield cleanAndGo(server);
}
});
add_task(function *test_upload_failed() {
Service.identity.username = "foo";
let collection = new ServerCollection();
collection._wbos.flying = new ServerWBO('flying');
let server = sync_httpd_setup({
"/1.1/foo/storage/rotary": collection.handler()
});
let syncTesting = new SyncTestingInfrastructure(server);
let engine = new RotaryEngine(Service);
engine.lastSync = 123; // needs to be non-zero so that tracker is queried
engine.lastSyncLocal = 456;
engine._store.items = {
flying: "LNER Class A3 4472",
scotsman: "Flying Scotsman",
peppercorn: "Peppercorn Class"
};
const FLYING_CHANGED = 12345;
const SCOTSMAN_CHANGED = 23456;
const PEPPERCORN_CHANGED = 34567;
engine._tracker.addChangedID("flying", FLYING_CHANGED);
engine._tracker.addChangedID("scotsman", SCOTSMAN_CHANGED);
engine._tracker.addChangedID("peppercorn", PEPPERCORN_CHANGED);
let meta_global = Service.recordManager.set(engine.metaURL, new WBORecord(engine.metaURL));
meta_global.payload.engines = { rotary: { version: engine.version, syncID: engine.syncID } };
try {
engine.enabled = true;
let ping = yield sync_engine_and_validate_telem(engine, true);
ok(!!ping);
equal(ping.engines.length, 1);
equal(ping.engines[0].incoming, null);
deepEqual(ping.engines[0].outgoing, [{ sent: 3, failed: 2 }]);
engine.lastSync = 123;
engine.lastSyncLocal = 456;
ping = yield sync_engine_and_validate_telem(engine, true);
ok(!!ping);
equal(ping.engines.length, 1);
equal(ping.engines[0].incoming.reconciled, 1);
deepEqual(ping.engines[0].outgoing, [{ sent: 2, failed: 2 }]);
} finally {
yield cleanAndGo(server);
}
});
add_task(function *test_sync_partialUpload() {
Service.identity.username = "foo";
let collection = new ServerCollection();
let server = sync_httpd_setup({
"/1.1/foo/storage/rotary": collection.handler()
});
let syncTesting = new SyncTestingInfrastructure(server);
generateNewKeys(Service.collectionKeys);
let engine = new RotaryEngine(Service);
engine.lastSync = 123;
engine.lastSyncLocal = 456;
// Create a bunch of records (and server side handlers)
for (let i = 0; i < 234; i++) {
let id = 'record-no-' + i;
engine._store.items[id] = "Record No. " + i;
engine._tracker.addChangedID(id, i);
// Let two items in the first upload batch fail.
if (i != 23 && i != 42) {
collection.insert(id);
}
}
let meta_global = Service.recordManager.set(engine.metaURL,
new WBORecord(engine.metaURL));
meta_global.payload.engines = {rotary: {version: engine.version,
syncID: engine.syncID}};
try {
engine.enabled = true;
let ping = yield sync_engine_and_validate_telem(engine, true);
ok(!!ping);
ok(!ping.failureReason);
equal(ping.engines.length, 1);
equal(ping.engines[0].name, "rotary");
ok(!ping.engines[0].incoming);
ok(!ping.engines[0].failureReason);
deepEqual(ping.engines[0].outgoing, [{ sent: 234, failed: 2 }]);
collection.post = function() { throw "Failure"; }
engine._store.items["record-no-1000"] = "Record No. 1000";
engine._tracker.addChangedID("record-no-1000", 1000);
collection.insert("record-no-1000", 1000);
engine.lastSync = 123;
engine.lastSyncLocal = 456;
ping = null;
try {
// should throw
yield sync_engine_and_validate_telem(engine, true, errPing => ping = errPing);
} catch (e) {}
// It would be nice if we had a more descriptive error for this...
let uploadFailureError = {
name: "othererror",
error: "error.engine.reason.record_upload_fail"
};
ok(!!ping);
deepEqual(ping.failureReason, uploadFailureError);
equal(ping.engines.length, 1);
equal(ping.engines[0].name, "rotary");
deepEqual(ping.engines[0].incoming, {
failed: 1,
newFailed: 1,
reconciled: 232
});
ok(!ping.engines[0].outgoing);
deepEqual(ping.engines[0].failureReason, uploadFailureError);
} finally {
yield cleanAndGo(server);
}
});
add_task(function* test_generic_engine_fail() {
Service.engineManager.register(SteamEngine);
let engine = Service.engineManager.get("steam");
engine.enabled = true;
let store = engine._store;
let server = serverForUsers({"foo": "password"}, {
meta: {global: {engines: {steam: {version: engine.version,
syncID: engine.syncID}}}},
steam: {}
});
new SyncTestingInfrastructure(server.server);
let e = new Error("generic failure message")
engine._errToThrow = e;
try {
let ping = yield sync_and_validate_telem(true);
equal(ping.status.service, SYNC_FAILED_PARTIAL);
deepEqual(ping.engines.find(e => e.name === "steam").failureReason, {
name: "unexpectederror",
error: String(e)
});
} finally {
Service.engineManager.unregister(engine);
yield cleanAndGo(server);
}
});
add_task(function* test_initial_sync_engines() {
Service.engineManager.register(SteamEngine);
let engine = Service.engineManager.get("steam");
engine.enabled = true;
let store = engine._store;
let engines = {};
// These are the only ones who actually have things to sync at startup.
let engineNames = ["clients", "bookmarks", "prefs", "tabs"];
let conf = { meta: { global: { engines } } };
for (let e of engineNames) {
engines[e] = { version: engine.version, syncID: engine.syncID };
conf[e] = {};
}
let server = serverForUsers({"foo": "password"}, conf);
new SyncTestingInfrastructure(server.server);
try {
let ping = yield wait_for_ping(() => Service.sync(), true);
equal(ping.engines.find(e => e.name === "clients").outgoing[0].sent, 1);
equal(ping.engines.find(e => e.name === "tabs").outgoing[0].sent, 1);
// for the rest we don't care about specifics
for (let e of ping.engines) {
if (!engineNames.includes(engine.name)) {
continue;
}
greaterOrEqual(e.took, 1);
ok(!!e.outgoing)
equal(e.outgoing.length, 1);
notEqual(e.outgoing[0].sent, undefined);
equal(e.outgoing[0].failed, undefined);
}
} finally {
yield cleanAndGo(server);
}
});
add_task(function* test_nserror() {
Service.engineManager.register(SteamEngine);
let engine = Service.engineManager.get("steam");
engine.enabled = true;
let store = engine._store;
let server = serverForUsers({"foo": "password"}, {
meta: {global: {engines: {steam: {version: engine.version,
syncID: engine.syncID}}}},
steam: {}
});
new SyncTestingInfrastructure(server.server);
engine._errToThrow = Components.Exception("NS_ERROR_UNKNOWN_HOST", Cr.NS_ERROR_UNKNOWN_HOST);
try {
let ping = yield sync_and_validate_telem(true);
deepEqual(ping.status, {
service: SYNC_FAILED_PARTIAL,
sync: LOGIN_FAILED_NETWORK_ERROR
});
let enginePing = ping.engines.find(e => e.name === "steam");
deepEqual(enginePing.failureReason, {
name: "nserror",
code: Cr.NS_ERROR_UNKNOWN_HOST
});
} finally {
Service.engineManager.unregister(engine);
yield cleanAndGo(server);
}
});

View File

@ -11,6 +11,7 @@ support-files =
missing-xpi-search.xml
places_v10_from_v11.sqlite
rewrite-search.xml
sync_ping_schema.json
!/services/common/tests/unit/head_helpers.js
!/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@ -185,3 +186,5 @@ support-files = prefs_test_prefs_store.js
# Synced tabs.
[test_syncedtabs.js]
[test_telemetry.js]

7650
testing/modules/ajv-4.1.1.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@ XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
TESTING_JS_MODULES += [
'ajv-4.1.1.js',
'AppData.jsm',
'AppInfo.jsm',
'Assert.jsm',

View File

@ -39,18 +39,16 @@ gEffectEasingTests.forEach(function(options) {
}, options.desc);
});
test(function(t) {
var div = createDiv(t);
var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
assert_throws({ name: 'TypeError' },
function() {
anim.effect.timing.easing = '';
});
assert_throws({ name: 'TypeError' },
function() {
anim.effect.timing.easing = 'test';
});
}, 'Test invalid easing value');
gInvalidEasingTests.forEach(function(options) {
test(function(t) {
var div = createDiv(t);
var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
assert_throws({ name: 'TypeError' },
function() {
anim.effect.timing.easing = options.easing;
});
}, 'Invalid effect easing value test: \'' + options.easing + '\'');
});
test(function(t) {
var delay = 1000 * MS_PER_SEC;

View File

@ -669,5 +669,15 @@ gStepTimingFunctionTests.forEach(function(options) {
}, options.description);
});
gInvalidEasingTests.forEach(function(options) {
test(function(t) {
var div = createDiv(t);
assert_throws({ name: 'TypeError' },
function() {
div.animate({ easing: options.easing }, 100 * MS_PER_SEC);
});
}, 'Invalid keyframe easing value: \'' + options.easing + '\'');
});
</script>
</body>

View File

@ -40,3 +40,30 @@ var gEffectEasingTests = [
easingFunction: cubicBezier(0, 1.5, 1, 1.5)
}
];
var gInvalidEasingTests = [
{
easing: ''
},
{
easing: 'test'
},
{
easing: 'cubic-bezier(1.1, 0, 1, 1)'
},
{
easing: 'cubic-bezier(0, 0, 1.1, 1)'
},
{
easing: 'cubic-bezier(-0.1, 0, 1, 1)'
},
{
easing: 'cubic-bezier(0, 0, -0.1, 1)'
},
{
easing: 'steps(-1, start)'
},
{
easing: 'steps(0.1, start)'
},
];

View File

@ -891,7 +891,10 @@ ExtensionData.prototype = {
}
try {
this.id = this.manifest.applications.gecko.id;
// Do not override the add-on id that has been already assigned.
if (!this.id && this.manifest.applications.gecko.id) {
this.id = this.manifest.applications.gecko.id;
}
} catch (e) {
// Errors are handled by the type checks above.
}

View File

@ -114,6 +114,7 @@ var PrintUtils = {
let mm = aBrowser.messageManager;
mm.sendAsyncMessage("Printing:Print", {
windowID: aWindowID,
simplifiedMode: this._shouldSimplify,
});
},

View File

@ -25,5 +25,6 @@ Client-side, this consists of:
crash-ping
uitour-ping
heartbeat-ping
sync-ping
preferences
crashes

View File

@ -50,6 +50,7 @@ Ping types
* ``upgrade`` - *planned* - sent right after an upgrade
* :doc:`heartbeat-ping` - contains information on Heartbeat surveys
* :doc:`deletion <deletion-ping>` - sent when FHR upload is disabled, requesting deletion of the data associated with this user
* :doc:`sync <sync-ping>` - sent after a sync is completed or fails, contains information on sync errors and performance.
Archiving
=========

View File

@ -0,0 +1,142 @@
"sync" ping
===========
This ping is generated after a sync is completed, for both successful and failed syncs. It's payload contains measurements
pertaining to sync performance and error information. It does not contain the enviroment block, nor the clientId.
A JSON-schema document describing the exact format of the ping's payload property can be found at `services/sync/tests/unit/sync\_ping\_schema.json <https://dxr.mozilla.org/mozilla-central/source/services/sync/tests/unit/sync_ping_schema.json>`_.
Structure::
{
version: 4,
type: "sync",
... common ping data
payload: {
version: 1,
when: <integer milliseconds since epoch>,
took: <integer duration in milliseconds>,
uid: <string>, // FxA unique ID, or empty string.
didLogin: <bool>, // Optional, is this the first sync after login? Excluded if we don't know.
why: <string>, // Optional, why the sync occured, excluded if we don't know.
// Optional, excluded if there was no error.
failureReason: {
name: <string>, // "httperror", "networkerror", "shutdownerror", etc.
code: <integer>, // Only present for "httperror" and "networkerror".
error: <string>, // Only present for "othererror" and "unexpectederror".
from: <string>, // Optional, and only present for "autherror".
},
// Internal sync status information. Omitted if it would be empty.
status: {
sync: <string>, // The value of the Status.sync property, unless it indicates success.
service: <string>, // The value of the Status.service property, unless it indicates success.
},
// Information about each engine's sync.
engines: [
{
name: <string>, // "bookmarks", "tabs", etc.
took: <integer duration in milliseconds>, // Optional, values of 0 are omitted.
status: <string>, // The value of Status.engines, if it holds a non-success value.
// Optional, excluded if all items would be 0. A missing item indicates a value of 0.
incoming: {
applied: <integer>, // Number of records applied
succeeded: <integer>, // Number of records that applied without error
failed: <integer>, // Number of records that failed to apply
newFailed: <integer>, // Number of records that failed for the first time this sync
reconciled: <integer>, // Number of records that were reconciled
},
// Optional, excluded if it would be empty. Records that would be
// empty (e.g. 0 sent and 0 failed) are omitted.
outgoing: [
{
sent: <integer>, // Number of outgoing records sent. Zero values are omitted.
failed: <integer>, // Number that failed to send. Zero values are omitted.
}
],
// Optional, excluded if there were no errors
failureReason: { ... }, // Same as above.
// Optional, excluded if it would be empty or if the engine cannot
// or did not run validation on itself. Entries with a count of 0
// are excluded.
validation: [
{
name: <string>, // The problem identified.
count: <integer>, // Number of times it occurred.
}
]
}
]
}
}
info
----
took
~~~~
These values should be monotonic. If we can't get a monotonic timestamp, -1 will be reported on the payload, and the values will be omitted from the engines. Additionally, the value will be omitted from an engine if it would be 0 (either due to timer inaccuracy or finishing instantaneously).
uid
~~~
This property containing the FxA account identifier, which is provided by the FxA auth server APIs: `<https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md>`_. It may be an empty string in the case that we are unable to authenticate with FxA, and have never authenticated in the past. If present, it should be a 32 character hexidecimal string.
why
~~~
One of the following values:
- ``startup``: This is the first sync triggered after browser startup.
- ``schedule``: This is a sync triggered because it has been too long since the last sync.
- ``score``: This sync is triggered by a high score value one of sync's trackers, indicating that many changes have occurred since the last sync.
- ``user``: The user manually triggered the sync.
- ``tabs``: The user opened the synced tabs sidebar, which triggers a sync.
status
~~~~~~
The ``engine.status``, ``payload.status.sync``, and ``payload.status.service`` properties are sync error codes, which are listed in `services/sync/modules/constants.js <https://dxr.mozilla.org/mozilla-central/source/services/sync/modules/constants.js>`_, and success values are not reported.
failureReason
~~~~~~~~~~~~~
Stores error information, if any is present. Always contains the "name" property, which identifies the type of error it is. The types can be.
- ``httperror``: Indicates that we recieved an HTTP error response code, but are unable to be more specific about the error. Contains the following properties:
- ``code``: Integer HTTP status code.
- ``nserror``: Indicates that an exception with the provided error code caused sync to fail.
- ``code``: The nsresult error code (integer).
- ``shutdownerror``: Indicates that the sync failed because we shut down before completion.
- ``autherror``: Indicates an unrecoverable authentication error.
- ``from``: Where the authentication error occurred, one of the following values: ``tokenserver``, ``fxaccounts``, or ``hawkclient``.
- ``othererror``: Indicates that it is a sync error code that we are unable to give more specific information on. As with the ``syncStatus`` property, it is a sync error code, which are listed in `services/sync/modules/constants.js <https://dxr.mozilla.org/mozilla-central/source/services/sync/modules/constants.js>`_.
- ``error``: String identifying which error was present.
- ``unexpectederror``: Indicates that some other error caused sync to fail, typically an uncaught exception.
- ``error``: The message provided by the error.
engine.name
~~~~~~~~~~~
Third-party engines are not reported, so only the following values are allowed: ``addons``, ``bookmarks``, ``clients``, ``forms``, ``history``, ``passwords``, ``prefs``, and ``tabs``.
engine.validation
~~~~~~~~~~~~~~~~~
For engines that can run validation on themselves, an array of objects describing validation errors that have occurred. Items that would have a count of 0 are excluded. Each engine will have its own set of items that it might put in the ``name`` field, but there are a finite number. See ``BookmarkProblemData.getSummary`` in `services/sync/modules/bookmark\_validator.js <https://dxr.mozilla.org/mozilla-central/source/services/sync/modules/bookmark_validator.js>`_ for an example.

View File

@ -466,7 +466,7 @@ var Printing = {
}
case "Printing:Print": {
this.print(Services.wm.getOuterWindowWithId(data.windowID));
this.print(Services.wm.getOuterWindowWithId(data.windowID), data.simplifiedMode);
break;
}
}
@ -631,9 +631,17 @@ var Printing = {
docShell.printPreview.exitPrintPreview();
},
print(contentWindow) {
print(contentWindow, simplifiedMode) {
let printSettings = this.getPrintSettings();
let rv = Cr.NS_OK;
// If we happen to be on simplified mode, we need to set docURL in order
// to generate header/footer content correctly, since simplified tab has
// "about:blank" as its URI.
if (printSettings && simplifiedMode) {
printSettings.docURL = contentWindow.document.baseURI;
}
try {
let print = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebBrowserPrint);

View File

@ -167,6 +167,8 @@
<field name="_constructed">false</field>
<property name="instantApply">
<getter>
if (this.getAttribute("instantApply") == "false")
return false;
return this.getAttribute("instantApply") == "true" || this.preferences.instantApply;
</getter>
</property>

View File

@ -429,6 +429,14 @@ xul|textbox {
padding-left: 10px;
}
/* Create a separate rule to unset these styles on .tree-input instead of
using :not(.tree-input) so the selector specifity doesn't change. */
xul|textbox.tree-input {
min-height: unset;
padding-right: unset;
padding-left: unset;
}
html|input[type="text"],
html|textarea {
font-family: inherit;

Some files were not shown because too many files have changed in this diff Show More