mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
Merge m-c to inbound, a=merge CLOSED TREE
This commit is contained in:
commit
0dcab503c5
@ -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);
|
||||
});
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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({
|
||||
|
@ -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");
|
||||
|
@ -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
|
||||
|
@ -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}`);
|
||||
|
@ -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.
|
||||
|
@ -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"}
|
||||
|
@ -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()
|
||||
)
|
||||
)
|
||||
);
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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)
|
||||
)
|
||||
);
|
||||
|
@ -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)
|
||||
)
|
||||
);
|
||||
|
@ -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"}
|
||||
|
@ -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
|
||||
}, "}")
|
||||
)
|
||||
);
|
||||
},
|
||||
|
@ -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
|
||||
}, "]")
|
||||
)
|
||||
);
|
||||
},
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
)
|
||||
);
|
||||
},
|
||||
|
@ -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 + "\""
|
||||
)
|
||||
);
|
||||
},
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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
|
||||
}, ">;")
|
||||
)
|
||||
);
|
||||
},
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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]
|
||||
|
@ -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>
|
@ -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");
|
||||
|
||||
|
@ -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");
|
||||
|
@ -247,3 +247,7 @@
|
||||
.theme-firebug #command-button-frames {
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.theme-firebug #element-picker {
|
||||
min-height: 21px;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -6,7 +6,9 @@
|
||||
"use strict";
|
||||
|
||||
const { messages } = require("./messages");
|
||||
const { prefs } = require("./prefs");
|
||||
|
||||
exports.reducers = {
|
||||
messages
|
||||
messages,
|
||||
prefs,
|
||||
};
|
||||
|
@ -6,4 +6,5 @@
|
||||
DevToolsModules(
|
||||
'index.js',
|
||||
'messages.js',
|
||||
'prefs.js',
|
||||
)
|
||||
|
@ -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;
|
@ -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;
|
||||
|
@ -5,4 +5,5 @@
|
||||
|
||||
DevToolsModules(
|
||||
'messages.js',
|
||||
'prefs.js',
|
||||
)
|
||||
|
@ -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;
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
});
|
||||
|
@ -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});
|
||||
};
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
26
dom/animation/test/crashtests/1278485-1.html
Normal file
26
dom/animation/test/crashtests/1278485-1.html
Normal 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>
|
@ -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
|
||||
|
@ -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]
|
||||
|
167
dom/animation/test/mozilla/file_cubic_bezier_limits.html
Normal file
167
dom/animation/test/mozilla/file_cubic_bezier_limits.html
Normal 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>
|
14
dom/animation/test/mozilla/test_cubic_bezier_limits.html
Normal file
14
dom/animation/test/mozilla/test_cubic_bezier_limits.html
Normal 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>
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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',
|
||||
|
@ -145,7 +145,7 @@ this.makeIdentityConfig = function(overrides) {
|
||||
kA: 'kA',
|
||||
kB: 'kB',
|
||||
sessionToken: 'sessionToken',
|
||||
uid: 'user_uid',
|
||||
uid: "a".repeat(32),
|
||||
verified: true,
|
||||
},
|
||||
token: {
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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;
|
||||
},
|
||||
|
432
services/sync/modules/telemetry.js
Normal file
432
services/sync/modules/telemetry.js
Normal 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);
|
@ -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',
|
||||
]
|
||||
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
147
services/sync/tests/unit/sync_ping_schema.json
Normal file
147
services/sync/tests/unit/sync_ping_schema.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
@ -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.");
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
});
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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 = [];
|
||||
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
|
409
services/sync/tests/unit/test_telemetry.js
Normal file
409
services/sync/tests/unit/test_telemetry.js
Normal 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);
|
||||
}
|
||||
});
|
@ -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
7650
testing/modules/ajv-4.1.1.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -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',
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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)'
|
||||
},
|
||||
];
|
||||
|
@ -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.
|
||||
}
|
||||
|
@ -114,6 +114,7 @@ var PrintUtils = {
|
||||
let mm = aBrowser.messageManager;
|
||||
mm.sendAsyncMessage("Printing:Print", {
|
||||
windowID: aWindowID,
|
||||
simplifiedMode: this._shouldSimplify,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -25,5 +25,6 @@ Client-side, this consists of:
|
||||
crash-ping
|
||||
uitour-ping
|
||||
heartbeat-ping
|
||||
sync-ping
|
||||
preferences
|
||||
crashes
|
||||
|
@ -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
|
||||
=========
|
||||
|
142
toolkit/components/telemetry/docs/sync-ping.rst
Normal file
142
toolkit/components/telemetry/docs/sync-ping.rst
Normal 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.
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user