author | Carsten "Tomcat" Book <cbook@mozilla.com> |
Wed, 07 Oct 2015 11:44:06 +0200 | |
changeset 266347 | d1c5a7c5b4331ee9ea5443de893fcfd0a5b80e2a |
parent 266334 | eb95242f2414246cf281041ccc70df936702cb9b (current diff) |
parent 266346 | 624f967efe43130cf4a74bebad06179620be5317 (diff) |
child 266348 | bfb9436756a0df8078460bd3520ebc9dde3dbe68 |
push id | 29488 |
push user | cbook@mozilla.com |
push date | Wed, 07 Oct 2015 09:48:25 +0000 |
treeherder | mozilla-central@d1c5a7c5b433 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 44.0a1 |
first release with | nightly linux32
d1c5a7c5b433
/
44.0a1
/
20151007030205
/
files
nightly linux64
d1c5a7c5b433
/
44.0a1
/
20151007030205
/
files
nightly mac
d1c5a7c5b433
/
44.0a1
/
20151007030205
/
files
nightly win32
d1c5a7c5b433
/
44.0a1
/
20151007030205
/
files
nightly win64
d1c5a7c5b433
/
44.0a1
/
20151007030205
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
44.0a1
/
20151007030205
/
pushlog to previous
nightly linux64
44.0a1
/
20151007030205
/
pushlog to previous
nightly mac
44.0a1
/
20151007030205
/
pushlog to previous
nightly win32
44.0a1
/
20151007030205
/
pushlog to previous
nightly win64
44.0a1
/
20151007030205
/
pushlog to previous
|
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1374,17 +1374,17 @@ pref("devtools.inspector.imagePreviewToo // Enable user agent style inspection in rule-view pref("devtools.inspector.showUserAgentStyles", false); // Show all native anonymous content (like controls in <video> tags) pref("devtools.inspector.showAllAnonymousContent", false); // Enable the MDN docs tooltip pref("devtools.inspector.mdnDocsTooltip.enabled", true); // DevTools default color unit -pref("devtools.defaultColorUnit", "hex"); +pref("devtools.defaultColorUnit", "authored"); // Enable the Responsive UI tool pref("devtools.responsiveUI.no-reload-notification", false); // Enable the Debugger pref("devtools.debugger.enabled", true); pref("devtools.debugger.chrome-debugging-host", "localhost"); pref("devtools.debugger.chrome-debugging-port", 6080);
--- a/browser/base/content/social-content.js +++ b/browser/base/content/social-content.js @@ -80,16 +80,29 @@ SocialErrorListener = { failure = aStatus != Components.results.NS_OK; } } } // Calling cancel() will raise some OnStateChange notifications by itself, // so avoid doing that more than once if (failure && aStatus != Components.results.NS_BINDING_ABORTED) { + // if tp is enabled and we get a failure, ignore failures (ie. STATE_STOP) + // on child resources since they *may* have been blocked. We don't have an + // easy way to know if a particular url is blocked by TP, only that + // something was. + if (docShell.hasTrackingContentBlocked) { + let frame = docShell.chromeEventHandler; + let src = frame.getAttribute("src"); + if (aRequest && aRequest.name != src) { + Cu.reportError("SocialErrorListener ignoring blocked content error for " + aRequest.name); + return; + } + } + aRequest.cancel(Components.results.NS_BINDING_ABORTED); this.setErrorPage(); } }, onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) { aRequest.cancel(Components.results.NS_BINDING_ABORTED);
--- a/browser/components/loop/content/shared/css/conversation.css +++ b/browser/components/loop/content/shared/css/conversation.css @@ -264,16 +264,17 @@ html[dir="rtl"] .conversation-toolbar-bt } .call-action-group .btn-group-chevron, .call-action-group .btn-group { width: 100%; } .call-action-group > .invite-button { + cursor: pointer; margin: 0 4px; position: relative; } .call-action-group > .invite-button > img { background-color: #00a9dc; border-radius: 100%; height: 28px;
--- a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd @@ -64,16 +64,20 @@ values from browser.dtd. --> - This label is visible in the options panel. --> <!ENTITY options.defaultColorUnit.label "Default color unit"> <!-- LOCALIZATION NOTE (options.defaultColorUnit.accesskey): This is the access - key for a dropdown list that controls the default color unit used in the - inspector. This is visible in the options panel. --> <!ENTITY options.defaultColorUnit.accesskey "U"> +<!-- LOCALIZATION NOTE (options.defaultColorUnit.authored): This is used in the + - 'Default color unit' dropdown list and is visible in the options panel. --> +<!ENTITY options.defaultColorUnit.authored "As Authored"> + <!-- LOCALIZATION NOTE (options.defaultColorUnit.hex): This is used in the - 'Default color unit' dropdown list and is visible in the options panel. --> <!ENTITY options.defaultColorUnit.hex "Hex"> <!-- LOCALIZATION NOTE (options.defaultColorUnit.hsl): This is used in the - 'Default color unit' dropdown list and is visible in the options panel. --> <!ENTITY options.defaultColorUnit.hsl "HSL(A)">
--- a/devtools/client/animationinspector/animation-controller.js +++ b/devtools/client/animationinspector/animation-controller.js @@ -84,17 +84,19 @@ var getServerTraits = Task.async(functio method: "setCurrentTime" }, { name: "hasMutationEvents", actor: "animations", method: "stopAnimationPlayerUpdates" }, { name: "hasSetPlaybackRate", actor: "animationplayer", method: "setPlaybackRate" }, { name: "hasTargetNode", actor: "domwalker", method: "getNodeFromActor" }, { name: "hasSetCurrentTimes", actor: "animations", - method: "setCurrentTimes" } + method: "setCurrentTimes" }, + { name: "hasGetFrames", actor: "animationplayer", + method: "getFrames" } ]; let traits = {}; for (let {name, actor, method} of config) { traits[name] = yield target.actorHasMethod(actor, method); } return traits;
--- a/devtools/client/framework/toolbox-options.xul +++ b/devtools/client/framework/toolbox-options.xul @@ -50,16 +50,17 @@ <description> <label control="defaultColorUnitMenuList" accesskey="&options.defaultColorUnit.accesskey;" >&options.defaultColorUnit.label;</label> <hbox> <menulist id="defaultColorUnitMenuList" data-pref="devtools.defaultColorUnit"> <menupopup> + <menuitem label="&options.defaultColorUnit.authored;" value="authored"/> <menuitem label="&options.defaultColorUnit.hex;" value="hex"/> <menuitem label="&options.defaultColorUnit.hsl;" value="hsl"/> <menuitem label="&options.defaultColorUnit.rgb;" value="rgb"/> <menuitem label="&options.defaultColorUnit.name;" value="name"/> </menupopup> </menulist> </hbox> </description>
--- a/devtools/client/layoutview/view.js +++ b/devtools/client/layoutview/view.js @@ -90,37 +90,37 @@ EditingSession.prototype = { for (let property of properties) { if (!this._modifications.has(property.name)) { this._modifications.set(property.name, this.getPropertyFromRule(this._rules[0], property.name)); } if (property.value == "") { - modifications.removeProperty(property.name); + modifications.removeProperty(-1, property.name); } else { - modifications.setProperty(property.name, property.value, ""); + modifications.setProperty(-1, property.name, property.value, ""); } } return modifications.apply().then(null, console.error); }, /** * Reverts all of the property changes made by this instance. Returns a * promise that will be resolved when complete. */ revert: function() { let modifications = this._rules[0].startModifyingProperties(); for (let [property, value] of this._modifications) { if (value != "") { - modifications.setProperty(property, value, ""); + modifications.setProperty(-1, property, value, ""); } else { - modifications.removeProperty(property); + modifications.removeProperty(-1, property); } } return modifications.apply().then(null, console.error); }, destroy: function() { this._doc = null;
--- a/devtools/client/shared/test/browser_filter-editor-01.js +++ b/devtools/client/shared/test/browser_filter-editor-01.js @@ -13,24 +13,24 @@ add_task(function *() { let [host, win, doc] = yield createHost("bottom", TEST_URI); const container = doc.querySelector("#container"); let widget = new CSSFilterEditorWidget(container, "none"); info("Test parsing of a valid CSS Filter value"); widget.setCssValue("blur(2px) contrast(200%)"); is(widget.getCssValue(), - "blur(2px) contrast(200%)", "setCssValue should work for computed values"); + "blur(2px) contrast(200%)", + "setCssValue should work for computed values"); info("Test parsing of space-filled value"); widget.setCssValue("blur( 2px ) contrast( 2 )"); is(widget.getCssValue(), - "blur(2px) contrast(200%)", "setCssValue should work for spaced values"); + "blur(2px) contrast(200%)", + "setCssValue should work for spaced values"); info("Test parsing of string-typed values"); widget.setCssValue("drop-shadow( 2px 1px 5px black) url( example.svg#filter )"); - const computedURI = - "chrome://devtools/content/shared/widgets/example.svg#filter"; - const expected = `drop-shadow(rgb(0, 0, 0) 2px 1px 5px) url(${computedURI})`; - is(widget.getCssValue(), expected, + is(widget.getCssValue(), + "drop-shadow(2px 1px 5px black) url(example.svg#filter)", "setCssValue should work for string-typed values"); });
--- a/devtools/client/shared/test/browser_filter-editor-02.js +++ b/devtools/client/shared/test/browser_filter-editor-02.js @@ -32,27 +32,27 @@ add_task(function*() { }, { label: "hue-rotate", value: "20.2", unit: "deg" }, { label: "drop-shadow", - value: "rgb(0, 0, 0) 5px 5px 0px", + value: "5px 5px black", unit: null } ] }, { cssValue: "url(example.svg)", expected: [ { label: "url", - value: "chrome://devtools/content/shared/widgets/example.svg", + value: "example.svg", unit: null } ] }, { cssValue: "none", expected: [] }
--- a/devtools/client/shared/test/browser_filter-editor-05.js +++ b/devtools/client/shared/test/browser_filter-editor-05.js @@ -18,56 +18,59 @@ const GRAYSCALE_MAX = 100, add_task(function*() { yield promiseTab("about:blank"); let [host, win, doc] = yield createHost("bottom", TEST_URI); const container = doc.querySelector("#container"); let widget = new CSSFilterEditorWidget(container, "grayscale(0%) url(test.svg)"); const filters = widget.el.querySelector("#filters"); - const grayscale = filters.children[0], - url = filters.children[1]; + const grayscale = filters.children[0]; + const url = filters.children[1]; info("Test label-dragging on number-type filters without modifiers"); widget._mouseDown({ target: grayscale.querySelector("label"), pageX: 0, altKey: false, shiftKey: false }); widget._mouseMove({ pageX: 12, altKey: false, shiftKey: false }); let expected = DEFAULT_VALUE_MULTIPLIER * 12; - is(widget.getValueAt(0), `${expected}%`, + is(widget.getValueAt(0), + `${expected}%`, "Should update value correctly without modifiers"); info("Test label-dragging on number-type filters with alt"); widget._mouseMove({ pageX: 20, // 20 - 12 = 8 altKey: true, shiftKey: false }); expected = expected + SLOW_VALUE_MULTIPLIER * 8; - is(widget.getValueAt(0), `${expected}%`, + is(widget.getValueAt(0), + `${expected}%`, "Should update value correctly with alt key"); info("Test label-dragging on number-type filters with shift"); widget._mouseMove({ pageX: 25, // 25 - 20 = 5 altKey: false, shiftKey: true }); expected = expected + FAST_VALUE_MULTIPLIER * 5; - is(widget.getValueAt(0), `${expected}%`, + is(widget.getValueAt(0), + `${expected}%`, "Should update value correctly with shift key"); info("Test releasing mouse and dragging again"); widget._mouseUp(); widget._mouseDown({ target: grayscale.querySelector("label"), @@ -78,39 +81,42 @@ add_task(function*() { widget._mouseMove({ pageX: 5, altKey: false, shiftKey: false }); expected = expected + DEFAULT_VALUE_MULTIPLIER * 5; - is(widget.getValueAt(0), `${expected}%`, + is(widget.getValueAt(0), + `${expected}%`, "Should reset multiplier to default"); info("Test value ranges"); widget._mouseMove({ pageX: 30, // 30 - 25 = 5 altKey: false, shiftKey: true }); expected = GRAYSCALE_MAX; - is(widget.getValueAt(0), `${expected}%`, + is(widget.getValueAt(0), + `${expected}%`, "Shouldn't allow values higher than max"); widget._mouseMove({ pageX: -11, altKey: false, shiftKey: true }); expected = GRAYSCALE_MIN; - is(widget.getValueAt(0), `${expected}%`, + is(widget.getValueAt(0), + `${expected}%`, "Shouldn't allow values less than min"); widget._mouseUp(); info("Test label-dragging on string-type filters"); widget._mouseDown({ target: url.querySelector("label"), pageX: 0, @@ -122,11 +128,12 @@ add_task(function*() { "Label-dragging should not work for string-type filters"); widget._mouseMove({ pageX: -11, altKey: false, shiftKey: true }); - is(widget.getValueAt(1), "chrome://devtools/content/shared/widgets/test.svg", + is(widget.getValueAt(1), + "test.svg", "Label-dragging on string-type filters shouldn't affect their value"); });
--- a/devtools/client/shared/test/browser_outputparser.js +++ b/devtools/client/shared/test/browser_outputparser.js @@ -28,82 +28,78 @@ function* performTest() { // Class name used in color swatch. var COLOR_TEST_CLASS = "test-class"; // Create a new CSS color-parsing test. |name| is the name of the CSS // property. |value| is the CSS text to use. |segments| is an array // describing the expected result. If an element of |segments| is a // string, it is simply appended to the expected string. Otherwise, -// it must be an object with a |value| property and a |name| property. -// These describe the color and are both used in the generated -// expected output -- |name| is the color name as it appears in the -// input (e.g., "red"); and |value| is the hash-style numeric value -// for the color, which parseCssProperty emits in some spots (e.g., -// "#F00"). +// it must be an object with a |value| property, which is the color +// name as it appears in the input. // // This approach is taken to reduce boilerplate and to make it simpler // to modify the test when the parseCssProperty output changes. function makeColorTest(name, value, segments) { let result = { name, value, expected: "" }; for (let segment of segments) { if (typeof (segment) === "string") { result.expected += segment; } else { - result.expected += "<span data-color=\"" + segment.value + "\">" + + result.expected += "<span data-color=\"" + segment.name + "\">" + "<span style=\"background-color:" + segment.name + "\" class=\"" + COLOR_TEST_CLASS + "\"></span><span>" + - segment.value + "</span></span>"; + segment.name + "</span></span>"; } } result.desc = "Testing " + name + ": " + value; return result; } function testParseCssProperty(doc, parser) { let tests = [ makeColorTest("border", "1px solid red", - ["1px solid ", {name: "red", value: "#F00"}]), + ["1px solid ", {name: "red"}]), makeColorTest("background-image", "linear-gradient(to right, #F60 10%, rgba(0,0,0,1))", - ["linear-gradient(to right, ", {name: "#F60", value: "#F60"}, - " 10%, ", {name: "rgba(0,0,0,1)", value: "#000"}, + ["linear-gradient(to right, ", {name: "#F60"}, + " 10%, ", {name: "rgba(0,0,0,1)"}, ")"]), // In "arial black", "black" is a font, not a color. makeColorTest("font-family", "arial black", ["arial black"]), makeColorTest("box-shadow", "0 0 1em red", - ["0 0 1em ", {name: "red", value: "#F00"}]), + ["0 0 1em ", {name: "red"}]), makeColorTest("box-shadow", "0 0 1em red, 2px 2px 0 0 rgba(0,0,0,.5)", - ["0 0 1em ", {name: "red", value: "#F00"}, + ["0 0 1em ", {name: "red"}, ", 2px 2px 0 0 ", - {name: "rgba(0,0,0,.5)", value: "rgba(0,0,0,.5)"}]), + {name: "rgba(0,0,0,.5)"}]), makeColorTest("content", "\"red\"", ["\"red\""]), // Invalid property names should not cause exceptions. makeColorTest("hellothere", "'red'", ["'red'"]), makeColorTest("filter", "blur(1px) drop-shadow(0 0 0 blue) url(red.svg#blue)", ["<span data-filters=\"blur(1px) drop-shadow(0 0 0 blue) ", "url(red.svg#blue)\"><span>", "blur(1px) drop-shadow(0 0 0 ", - {name: "blue", value: "#00F"}, + {name: "blue"}, ") url(red.svg#blue)</span></span>"]), makeColorTest("color", "currentColor", ["currentColor"]), ]; let target = doc.querySelector("div"); ok(target, "captain, we have the div");
--- a/devtools/client/shared/widgets/FilterWidget.js +++ b/devtools/client/shared/widgets/FilterWidget.js @@ -702,24 +702,17 @@ CSSFilterEditorWidget.prototype = { this.filters = []; if (cssValue === "none") { this.emit("updated", this.getCssValue()); this.render(); return; } - // Apply filter to a temporary element - // and get the computed value to make parsing - // easier - let tmp = this.doc.createElement("i"); - tmp.style.filter = cssValue; - const computedValue = this.win.getComputedStyle(tmp).filter; - - for (let {name, value} of tokenizeComputedFilter(computedValue)) { + for (let {name, value} of tokenizeFilterValue(cssValue)) { this.add(name, value); } this.emit("updated", this.getCssValue()); this.render(); }, /** @@ -870,29 +863,22 @@ function fixFloat(a, number) { * @param {Number} b * index of second element */ function swapArrayIndices(array, a, b) { array[a] = array.splice(b, 1, array[a])[0]; } /** - * Tokenizes CSS Filter value and returns an array of {name, value} pairs - * - * This is only a very simple tokenizer that only works its way through - * parenthesis in the string to detect function names and values. - * It assumes that the string actually is a well-formed filter value - * (e.g. "blur(2px) hue-rotate(100deg)"). - * - * @param {String} css - * CSS Filter value to be parsed - * @return {Array} - * An array of {name, value} pairs - */ -function tokenizeComputedFilter(css) { + * Tokenizes a CSS Filter value and returns an array of {name, value} pairs. + * + * @param {String} css CSS Filter value to be parsed + * @return {Array} An array of {name, value} pairs + */ +function tokenizeFilterValue(css) { let filters = []; let depth = 0; if (css === "none") { return filters; } let state = "initial"; @@ -902,27 +888,27 @@ function tokenizeComputedFilter(css) { switch (state) { case "initial": if (token.tokenType === "function") { name = token.text; contents = ""; state = "function"; depth = 1; } else if (token.tokenType === "url" || token.tokenType === "bad_url") { - filters.push({name: "url", value: token.text}); + filters.push({name: "url", value: token.text.trim()}); // Leave state as "initial" because the URL token includes // the trailing close paren. } break; case "function": if (token.tokenType === "symbol" && token.text === ")") { --depth; if (depth === 0) { - filters.push({name: name, value: contents}); + filters.push({name: name, value: contents.trim()}); state = "initial"; break; } } contents += css.substring(token.startOffset, token.endOffset); if (token.tokenType === "function" || (token.tokenType === "symbol" && token.text === "(")) { ++depth;
--- a/devtools/client/shared/widgets/Tooltip.js +++ b/devtools/client/shared/widgets/Tooltip.js @@ -1140,16 +1140,18 @@ SwatchColorPickerTooltip.prototype = Her * color. */ show: function() { // Call then parent class' show function SwatchBasedEditorTooltip.prototype.show.call(this); // Then set spectrum's color and listen to color changes to preview them if (this.activeSwatch) { this.currentSwatchColor = this.activeSwatch.nextSibling; + this._colorUnit = + colorUtils.classifyColor(this.currentSwatchColor.textContent); let color = this.activeSwatch.style.backgroundColor; this.spectrum.then(spectrum => { spectrum.off("changed", this._onSpectrumColorChange); spectrum.rgb = this._colorToRgba(color); spectrum.on("changed", this._onSpectrumColorChange); spectrum.updateUI(); }); } @@ -1217,16 +1219,17 @@ SwatchColorPickerTooltip.prototype = Her _colorToRgba: function(color) { color = new colorUtils.CssColor(color); let rgba = color._getRGBATuple(); return [rgba.r, rgba.g, rgba.b, rgba.a]; }, _toDefaultType: function(color) { let colorObj = new colorUtils.CssColor(color); + colorObj.colorUnit = this._colorUnit; return colorObj.toString(); }, destroy: function() { SwatchBasedEditorTooltip.prototype.destroy.call(this); this.currentSwatchColor = null; this.spectrum.then(spectrum => { spectrum.off("changed", this._onSpectrumColorChange);
--- a/devtools/client/styleeditor/StyleSheetEditor.jsm +++ b/devtools/client/styleeditor/StyleSheetEditor.jsm @@ -81,16 +81,22 @@ function StyleSheetEditor(styleSheet, wi this.styleSheet = styleSheet; this._inputElement = null; this.sourceEditor = null; this._window = win; this._isNew = isNew; this.walker = walker; this.highlighter = highlighter; + // True when we've called update() on the style sheet. + this._isUpdating = false; + // True when we've just set the editor text based on a style-applied + // event from the StyleSheetActor. + this._justSetText = false; + this._state = { // state to use when inputElement attaches text: "", selection: { start: {line: 0, ch: 0}, end: {line: 0, ch: 0} } }; @@ -98,32 +104,34 @@ function StyleSheetEditor(styleSheet, wi if (styleSheet.href && Services.io.extractScheme(this.styleSheet.href) == "file") { this._styleSheetFilePath = this.styleSheet.href; } this._onPropertyChange = this._onPropertyChange.bind(this); this._onError = this._onError.bind(this); this._onMediaRuleMatchesChange = this._onMediaRuleMatchesChange.bind(this); - this._onMediaRulesChanged = this._onMediaRulesChanged.bind(this) + this._onMediaRulesChanged = this._onMediaRulesChanged.bind(this); + this._onStyleApplied = this._onStyleApplied.bind(this); this.checkLinkedFileForChanges = this.checkLinkedFileForChanges.bind(this); this.markLinkedFileBroken = this.markLinkedFileBroken.bind(this); this.saveToFile = this.saveToFile.bind(this); this.updateStyleSheet = this.updateStyleSheet.bind(this); this._updateStyleSheet = this._updateStyleSheet.bind(this); this._onMouseMove = this._onMouseMove.bind(this); this._focusOnSourceEditorReady = false; this.cssSheet.on("property-change", this._onPropertyChange); this.styleSheet.on("error", this._onError); this.mediaRules = []; if (this.cssSheet.getMediaRules) { this.cssSheet.getMediaRules().then(this._onMediaRulesChanged, Cu.reportError); } this.cssSheet.on("media-rules-changed", this._onMediaRulesChanged); + this.cssSheet.on("style-applied", this._onStyleApplied); this.savedFile = file; this.linkCSSFile(); } StyleSheetEditor.prototype = { /** * Whether there are unsaved changes in the editor */ @@ -240,35 +248,48 @@ StyleSheetEditor.prototype = { OS.File.stat(path).then((info) => { this._fileModDate = info.lastModificationDate.getTime(); }, this.markLinkedFileBroken); this.emit("linked-css-file"); }, /** + * A helper function that fetches the source text from the style + * sheet. The text is possibly prettified using + * CssLogic.prettifyCSS. This also sets |this._state.text| to the + * new text. + * + * @return {Promise} a promise that resolves to the new text + */ + _getSourceTextAndPrettify: function() { + return this.styleSheet.getText().then((longStr) => { + return longStr.string(); + }).then((source) => { + let ruleCount = this.styleSheet.ruleCount; + if (!this.styleSheet.isOriginalSource) { + source = CssLogic.prettifyCSS(source, ruleCount); + } + this._state.text = source; + return source; + }); + }, + + /** * Start fetching the full text source for this editor's sheet. * * @return {Promise} * A promise that'll resolve with the source text once the source * has been loaded or reject on unexpected error. */ - fetchSource: function () { - return Task.spawn(function* () { - let longStr = yield this.styleSheet.getText(); - let source = yield longStr.string(); - let ruleCount = this.styleSheet.ruleCount; - if (!this.styleSheet.isOriginalSource) { - source = CssLogic.prettifyCSS(source, ruleCount); - } - this._state.text = source; + fetchSource: function() { + return this._getSourceTextAndPrettify().then((source) => { this.sourceLoaded = true; - return source; - }.bind(this)).then(null, e => { + }).then(null, e => { if (this._isDestroyed) { console.warn("Could not fetch the source for " + this.styleSheet.href + ", the editor was destroyed"); Cu.reportError(e); } else { this.emit("error", { key: LOAD_ERROR, append: this.styleSheet.href }); throw e; @@ -319,16 +340,36 @@ StyleSheetEditor.prototype = { * @param {string} property * Property that has changed on sheet */ _onPropertyChange: function(property, value) { this.emit("property-change", property, value); }, /** + * Called when the stylesheet text changes. + */ + _onStyleApplied: function() { + if (this._isUpdating) { + // We just applied an edit in the editor, so we can drop this + // notification. + this._isUpdating = false; + } else if (this.sourceEditor) { + this._getSourceTextAndPrettify().then((newText) => { + this._justSetText = true; + let firstLine = this.sourceEditor.getFirstVisibleLine(); + let pos = this.sourceEditor.getCursor(); + this.sourceEditor.setText(newText); + this.sourceEditor.setFirstVisibleLine(firstLine); + this.sourceEditor.setCursor(pos); + }); + } + }, + + /** * Handles changes to the list of @media rules in the stylesheet. * Emits 'media-rules-changed' if the list has changed. * * @param {array} rules * Array of MediaRuleFronts for new media rules of sheet. */ _onMediaRulesChanged: function(rules) { if (!rules.length && !this.mediaRules.length) { @@ -484,25 +525,31 @@ StyleSheetEditor.prototype = { /** * Update live style sheet according to modifications. */ _updateStyleSheet: function() { if (this.styleSheet.disabled) { return; // TODO: do we want to do this? } + if (this._justSetText) { + this._justSetText = false; + return; + } + this._updateTask = null; // reset only if we actually perform an update // (stylesheet is enabled) so that 'missed' updates // while the stylesheet is disabled can be performed // when it is enabled back. @see enableStylesheet if (this.sourceEditor) { this._state.text = this.sourceEditor.getText(); } + this._isUpdating = true; this.styleSheet.update(this._state.text, this.transitionsEnabled) .then(null, Cu.reportError); }, /** * Handle mousemove events, calling _highlightSelectorAt after a delay only * and reseting the delay everytime. */ @@ -721,20 +768,21 @@ StyleSheetEditor.prototype = { if (this.highlighter && this.walker && this._sourceEditor.container) { this._sourceEditor.container.removeEventListener("mousemove", this._onMouseMove); } this._sourceEditor.destroy(); } this.cssSheet.off("property-change", this._onPropertyChange); this.cssSheet.off("media-rules-changed", this._onMediaRulesChanged); + this.cssSheet.off("style-applied", this._onStyleApplied); this.styleSheet.off("error", this._onError); this._isDestroyed = true; } -} +}; /** * Find a path on disk for a file given it's hosted uri, the uri of the * original resource that generated it (e.g. Sass file), and the location of the * local file for that source. * * @param {nsIURI} uri * The uri of the resource
--- a/devtools/client/styleeditor/test/browser.ini +++ b/devtools/client/styleeditor/test/browser.ini @@ -43,16 +43,17 @@ support-files = sourcemaps-large.html sourcemaps-watching.html test_private.css test_private.html doc_long.css doc_uncached.css doc_uncached.html doc_xulpage.xul + sync.html [browser_styleeditor_autocomplete.js] [browser_styleeditor_autocomplete-disabled.js] [browser_styleeditor_bug_740541_iframes.js] [browser_styleeditor_bug_851132_middle_click.js] [browser_styleeditor_bug_870339.js] [browser_styleeditor_cmd_edit.js] skip-if = e10s # Bug 1055333 - style editor tests disabled with e10s @@ -78,10 +79,15 @@ skip-if = e10s # Bug 1055333 - style edi [browser_styleeditor_scroll.js] [browser_styleeditor_sv_keynav.js] [browser_styleeditor_sv_resize.js] [browser_styleeditor_selectstylesheet.js] [browser_styleeditor_sourcemaps.js] [browser_styleeditor_sourcemap_large.js] [browser_styleeditor_sourcemap_watching.js] skip-if = e10s # Bug 1055333 - style editor tests disabled with e10s +[browser_styleeditor_sync.js] +[browser_styleeditor_syncAddRule.js] +[browser_styleeditor_syncAlreadyOpen.js] +[browser_styleeditor_syncEditSelector.js] +[browser_styleeditor_syncIntoRuleView.js] [browser_styleeditor_transition_rule.js] [browser_styleeditor_xul.js]
new file mode 100644 --- /dev/null +++ b/devtools/client/styleeditor/test/browser_styleeditor_sync.js @@ -0,0 +1,74 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that changes in the style inspector are synchronized into the +// style editor. + +Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleinspector/test/head.js", this); + +const TESTCASE_URI = TEST_BASE_HTTP + "sync.html"; + +const expectedText = ` + body { + border-width: 15px; + /*! color: red; */ + } + + #testid { + /*! font-size: 4em; */ + } + `; + +function* closeAndReopenToolbox() { + let target = TargetFactory.forTab(gBrowser.selectedTab); + yield gDevTools.closeToolbox(target); + let { ui: newui } = yield openStyleEditor(); + return newui; +} + +add_task(function*() { + yield addTab(TESTCASE_URI); + let { inspector, view } = yield openRuleView(); + yield selectNode("#testid", inspector); + let ruleEditor = getRuleViewRuleEditor(view, 1); + + // Disable the "font-size" property. + let propEditor = ruleEditor.rule.textProps[0].editor; + let onModification = view.once("ruleview-changed"); + propEditor.enable.click(); + yield onModification; + + // Disable the "color" property. Note that this property is in a + // rule that also contains a non-inherited property -- so this test + // is also testing that property editing works properly in this + // situation. + ruleEditor = getRuleViewRuleEditor(view, 3); + propEditor = ruleEditor.rule.textProps[1].editor; + onModification = view.once("ruleview-changed"); + propEditor.enable.click(); + yield onModification; + + let { ui } = yield openStyleEditor(); + + let editor = yield ui.editors[0].getSourceEditor(); + let text = editor.sourceEditor.getText(); + is(text, expectedText, "style inspector changes are synced"); + + // Close and reopen the toolbox, to see that the edited text remains + // available. + ui = yield closeAndReopenToolbox(); + editor = yield ui.editors[0].getSourceEditor(); + text = editor.sourceEditor.getText(); + is(text, expectedText, "changes remain after close and reopen"); + + // For the time being, the actor does not update the style's owning + // node's textContent. See bug 1205380. + yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() { + let style = content.document.querySelector("style"); + return style.textContent; + }).then((textContent) => { + isnot(textContent, expectedText, "changes not written back to style node"); + }); +});
new file mode 100644 --- /dev/null +++ b/devtools/client/styleeditor/test/browser_styleeditor_syncAddRule.js @@ -0,0 +1,33 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that adding a new rule is synced to the style editor. + +Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleinspector/test/head.js", this); + +const TESTCASE_URI = TEST_BASE_HTTP + "sync.html"; + +const expectedText = ` +#testid { +}`; + +add_task(function*() { + yield addTab(TESTCASE_URI); + let { inspector, view } = yield openRuleView(); + yield selectNode("#testid", inspector); + + let onRuleViewChanged = once(view, "ruleview-changed"); + view.addRuleButton.click(); + yield onRuleViewChanged; + + let { ui } = yield openStyleEditor(); + + info("Selecting the second editor"); + yield ui.selectStyleSheet(ui.editors[1].styleSheet); + + let editor = ui.editors[1]; + let text = editor.sourceEditor.getText(); + is(text, expectedText, "selector edits are synced"); +});
new file mode 100644 --- /dev/null +++ b/devtools/client/styleeditor/test/browser_styleeditor_syncAlreadyOpen.js @@ -0,0 +1,52 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that changes in the style inspector are synchronized into the +// style editor. + +Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleinspector/test/head.js", this); + +const TESTCASE_URI = TEST_BASE_HTTP + "sync.html"; + +const expectedText = ` + body { + border-width: 15px; + color: red; + } + + #testid { + /*! font-size: 4em; */ + } + `; + +add_task(function*() { + yield addTab(TESTCASE_URI); + + let { inspector, view } = yield openRuleView(); + + // In this test, make sure the style editor is open before making + // changes in the inspector. + let { ui } = yield openStyleEditor(); + let editor = yield ui.editors[0].getSourceEditor(); + + let onEditorChange = promise.defer(); + editor.sourceEditor.on("change", onEditorChange.resolve); + + yield openRuleView(); + yield selectNode("#testid", inspector); + let ruleEditor = getRuleViewRuleEditor(view, 1); + + // Disable the "font-size" property. + let propEditor = ruleEditor.rule.textProps[0].editor; + let onModification = view.once("ruleview-changed"); + propEditor.enable.click(); + yield onModification; + + yield openStyleEditor(); + yield onEditorChange.promise; + + let text = editor.sourceEditor.getText(); + is(text, expectedText, "style inspector changes are synced"); +});
new file mode 100644 --- /dev/null +++ b/devtools/client/styleeditor/test/browser_styleeditor_syncEditSelector.js @@ -0,0 +1,41 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that changes in the style inspector are synchronized into the +// style editor. + +Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleinspector/test/head.js", this); + +const TESTCASE_URI = TEST_BASE_HTTP + "sync.html"; + +const expectedText = ` + body { + border-width: 15px; + color: red; + } + + #testid, span { + font-size: 4em; + } + `; + +add_task(function*() { + yield addTab(TESTCASE_URI); + let { inspector, view } = yield openRuleView(); + yield selectNode("#testid", inspector); + let ruleEditor = getRuleViewRuleEditor(view, 1); + + let editor = yield focusEditableField(view, ruleEditor.selectorText); + editor.input.value = "#testid, span"; + let onRuleViewChanged = once(view, "ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + let { ui } = yield openStyleEditor(); + + editor = yield ui.editors[0].getSourceEditor(); + let text = editor.sourceEditor.getText(); + is(text, expectedText, "selector edits are synced"); +});
new file mode 100644 --- /dev/null +++ b/devtools/client/styleeditor/test/browser_styleeditor_syncIntoRuleView.js @@ -0,0 +1,51 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that changes in the style editor are synchronized into the +// style inspector. + +Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleinspector/test/head.js", this); + +const TEST_URI = ` + <style type='text/css'> + div { background-color: seagreen; } + </style> + <div id='testid' class='testclass'>Styled Node</div> +`; + +const TESTCASE_CSS_SOURCE = "#testid { color: chartreuse; }"; + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + let { panel, ui } = yield openStyleEditor(); + + let editor = yield ui.editors[0].getSourceEditor(); + + let waitForRuleView = view.once("ruleview-refreshed"); + yield typeInEditor(editor, panel.panelWindow); + yield waitForRuleView; + + let value = getRuleViewPropertyValue(view, "#testid", "color"); + is(value, "chartreuse", "check that edits were synced to rule view"); +}); + +function typeInEditor(aEditor, panelWindow) { + let deferred = promise.defer(); + + waitForFocus(function() { + for (let c of TESTCASE_CSS_SOURCE) { + EventUtils.synthesizeKey(c, {}, panelWindow); + } + ok(aEditor.unsaved, "new editor has unsaved flag"); + + deferred.resolve(); + }, panelWindow); + + return deferred.promise; +}
--- a/devtools/client/styleeditor/test/head.js +++ b/devtools/client/styleeditor/test/head.js @@ -66,27 +66,39 @@ function* cleanup() let target = TargetFactory.forTab(gBrowser.selectedTab); yield gDevTools.closeToolbox(target); gBrowser.removeCurrentTab(); } } /** + * Open the style editor for the current tab. + */ +var openStyleEditor = Task.async(function*(tab) { + if (!tab) { + tab = gBrowser.selectedTab; + } + let target = TargetFactory.forTab(tab); + let toolbox = yield gDevTools.showToolbox(target, "styleeditor"); + let panel = toolbox.getPanel("styleeditor"); + let ui = panel.UI; + + return { toolbox, panel, ui }; +}); + +/** * Creates a new tab in specified window navigates it to the given URL and * opens style editor in it. */ var openStyleEditorForURL = Task.async(function* (url, win) { let tab = yield addTab(url, win); - let target = TargetFactory.forTab(tab); - let toolbox = yield gDevTools.showToolbox(target, "styleeditor"); - let panel = toolbox.getPanel("styleeditor"); - let ui = panel.UI; - - return { tab, toolbox, panel, ui }; + let result = yield openStyleEditor(tab); + result.tab = tab; + return result; }); /** * Loads shared/frame-script-utils.js in the specified tab. * * @param tab * Optional tab to load the frame script in. Defaults to the current tab. */
new file mode 100644 --- /dev/null +++ b/devtools/client/styleeditor/test/sync.html @@ -0,0 +1,20 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>simple testcase</title> + <style type="text/css"> + body { + border-width: 15px; + color: red; + } + + #testid { + font-size: 4em; + } + </style> +</head> +<body> + <div id="testid">simple testcase</div> +</body> +</html>
--- a/devtools/client/styleinspector/rule-view.js +++ b/devtools/client/styleinspector/rule-view.js @@ -104,17 +104,17 @@ function createDummyDocument() { * ElementStyle: * Responsible for keeping track of which properties are overridden. * Maintains a list of Rule objects that apply to the element. * Rule: * Manages a single style declaration or rule. * Responsible for applying changes to the properties in a rule. * Maintains a list of TextProperty objects. * TextProperty: - * Manages a single property from the cssText attribute of the + * Manages a single property from the authoredText attribute of the * relevant declaration. * Maintains a list of computed properties that come from this * property declaration. * Changes to the TextProperty are sent to its related Rule for * application. */ /** @@ -178,16 +178,22 @@ ElementStyle.prototype = { }, destroy: function() { if (this.destroyed) { return; } this.destroyed = true; + for (let rule of this.rules) { + if (rule.editor) { + rule.editor.destroy(); + } + } + this.dummyElement = null; this.dummyElementPromise.then(dummyElement => { dummyElement.remove(); this.dummyElementPromise = null; }, console.error); }, /** @@ -221,31 +227,35 @@ ElementStyle.prototype = { return this.dummyElementPromise.then(() => { if (this.populated !== populated) { // Don't care anymore. return; } // Store the current list of rules (if any) during the population // process. They will be reused if possible. - this._refreshRules = this.rules; + let existingRules = this.rules; this.rules = []; for (let entry of entries) { - this._maybeAddRule(entry); + this._maybeAddRule(entry, existingRules); } // Mark overridden computed styles. this.markOverriddenAll(); this._sortRulesForPseudoElement(); // We're done with the previous list of rules. - delete this._refreshRules; + for (let r of existingRules) { + if (r && r.editor) { + r.editor.destroy(); + } + } }); }).then(null, e => { // populate is often called after a setTimeout, // the connection may already be closed. if (this.destroyed) { return promise.resolve(undefined); } return promiseWarn(e); @@ -264,51 +274,53 @@ ElementStyle.prototype = { }, /** * Add a rule if it's one we care about. Filters out duplicates and * inherited styles with no inherited properties. * * @param {Object} options * Options for creating the Rule, see the Rule constructor. + * @param {Array} existingRules + * Rules to reuse if possible. If a rule is reused, then it + * it will be deleted from this array. * @return {Boolean} true if we added the rule. */ - _maybeAddRule: function(options) { + _maybeAddRule: function(options, existingRules) { // If we've already included this domRule (for example, when a // common selector is inherited), ignore it. if (options.rule && this.rules.some(rule => rule.domRule === options.rule)) { return false; } if (options.system) { return false; } let rule = null; // If we're refreshing and the rule previously existed, reuse the // Rule object. - if (this._refreshRules) { - for (let r of this._refreshRules) { - if (r.matches(options)) { - rule = r; - rule.refresh(options); - break; - } + if (existingRules) { + let ruleIndex = existingRules.findIndex((r) => r.matches(options)); + if (ruleIndex >= 0) { + rule = existingRules[ruleIndex]; + rule.refresh(options); + existingRules.splice(ruleIndex, 1); } } // If this is a new rule, create its Rule object. if (!rule) { rule = new Rule(this, options); } - // Ignore inherited rules with no properties. - if (options.inherited && rule.textProps.length === 0) { + // Ignore inherited rules with no visible properties. + if (options.inherited && !rule.hasAnyVisibleProperties()) { return false; } this.rules.push(rule); return true; }, /** @@ -367,16 +379,26 @@ ElementStyle.prototype = { // If the new property is a lower or equal priority, mark it as // overridden. // // _overriddenDirty will be set on each prop, indicating whether its // dirty status changed during this pass. let taken = {}; for (let computedProp of computedProps) { let earlier = taken[computedProp.name]; + + // Prevent -webkit-gradient from being selected after unchecking + // linear-gradient in this case: + // -moz-linear-gradient: ...; + // -webkit-linear-gradient: ...; + // linear-gradient: ...; + if (!computedProp.textProp.isValid()) { + computedProp.overridden = true; + continue; + } let overridden; if (earlier && computedProp.priority === "important" && earlier.priority !== "important" && (earlier.textProp.rule.inherited || !computedProp.textProp.rule.inherited)) { // New property is higher priority. Mark the earlier property // overridden (which will reverse its dirty state). @@ -458,37 +480,32 @@ function Rule(elementStyle, options) { this.inherited = options.inherited || null; this.keyframes = options.keyframes || null; this._modificationDepth = 0; if (this.domRule && this.domRule.mediaText) { this.mediaText = this.domRule.mediaText; } - // Populate the text properties with the style's current cssText + // Populate the text properties with the style's current authoredText // value, and add in any disabled properties from the store. this.textProps = this._getTextProperties(); this.textProps = this.textProps.concat(this._getDisabledProperties()); } Rule.prototype = { mediaText: "", get title() { - if (this._title) { - return this._title; - } - this._title = CssLogic.shortSource(this.sheet); + let title = CssLogic.shortSource(this.sheet); if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) { - this._title += ":" + this.ruleLine; + title += ":" + this.ruleLine; } - this._title = this._title + - (this.mediaText ? " @media " + this.mediaText : ""); - return this._title; + return title + (this.mediaText ? " @media " + this.mediaText : ""); }, get inheritedSource() { if (this._inheritedSource) { return this._inheritedSource; } this._inheritedSource = ""; if (this.inherited) { @@ -546,30 +563,25 @@ Rule.prototype = { * Get display name for this rule based on the original source * for this rule's style sheet. * * @return {Promise} * Promise which resolves with location as an object containing * both the full and short version of the source string. */ getOriginalSourceStrings: function() { - if (this._originalSourceStrings) { - return promise.resolve(this._originalSourceStrings); - } - return this.domRule.getOriginalLocation().then(({href, line, mediaText}) => { let mediaString = mediaText ? " @" + mediaText : ""; let sourceStrings = { full: (href || CssLogic.l10n("rule.sourceInline")) + ":" + line + mediaString, short: CssLogic.shortSource({href: href}) + ":" + line + mediaString }; - this._originalSourceStrings = sourceStrings; return sourceStrings; }); }, /** * Returns true if the rule matches the creation options * specified. * @@ -590,69 +602,73 @@ Rule.prototype = { * @param {String} priority * The property's priority (either "important" or an empty string). * @param {TextProperty} siblingProp * Optional, property next to which the new property will be added. */ createProperty: function(name, value, priority, siblingProp) { let prop = new TextProperty(this, name, value, priority); + let ind; if (siblingProp) { - let ind = this.textProps.indexOf(siblingProp); - this.textProps.splice(ind + 1, 0, prop); + ind = this.textProps.indexOf(siblingProp) + 1; + this.textProps.splice(ind, 0, prop); } else { + ind = this.textProps.length; this.textProps.push(prop); } - this.applyProperties(); + this.applyProperties((modifications) => { + modifications.createProperty(ind, name, value, priority); + }); return prop; }, /** - * Reapply all the properties in this rule, and update their - * computed styles. Store disabled properties in the element - * style's store. Will re-mark overridden properties. + * Helper function for applyProperties that is called when the actor + * does not support as-authored styles. Store disabled properties + * in the element style's store. */ - applyProperties: function(modifications) { + _applyPropertiesNoAuthored: function(modifications) { this.elementStyle.markOverriddenAll(); - if (!modifications) { - modifications = this.style.startModifyingProperties(); - } let disabledProps = []; for (let prop of this.textProps) { + if (prop.invisible) { + continue; + } if (!prop.enabled) { disabledProps.push({ name: prop.name, value: prop.value, priority: prop.priority }); continue; } if (prop.value.trim() === "") { continue; } - modifications.setProperty(prop.name, prop.value, prop.priority); + modifications.setProperty(-1, prop.name, prop.value, prop.priority); prop.updateComputed(); } // Store disabled properties in the disabled store. let disabled = this.elementStyle.store.disabled; if (disabledProps.length > 0) { disabled.set(this.style, disabledProps); } else { disabled.delete(this.style); } - let modificationsPromise = modifications.apply().then(() => { + return modifications.apply().then(() => { let cssProps = {}; - for (let cssProp of parseDeclarations(this.style.cssText)) { + for (let cssProp of parseDeclarations(this.style.authoredText)) { cssProps[cssProp.name] = cssProp; } for (let textProp of this.textProps) { if (!textProp.enabled) { continue; } let cssProp = cssProps[textProp.name]; @@ -662,46 +678,97 @@ Rule.prototype = { name: textProp.name, value: "", priority: "" }; } textProp.priority = cssProp.priority; } - - this.elementStyle.markOverriddenAll(); - - if (modificationsPromise === this._applyingModifications) { - this._applyingModifications = null; + }); + }, + + /** + * A helper for applyProperties that applies properties in the "as + * authored" case; that is, when the StyleRuleActor supports + * setRuleText. + */ + _applyPropertiesAuthored: function(modifications) { + return modifications.apply().then(() => { + // The rewriting may have required some other property values to + // change, e.g., to insert some needed terminators. Update the + // relevant properties here. + for (let index in modifications.changedDeclarations) { + let newValue = modifications.changedDeclarations[index]; + this.textProps[index].noticeNewValue(newValue); + } + // Recompute and redisplay the computed properties. + for (let prop of this.textProps) { + if (!prop.invisible && prop.enabled) { + prop.updateComputed(); + prop.updateEditor(); + } } - - this.elementStyle._changed(); - }).then(null, promiseWarn); - - this._applyingModifications = modificationsPromise; - return modificationsPromise; + }); + }, + + /** + * Reapply all the properties in this rule, and update their + * computed styles. Will re-mark overridden properties. Sets the + * |_applyingModifications| property to a promise which will resolve + * when the edit has completed. + * + * @param {Function} modifier a function that takes a RuleModificationList + * (or RuleRewriter) as an argument and that modifies it + * to apply the desired edit + * @return {Promise} a promise which will resolve when the edit + * is complete + */ + applyProperties: function(modifier) { + // If there is already a pending modification, we have to wait + // until it settles before applying the next modification. + let resultPromise = + promise.resolve(this._applyingModifications).then(() => { + let modifications = this.style.startModifyingProperties(); + modifier(modifications); + if (this.style.canSetRuleText) { + return this._applyPropertiesAuthored(modifications); + } + return this._applyPropertiesNoAuthored(modifications); + }).then(() => { + this.elementStyle.markOverriddenAll(); + + if (resultPromise === this._applyingModifications) { + this._applyingModifications = null; + this.elementStyle._changed(); + } + }).catch(promiseWarn); + + this._applyingModifications = resultPromise; + return resultPromise; }, /** * Renames a property. * * @param {TextProperty} property * The property to rename. * @param {String} name * The new property name (such as "background" or "border-top"). */ setPropertyName: function(property, name) { if (name === property.name) { return; } - let modifications = this.style.startModifyingProperties(); - modifications.removeProperty(property.name); + property.name = name; - this.applyProperties(modifications, name); + let index = this.textProps.indexOf(property); + this.applyProperties((modifications) => { + modifications.renameProperty(index, property.name, name); + }); }, /** * Sets the value and priority of a property, then reapply all properties. * * @param {TextProperty} property * The property to manipulate. * @param {String} value @@ -711,88 +778,100 @@ Rule.prototype = { */ setPropertyValue: function(property, value, priority) { if (value === property.value && priority === property.priority) { return; } property.value = value; property.priority = priority; - this.applyProperties(null, property.name); + + let index = this.textProps.indexOf(property); + this.applyProperties((modifications) => { + modifications.setProperty(index, property.name, value, priority); + }); }, /** * Just sets the value and priority of a property, in order to preview its * effect on the content document. * * @param {TextProperty} property * The property which value will be previewed * @param {String} value * The value to be used for the preview * @param {String} priority * The property's priority (either "important" or an empty string). */ previewPropertyValue: function(property, value, priority) { let modifications = this.style.startModifyingProperties(); - modifications.setProperty(property.name, value, priority); + modifications.setProperty(this.textProps.indexOf(property), + property.name, value, priority); modifications.apply().then(() => { // Ensure dispatching a ruleview-changed event // also for previews this.elementStyle._changed(); }); }, /** * Disables or enables given TextProperty. * * @param {TextProperty} property * The property to enable/disable * @param {Boolean} value */ setPropertyEnabled: function(property, value) { + if (property.enabled === !!value) { + return; + } property.enabled = !!value; - let modifications = this.style.startModifyingProperties(); - if (!property.enabled) { - modifications.removeProperty(property.name); - } - this.applyProperties(modifications); + let index = this.textProps.indexOf(property); + this.applyProperties((modifications) => { + modifications.setPropertyEnabled(index, property.name, property.enabled); + }); }, /** * Remove a given TextProperty from the rule and update the rule * accordingly. * * @param {TextProperty} property * The property to be removed */ removeProperty: function(property) { - this.textProps = this.textProps.filter(prop => prop !== property); - let modifications = this.style.startModifyingProperties(); - modifications.removeProperty(property.name); + let index = this.textProps.indexOf(property); + this.textProps.splice(index, 1); // Need to re-apply properties in case removing this TextProperty // exposes another one. - this.applyProperties(modifications); + this.applyProperties((modifications) => { + modifications.removeProperty(index, property.name); + }); }, /** * Get the list of TextProperties from the style. Needs - * to parse the style's cssText. + * to parse the style's authoredText. */ _getTextProperties: function() { let textProps = []; let store = this.elementStyle.store; - let props = parseDeclarations(this.style.cssText); + let props = parseDeclarations(this.style.authoredText, true); for (let prop of props) { let name = prop.name; - if (this.inherited && !domUtils.isInheritedProperty(name)) { - continue; - } + // In an inherited rule, we only show inherited properties. + // However, we must keep all properties in order for rule + // rewriting to work properly. So, compute the "invisible" + // property here. + let invisible = this.inherited && !domUtils.isInheritedProperty(name); let value = store.userProperties.getProperty(this.style, name, prop.value); - let textProp = new TextProperty(this, name, value, prop.priority); + let textProp = new TextProperty(this, name, value, prop.priority, + !("commentOffsets" in prop), + invisible); textProps.push(textProp); } return textProps; }, /** * Return the list of disabled properties from the store for this rule. @@ -859,33 +938,33 @@ Rule.prototype = { // Refresh the editor if one already exists. if (this.editor) { this.editor.populate(); } }, /** * Update the current TextProperties that match a given property - * from the cssText. Will choose one existing TextProperty to update + * from the authoredText. Will choose one existing TextProperty to update * with the new property's value, and will disable all others. * * When choosing the best match to reuse, properties will be chosen * by assigning a rank and choosing the highest-ranked property: * Name, value, and priority match, enabled. (6) * Name, value, and priority match, disabled. (5) * Name and value match, enabled. (4) * Name and value match, disabled. (3) * Name matches, enabled. (2) * Name matches, disabled. (1) * * If no existing properties match the property, nothing happens. * * @param {TextProperty} newProp * The current version of the property, as parsed from the - * cssText in Rule._getTextProperties(). + * authoredText in Rule._getTextProperties(). * @return {Boolean} true if a property was updated, false if no properties * were updated. */ _updateTextProperty: function(newProp) { let match = { rank: 0, prop: null }; for (let prop of this.textProps) { if (prop.name !== newProp.name) { @@ -948,66 +1027,99 @@ Rule.prototype = { * The text property that will be left to focus on a sibling. * @param {Number} direction * The move focus direction number. */ editClosestTextProperty: function(textProperty, direction) { let index = this.textProps.indexOf(textProperty); if (direction === Ci.nsIFocusManager.MOVEFOCUS_FORWARD) { - if (index === this.textProps.length - 1) { + for (++index; index < this.textProps.length; ++index) { + if (!this.textProps[index].invisible) { + break; + } + } + if (index === this.textProps.length) { textProperty.rule.editor.closeBrace.click(); } else { - let nextProp = this.textProps[index + 1]; - nextProp.editor.nameSpan.click(); + this.textProps[index].editor.nameSpan.click(); } } else if (direction === Ci.nsIFocusManager.MOVEFOCUS_BACKWARD) { - if (index === 0) { + for (--index; index >= 0; --index) { + if (!this.textProps[index].invisible) { + break; + } + } + if (index < 0) { textProperty.editor.ruleEditor.selectorText.click(); } else { - let prevProp = this.textProps[index - 1]; - prevProp.editor.valueSpan.click(); + this.textProps[index].editor.valueSpan.click(); } } }, /** * Return a string representation of the rule. */ stringifyRule: function() { let selectorText = this.selectorText; let cssText = ""; let terminator = osString === "WINNT" ? "\r\n" : "\n"; for (let textProp of this.textProps) { - cssText += "\t" + textProp.stringifyProperty() + terminator; + if (!textProp.invisible) { + cssText += "\t" + textProp.stringifyProperty() + terminator; + } } return selectorText + " {" + terminator + cssText + "}"; + }, + + /** + * See whether this rule has any non-invisible properties. + * @return {Boolean} true if there is any visible property, or false + * if all properties are invisible + */ + hasAnyVisibleProperties: function() { + for (let prop of this.textProps) { + if (!prop.invisible) { + return true; + } + } + return false; } }; /** - * A single property in a rule's cssText. + * A single property in a rule's authoredText. * * @param {Rule} rule * The rule this TextProperty came from. * @param {String} name * The text property name (such as "background" or "border-top"). * @param {String} value * The property's value (not including priority). * @param {String} priority * The property's priority (either "important" or an empty string). + * @param {Boolean} enabled + * Whether the property is enabled. + * @param {Boolean} invisible + * Whether the property is invisible. An invisible property + * does not show up in the UI; these are needed so that the + * index of a property in Rule.textProps is the same as the index + * coming from parseDeclarations. */ -function TextProperty(rule, name, value, priority) { +function TextProperty(rule, name, value, priority, enabled = true, + invisible = false) { this.rule = rule; this.name = name; this.value = value; this.priority = priority; - this.enabled = true; + this.enabled = !!enabled; + this.invisible = invisible; this.updateComputed(); } TextProperty.prototype = { /** * Update the editor associated with this text property, * if any. */ @@ -1082,16 +1194,27 @@ TextProperty.prototype = { if (this.editor && value !== this.editor.committed.value || force) { store.userProperties.setProperty(this.rule.style, this.name, value); } this.rule.setPropertyValue(this, value, priority); this.updateEditor(); }, + /** + * Called when the property's value has been updated externally, and + * the property and editor should update. + */ + noticeNewValue: function(value) { + if (value !== this.value) { + this.value = value; + this.updateEditor(); + } + }, + setName: function(name) { let store = this.rule.elementStyle.store; if (name !== this.name) { store.userProperties.setProperty(this.rule.style, name, this.editor.committed.value); } @@ -1117,16 +1240,43 @@ TextProperty.prototype = { ";"; // Comment out property declarations that are not enabled if (!this.enabled) { declaration = "/* " + escapeCSSComment(declaration) + " */"; } return declaration; + }, + + /** + * See whether this property's name is known. + * + * @return {Boolean} true if the property name is known, false otherwise. + */ + isKnownProperty: function() { + try { + // If the property name is invalid, the cssPropertyIsShorthand + // will throw an exception. But if it is valid, no exception will + // be thrown; so we just ignore the return value. + domUtils.cssPropertyIsShorthand(this.name); + return true; + } catch (e) { + return false; + } + }, + + /** + * Validate this property. Does it make sense for this value to be assigned + * to this property name? This does not apply the property value + * + * @return {Boolean} true if the property value is valid, false otherwise. + */ + isValid: function() { + return domUtils.cssPropertyIsValid(this.name, this.value); } }; /** * View hierarchy mostly follows the model hierarchy. * * CssRuleView: * Owns an ElementStyle and creates a list of RuleEditors for its @@ -1480,29 +1630,25 @@ CssRuleView.prototype = { clipboardHelper.copyString(text); } catch(e) { console.error(e); } }, /** - * Add a new rule to the current element. + * A helper for _onAddRule that handles the case where the actor + * does not support as-authored styles. */ - _onAddRule: function() { + _onAddNewRuleNonAuthored: function() { let elementStyle = this._elementStyle; let element = elementStyle.element; let rules = elementStyle.rules; - let client = this.inspector.toolbox._target.client; let pseudoClasses = element.pseudoClassLocks; - if (!client.traits.addNewRule) { - return; - } - this.pageStyle.addNewRule(element, pseudoClasses).then(options => { let newRule = new Rule(elementStyle, options); rules.push(newRule); let editor = new RuleEditor(this, newRule); newRule.editor = editor; // Insert the new rule editor after the inline element rule if (rules.length <= 1) { @@ -1519,16 +1665,54 @@ CssRuleView.prototype = { // Focus and make the new rule's selector editable editor.selectorText.click(); elementStyle._changed(); }); }, /** + * Add a new rule to the current element. + */ + _onAddRule: function() { + let elementStyle = this._elementStyle; + let element = elementStyle.element; + let client = this.inspector.toolbox._target.client; + let pseudoClasses = element.pseudoClassLocks; + + if (!client.traits.addNewRule) { + return; + } + + if (!this.pageStyle.supportsAuthoredStyles) { + // We're talking to an old server. + this._onAddNewRuleNonAuthored(); + return; + } + + // Adding a new rule with authored styles will cause the actor to + // emit an event, which will in turn cause the rule view to be + // updated. So, we wait for this update and for the rule creation + // request to complete, and then focus the new rule's selector. + let eventPromise = this.once("ruleview-refreshed"); + let newRulePromise = this.pageStyle.addNewRule(element, pseudoClasses); + promise.all([eventPromise, newRulePromise]).then((values) => { + let options = values[1]; + // Be sure the reference the correct |rules| here. + for (let rule of this._elementStyle.rules) { + if (options.rule === rule.domRule) { + rule.editor.selectorText.click(); + elementStyle._changed(); + break; + } + } + }); + }, + + /** * Disables add rule button when needed */ refreshAddRuleButtonState: function() { let shouldBeDisabled = !this._viewedElement || !this.inspector.selection.isElementNode() || this.inspector.selection.isAnonymousNode(); this.addRuleButton.disabled = shouldBeDisabled; }, @@ -2141,17 +2325,17 @@ CssRuleView.prototype = { */ highlightRule: function(rule) { let isRuleSelectorHighlighted = this._highlightRuleSelector(rule); let isStyleSheetHighlighted = this._highlightStyleSheet(rule); let isHighlighted = isRuleSelectorHighlighted || isStyleSheetHighlighted; // Highlight search matches in the rule properties for (let textProp of rule.textProps) { - if (this._highlightProperty(textProp.editor)) { + if (!textProp.invisible && this._highlightProperty(textProp.editor)) { isHighlighted = true; } } return isHighlighted; }, /** @@ -2430,21 +2614,28 @@ function RuleEditor(aRuleView, aRule) { this.isEditable = !aRule.isSystem; // Flag that blocks updates of the selector and properties when it is // being edited this.isEditing = false; this._onNewProperty = this._onNewProperty.bind(this); this._newPropertyDestroy = this._newPropertyDestroy.bind(this); this._onSelectorDone = this._onSelectorDone.bind(this); + this._locationChanged = this._locationChanged.bind(this); + + this.rule.domRule.on("location-changed", this._locationChanged); this._create(); } RuleEditor.prototype = { + destroy: function() { + this.rule.domRule.off("location-changed"); + }, + get isSelectorEditable() { let toolbox = this.ruleView.inspector.toolbox; let trait = this.isEditable && toolbox.target.client.traits.selectorEditable && this.rule.domRule.type !== ELEMENT_STYLE && this.rule.domRule.type !== Ci.nsIDOMCSSRule.KEYFRAME_RULE; // Do not allow editing anonymousselectors until we can @@ -2549,38 +2740,47 @@ RuleEditor.prototype = { // Create a property editor when the close brace is clicked. editableItem({ element: this.closeBrace }, () => { this.newProperty(); }); } }, + /** + * Event handler called when a property changes on the + * StyleRuleActor. + */ + _locationChanged: function(line, column) { + this.updateSourceLink(); + }, + updateSourceLink: function() { let sourceLabel = this.element.querySelector(".ruleview-rule-source-label"); + let title = this.rule.title; let sourceHref = (this.rule.sheet && this.rule.sheet.href) ? - this.rule.sheet.href : this.rule.title; + this.rule.sheet.href : title; let sourceLine = this.rule.ruleLine > 0 ? ":" + this.rule.ruleLine : ""; sourceLabel.setAttribute("tooltiptext", sourceHref + sourceLine); if (this.rule.isSystem) { let uaLabel = _strings.GetStringFromName("rule.userAgentStyles"); - sourceLabel.setAttribute("value", uaLabel + " " + this.rule.title); + sourceLabel.setAttribute("value", uaLabel + " " + title); // Special case about:PreferenceStyleSheet, as it is generated on the // fly and the URI is not registered with the about: handler. // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37 if (sourceHref === "about:PreferenceStyleSheet") { sourceLabel.parentNode.setAttribute("unselectable", "true"); sourceLabel.setAttribute("value", uaLabel); sourceLabel.removeAttribute("tooltiptext"); } } else { - sourceLabel.setAttribute("value", this.rule.title); + sourceLabel.setAttribute("value", title); if (this.rule.ruleLine === -1 && this.rule.domRule.parentStyleSheet) { sourceLabel.parentNode.setAttribute("unselectable", "true"); } } let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); if (showOrig && !this.rule.isSystem && this.rule.domRule.type !== ELEMENT_STYLE) { @@ -2649,17 +2849,17 @@ RuleEditor.prototype = { textContent: selectorText.value, class: selectorClass }); } }); } for (let prop of this.rule.textProps) { - if (!prop.editor) { + if (!prop.editor && !prop.invisible) { let editor = new TextPropertyEditor(this, prop); this.propertyList.appendChild(editor.element); } } }, /** * Programatically add a new property to the rule. @@ -2857,23 +3057,23 @@ RuleEditor.prototype = { let newRule = new Rule(elementStyle, ruleProps); let editor = new RuleEditor(ruleView, newRule); let rules = elementStyle.rules; rules.splice(rules.indexOf(this.rule), 1); rules.push(newRule); elementStyle._changed(); + elementStyle.markOverriddenAll(); editor.element.setAttribute("unmatched", !isMatching); this.element.parentNode.replaceChild(editor.element, this.element); // Remove highlight for modified selector - if (ruleView.highlightedSelector && - ruleView.highlightedSelector === this.rule.selectorText) { + if (ruleView.highlightedSelector) { ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon, ruleView.highlightedSelector); } editor._moveSelectorFocus(direction); }).then(null, err => { this.isEditing = false; promiseWarn(err); @@ -3167,17 +3367,18 @@ TextPropertyEditor.prototype = { this.enable.removeAttribute("checked"); } this.warning.hidden = this.editing || this.isValid(); this.filterProperty.hidden = this.editing || !this.isValid() || !this.prop.overridden; - if (this.prop.overridden || !this.prop.enabled) { + if (this.prop.overridden || !this.prop.enabled || + !this.prop.isKnownProperty()) { this.element.classList.add("ruleview-overridden"); } else { this.element.classList.remove("ruleview-overridden"); } let name = this.prop.name; this.nameSpan.textContent = name; @@ -3497,18 +3698,20 @@ TextPropertyEditor.prototype = { _onValueDone: function(value="", commit, direction) { let parsedProperties = this._getValueAndExtraProperties(value); let val = parseSingleValue(parsedProperties.firstValue); let isValueUnchanged = (!commit && !this.ruleEditor.isEditing) || !parsedProperties.propertiesToAdd.length && this.committed.value === val.value && this.committed.priority === val.priority; // If the value is not empty and unchanged, revert the property back to - // its original enabled or disabled state + // its original value and enabled or disabled state if (value.trim() && isValueUnchanged) { + this.ruleEditor.rule.previewPropertyValue(this.prop, val.value, + val.priority); this.rule.setPropertyEnabled(this.prop, this.prop.enabled); return; } // First, set this property value (common case, only modified a property) this.prop.setValue(val.value, val.priority); if (!this.prop.enabled) { @@ -3550,17 +3753,17 @@ TextPropertyEditor.prototype = { this._previewValue(this.valueSpan.textContent); }, /** * Called when the swatch editor closes from an ESC. Revert to the original * value of this property before editing. */ _onSwatchRevert: function() { - this.rule.setPropertyEnabled(this.prop, this.prop.enabled); + this._previewValue(this.prop.value); this.update(); }, /** * Parse a value string and break it into pieces, starting with the * first value, and into an array of additional properties (if any). * * Example: Calling with "red; width: 100px" would return @@ -3626,17 +3829,17 @@ TextPropertyEditor.prototype = { /** * Validate this property. Does it make sense for this value to be assigned * to this property name? This does not apply the property value * * @return {Boolean} true if the property value is valid, false otherwise. */ isValid: function() { - return domUtils.cssPropertyIsValid(this.prop.name, this.prop.value); + return this.prop.isValid(); } }; /** * Store of CSSStyleDeclarations mapped to properties that have been changed by * the user. */ function UserProperties() {
--- a/devtools/client/styleinspector/style-inspector.js +++ b/devtools/client/styleinspector/style-inspector.js @@ -43,16 +43,17 @@ function RuleViewTool(inspector, window) this.view.on("ruleview-linked-clicked", this.onLinkClicked); this.inspector.selection.on("detached", this.onSelected); this.inspector.selection.on("new-node-front", this.onSelected); this.inspector.on("layout-change", this.refresh); this.inspector.selection.on("pseudoclass", this.refresh); this.inspector.target.on("navigate", this.clearUserProperties); this.inspector.sidebar.on("ruleview-selected", this.onPanelSelected); + this.inspector.pageStyle.on("stylesheet-updated", this.refresh); this.onSelected(); } RuleViewTool.prototype = { isSidebarActive: function() { if (!this.view) { return false; @@ -147,16 +148,19 @@ RuleViewTool.prototype = { }, destroy: function() { this.inspector.off("layout-change", this.refresh); this.inspector.selection.off("pseudoclass", this.refresh); this.inspector.selection.off("new-node-front", this.onSelected); this.inspector.target.off("navigate", this.clearUserProperties); this.inspector.sidebar.off("ruleview-selected", this.onPanelSelected); + if (this.inspector.pageStyle) { + this.inspector.pageStyle.off("stylesheet-updated", this.refresh); + } this.view.off("ruleview-linked-clicked", this.onLinkClicked); this.view.off("ruleview-changed", this.onPropertyChanged); this.view.off("ruleview-refreshed", this.onViewRefreshed); this.view.destroy(); this.view = this.document = this.inspector = null; @@ -174,16 +178,17 @@ function ComputedViewTool(inspector, win this.refresh = this.refresh.bind(this); this.onPanelSelected = this.onPanelSelected.bind(this); this.inspector.selection.on("detached", this.onSelected); this.inspector.selection.on("new-node-front", this.onSelected); this.inspector.on("layout-change", this.refresh); this.inspector.selection.on("pseudoclass", this.refresh); this.inspector.sidebar.on("computedview-selected", this.onPanelSelected); + this.inspector.pageStyle.on("stylesheet-updated", this.refresh); this.view.selectElement(null); this.onSelected(); } ComputedViewTool.prototype = { isSidebarActive: function() { @@ -239,16 +244,19 @@ ComputedViewTool.prototype = { }, destroy: function() { this.inspector.off("layout-change", this.refresh); this.inspector.sidebar.off("computedview-selected", this.refresh); this.inspector.selection.off("pseudoclass", this.refresh); this.inspector.selection.off("new-node-front", this.onSelected); this.inspector.sidebar.off("computedview-selected", this.onPanelSelected); + if (this.inspector.pageStyle) { + this.inspector.pageStyle.off("stylesheet-updated", this.refresh); + } this.view.destroy(); this.view = this.document = this.inspector = null; } }; exports.RuleViewTool = RuleViewTool;
--- a/devtools/client/styleinspector/test/browser.ini +++ b/devtools/client/styleinspector/test/browser.ini @@ -14,16 +14,17 @@ support-files = doc_custom.html doc_filter.html doc_frame_script.js doc_keyframeanimation.html doc_keyframeanimation.css doc_matched_selectors.html doc_media_queries.html doc_pseudoelement.html + doc_ruleLineNumbers.html doc_sourcemaps.css doc_sourcemaps.css.map doc_sourcemaps.html doc_sourcemaps.scss doc_style_editor_link.css doc_test_image.png doc_urls_clickable.css doc_urls_clickable.html @@ -55,16 +56,17 @@ support-files = [browser_ruleview_add-property_01.js] [browser_ruleview_add-property_02.js] [browser_ruleview_add-property-svg.js] [browser_ruleview_add-rule_01.js] [browser_ruleview_add-rule_02.js] [browser_ruleview_add-rule_03.js] [browser_ruleview_add-rule_04.js] [browser_ruleview_add-rule_pseudo_class.js] +[browser_ruleview_authored.js] [browser_ruleview_colorpicker-and-image-tooltip_01.js] [browser_ruleview_colorpicker-and-image-tooltip_02.js] [browser_ruleview_colorpicker-appears-on-swatch-click.js] [browser_ruleview_colorpicker-commit-on-ENTER.js] [browser_ruleview_colorpicker-edit-gradient.js] [browser_ruleview_colorpicker-hides-on-tooltip.js] [browser_ruleview_colorpicker-multiple-changes.js] [browser_ruleview_colorpicker-release-outside-frame.js] @@ -113,28 +115,31 @@ skip-if = e10s # Bug 1039528: "inspect e [browser_ruleview_edit-selector_06.js] [browser_ruleview_editable-field-focus_01.js] [browser_ruleview_editable-field-focus_02.js] [browser_ruleview_eyedropper.js] [browser_ruleview_filtereditor-appears-on-swatch-click.js] [browser_ruleview_filtereditor-commit-on-ENTER.js] [browser_ruleview_filtereditor-revert-on-ESC.js] skip-if = (os == "win" && debug) || e10s # bug 963492: win. bug 1040653: e10s. +[browser_ruleview_guessIndentation.js] [browser_ruleview_inherited-properties_01.js] [browser_ruleview_inherited-properties_02.js] [browser_ruleview_inherited-properties_03.js] [browser_ruleview_keybindings.js] [browser_ruleview_keyframes-rule_01.js] [browser_ruleview_keyframes-rule_02.js] +[browser_ruleview_lineNumbers.js] [browser_ruleview_livepreview.js] [browser_ruleview_mark_overridden_01.js] [browser_ruleview_mark_overridden_02.js] [browser_ruleview_mark_overridden_03.js] [browser_ruleview_mark_overridden_04.js] [browser_ruleview_mark_overridden_05.js] +[browser_ruleview_mark_overridden_06.js] [browser_ruleview_mark_overridden_07.js] [browser_ruleview_mathml-element.js] [browser_ruleview_media-queries.js] [browser_ruleview_multiple-properties-duplicates.js] [browser_ruleview_multiple-properties-priority.js] [browser_ruleview_multiple-properties-unfinished_01.js] [browser_ruleview_multiple-properties-unfinished_02.js] [browser_ruleview_multiple_properties_01.js]
--- a/devtools/client/styleinspector/test/browser_computedview_cycle_color.js +++ b/devtools/client/styleinspector/test/browser_computedview_cycle_color.js @@ -29,17 +29,23 @@ add_task(function*() { checkColorCycling(container, inspector); }); function checkColorCycling(container, inspector) { let swatch = container.querySelector(".computedview-colorswatch"); let valueNode = container.querySelector(".computedview-color"); let win = inspector.sidebar.getWindowForTab("computedview"); - // Hex (default) + // "Authored" (default; currently the computed value) + is(valueNode.textContent, "rgb(255, 0, 0)", + "Color displayed as an RGB value."); + + // Hex + EventUtils.synthesizeMouseAtCenter(swatch, + {type: "mousedown", shiftKey: true}, win); is(valueNode.textContent, "#F00", "Color displayed as a hex value."); // HSL EventUtils.synthesizeMouseAtCenter(swatch, {type: "mousedown", shiftKey: true}, win); is(valueNode.textContent, "hsl(0, 100%, 50%)", "Color displayed as an HSL value."); @@ -50,20 +56,14 @@ function checkColorCycling(container, in "Color displayed as an RGB value."); // Color name EventUtils.synthesizeMouseAtCenter(swatch, {type: "mousedown", shiftKey: true}, win); is(valueNode.textContent, "red", "Color displayed as a color name."); - // "Authored" (currently the computed value) + // Back to "Authored" EventUtils.synthesizeMouseAtCenter(swatch, {type: "mousedown", shiftKey: true}, win); is(valueNode.textContent, "rgb(255, 0, 0)", "Color displayed as an RGB value."); - - // Back to hex - EventUtils.synthesizeMouseAtCenter(swatch, - {type: "mousedown", shiftKey: true}, win); - is(valueNode.textContent, "#F00", - "Color displayed as hex again."); }
--- a/devtools/client/styleinspector/test/browser_computedview_getNodeInfo.js +++ b/devtools/client/styleinspector/test/browser_computedview_getNodeInfo.js @@ -70,30 +70,30 @@ const TEST_DATA = [ getHoveredNode: function*(view) { return getComputedViewProperty(view, "color").nameSpan; }, assertNodeInfo: function(nodeInfo) { is(nodeInfo.type, VIEW_NODE_PROPERTY_TYPE); ok("property" in nodeInfo.value); ok("value" in nodeInfo.value); is(nodeInfo.value.property, "color"); - is(nodeInfo.value.value, "#F00"); + is(nodeInfo.value.value, "rgb(255, 0, 0)"); } }, { desc: "Testing a property value", getHoveredNode: function*(view) { return getComputedViewProperty(view, "color").valueSpan; }, assertNodeInfo: function(nodeInfo) { is(nodeInfo.type, VIEW_NODE_VALUE_TYPE); ok("property" in nodeInfo.value); ok("value" in nodeInfo.value); is(nodeInfo.value.property, "color"); - is(nodeInfo.value.value, "#F00"); + is(nodeInfo.value.value, "rgb(255, 0, 0)"); } }, { desc: "Testing an image url", getHoveredNode: function*(view) { let {valueSpan} = getComputedViewProperty(view, "background-image"); return valueSpan.querySelector(".theme-link"); }, @@ -144,17 +144,17 @@ const TEST_DATA = [ desc: "Testing a matched rule value", getHoveredNode: function*(view) { let content = yield getComputedViewMatchedRules(view, "color"); return content.querySelector(".other-property-value"); }, assertNodeInfo: function(nodeInfo) { is(nodeInfo.type, VIEW_NODE_VALUE_TYPE); is(nodeInfo.value.property, "color"); - is(nodeInfo.value.value, "#F00"); + is(nodeInfo.value.value, "red"); } }, { desc: "Testing a matched rule stylesheet link", getHoveredNode: function*(view) { let content = yield getComputedViewMatchedRules(view, "color"); return content.querySelector(".rule-link .theme-link"); },
--- a/devtools/client/styleinspector/test/browser_computedview_refresh-on-style-change_01.js +++ b/devtools/client/styleinspector/test/browser_computedview_refresh-on-style-change_01.js @@ -20,10 +20,10 @@ add_task(function*() { info("Changing the node's style and waiting for the update"); let onUpdated = inspector.once("computed-view-refreshed"); getNode("#testdiv").style.cssText = "font-size: 15px; color: red;"; yield onUpdated; fontSize = getComputedViewPropertyValue(view, "font-size"); is(fontSize, "15px", "The computed view shows the updated font-size"); let color = getComputedViewPropertyValue(view, "color"); - is(color, "#F00", "The computed view also shows the color now"); + is(color, "rgb(255, 0, 0)", "The computed view also shows the color now"); });
--- a/devtools/client/styleinspector/test/browser_computedview_select-and-copy-styles.js +++ b/devtools/client/styleinspector/test/browser_computedview_select-and-copy-styles.js @@ -76,17 +76,17 @@ function checkSelectAll(view) { info("Testing select-all copy"); let contentDoc = view.styleDocument; let prop = contentDoc.querySelector(".property-view"); info("Checking that _onSelectAll() then copy returns the correct " + "clipboard value"); view._contextmenu._onSelectAll(); - let expectedPattern = "color: #FF0;[\\r\\n]+" + + let expectedPattern = "color: rgb\\(255, 255, 0\\);[\\r\\n]+" + "font-family: helvetica,sans-serif;[\\r\\n]+" + "font-size: 16px;[\\r\\n]+" + "font-variant-caps: small-caps;[\\r\\n]*"; return waitForClipboard(() => { fireCopyEvent(prop); }, () => { return checkClipboardData(expectedPattern);
--- a/devtools/client/styleinspector/test/browser_ruleview_add-property_01.js +++ b/devtools/client/styleinspector/test/browser_ruleview_add-property_01.js @@ -64,11 +64,11 @@ function* testCreateNew(view) { editor.input.value = "#XYZ"; let onBlur = once(editor.input, "blur"); onModifications = elementRuleEditor.rule._applyingModifications; editor.input.blur(); yield onBlur; yield onModifications; is(textProp.value, "#XYZ", "Text prop should have been changed."); - is(textProp.overridden, false, "Property should not be overridden"); + is(textProp.overridden, true, "Property should be overridden"); is(textProp.editor.isValid(), false, "#XYZ should not be a valid entry"); }
new file mode 100644 --- /dev/null +++ b/devtools/client/styleinspector/test/browser_ruleview_authored.js @@ -0,0 +1,127 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for as-authored styles. + +add_task(function*() { + yield basicTest(); + yield overrideTest(); + yield colorEditingTest(); +}); + +function* createTestContent(style) { + let content = `<style type="text/css"> + ${style} + </style> + <div id="testid" class="testclass">Styled Node</div>`; + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(content)); + + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + return view; +} + +function* basicTest() { + let view = yield createTestContent("#testid {" + + // Invalid property. + " something: random;" + + // Invalid value. + " color: orang;" + + // Override. + " background-color: blue;" + + " background-color: #f0c;" + + "} "); + + let elementStyle = view._elementStyle; + + let expected = [ + {name: "something", overridden: true}, + {name: "color", overridden: true}, + {name: "background-color", overridden: true}, + {name: "background-color", overridden: false} + ]; + + let rule = elementStyle.rules[1]; + + for (let i = 0; i < expected.length; ++i) { + let prop = rule.textProps[i]; + is(prop.name, expected[i].name, "test name for prop " + i); + is(prop.overridden, expected[i].overridden, + "test overridden for prop " + i); + } +} + +function* overrideTest() { + let gradientText = "(45deg, rgba(255,255,255,0.2) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.2) 50%, rgba(255,255,255,0.2) 75%, transparent 75%, transparent);"; + + let view = + yield createTestContent("#testid {" + + " background-image: -moz-linear-gradient" + + gradientText + + " background-image: -webkit-linear-gradient" + + gradientText + + " background-image: linear-gradient" + + gradientText + + "} "); + + let elementStyle = view._elementStyle; + let rule = elementStyle.rules[1]; + + // Initially the last property should be active. + for (let i = 0; i < 3; ++i) { + let prop = rule.textProps[i]; + is(prop.name, "background-image", "check the property name"); + is(prop.overridden, i !== 2, "check overridden for " + i); + } + + rule.textProps[2].setEnabled(false); + yield rule._applyingModifications; + + // Now the first property should be active. + for (let i = 0; i < 3; ++i) { + let prop = rule.textProps[i]; + is(prop.overridden || !prop.enabled, i !== 0, + "post-change check overridden for " + i); + } +} + +function* colorEditingTest() { + let colors = [ + {name: "hex", text: "#f0c", result: "#0F0"}, + {name: "rgb", text: "rgb(0,128,250)", result: "rgb(0, 255, 0)"} + ]; + + Services.prefs.setCharPref("devtools.defaultColorUnit", "authored"); + + for (let color of colors) { + let view = yield createTestContent("#testid {" + + " color: " + color.text + ";" + + "} "); + + let cPicker = view.tooltips.colorPicker; + let swatch = getRuleViewProperty(view, "#testid", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + let onShown = cPicker.tooltip.once("shown"); + swatch.click(); + yield onShown; + + let testNode = yield getNode("#testid"); + + yield simulateColorPickerChange(view, cPicker, [0, 255, 0, 1], { + element: testNode, + name: "color", + value: "rgb(0, 255, 0)" + }); + + let spectrum = yield cPicker.spectrum; + let onHidden = cPicker.tooltip.once("hidden"); + EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView); + yield onHidden; + + is(getRuleViewPropertyValue(view, "#testid", "color"), color.result, + "changing the color preserved the unit for " + color.name); + } +}
--- a/devtools/client/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_01.js +++ b/devtools/client/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_01.js @@ -16,17 +16,17 @@ const TEST_URI = ` </style> Testing the color picker tooltip! `; add_task(function*() { yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); let {view} = yield openRuleView(); let value = getRuleViewProperty(view, "body", "background").valueSpan; - let swatch = value.querySelectorAll(".ruleview-colorswatch")[1]; + let swatch = value.querySelectorAll(".ruleview-colorswatch")[0]; let url = value.querySelector(".theme-link"); yield testImageTooltipAfterColorChange(swatch, url, view); }); function* testImageTooltipAfterColorChange(swatch, url, ruleView) { info("First, verify that the image preview tooltip works"); let anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip, url);
--- a/devtools/client/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_02.js +++ b/devtools/client/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_02.js @@ -56,12 +56,12 @@ function* testColorChangeIsntRevertedWhe onShown = ruleView.tooltips.previewTooltip.once("shown"); let anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip, url); ruleView.tooltips.previewTooltip.show(anchor); yield onShown; info("Image tooltip is shown, verify that the swatch is still correct"); swatch = value.querySelector(".ruleview-colorswatch"); - is(swatch.style.backgroundColor, "rgb(0, 0, 0)", + is(swatch.style.backgroundColor, "black", "The swatch's color is correct"); - is(swatch.nextSibling.textContent, "#000", "The color name is correct"); + is(swatch.nextSibling.textContent, "black", "The color name is correct"); }
--- a/devtools/client/styleinspector/test/browser_ruleview_colorpicker-edit-gradient.js +++ b/devtools/client/styleinspector/test/browser_ruleview_colorpicker-edit-gradient.js @@ -34,17 +34,17 @@ function testColorParsing(view) { let swatchEls = ruleEl.valueSpan.querySelectorAll(".ruleview-colorswatch"); ok(swatchEls, "The color swatch elements were found"); is(swatchEls.length, 3, "There are 3 color swatches"); let colorEls = ruleEl.valueSpan.querySelectorAll(".ruleview-color"); ok(colorEls, "The color elements were found"); is(colorEls.length, 3, "There are 3 color values"); - let colors = ["#F06", "#333", "#000"]; + let colors = ["#f06", "#333", "#000"]; for (let i = 0; i < colors.length; i++) { is(colorEls[i].textContent, colors[i], "The right color value was found"); } } function* testPickingNewColor(view) { // Grab the first color swatch and color in the gradient let ruleEl = getRuleViewProperty(view, "body", "background-image");
--- a/devtools/client/styleinspector/test/browser_ruleview_cycle-color.js +++ b/devtools/client/styleinspector/test/browser_ruleview_cycle-color.js @@ -22,17 +22,17 @@ add_task(function*() { checkColorCycling(container, inspector); }); function checkColorCycling(container, inspector) { let swatch = container.querySelector(".ruleview-colorswatch"); let valueNode = container.querySelector(".ruleview-color"); let win = inspector.sidebar.getWindowForTab("ruleview"); - // Hex (default) + // Hex is(valueNode.textContent, "#F00", "Color displayed as a hex value."); // HSL EventUtils.synthesizeMouseAtCenter(swatch, {type: "mousedown", shiftKey: true}, win); is(valueNode.textContent, "hsl(0, 100%, 50%)", "Color displayed as an HSL value."); @@ -43,20 +43,21 @@ function checkColorCycling(container, in "Color displayed as an RGB value."); // Color name EventUtils.synthesizeMouseAtCenter(swatch, {type: "mousedown", shiftKey: true}, win); is(valueNode.textContent, "red", "Color displayed as a color name."); - // "Authored" (currently the computed value) - EventUtils.synthesizeMouseAtCenter(swatch, - {type: "mousedown", shiftKey: true}, win); - is(valueNode.textContent, "rgb(255, 0, 0)", - "Color displayed as an RGB value."); - - // Back to hex + // "Authored" EventUtils.synthesizeMouseAtCenter(swatch, {type: "mousedown", shiftKey: true}, win); is(valueNode.textContent, "#F00", - "Color displayed as hex again."); + "Color displayed as an authored value."); + + // One more click skips hex, because it is the same as authored, and + // instead goes back to HSL. + EventUtils.synthesizeMouseAtCenter(swatch, + {type: "mousedown", shiftKey: true}, win); + is(valueNode.textContent, "hsl(0, 100%, 50%)", + "Color displayed as an HSL value again."); }
--- a/devtools/client/styleinspector/test/browser_ruleview_edit-property-remove_02.js +++ b/devtools/client/styleinspector/test/browser_ruleview_edit-property-remove_02.js @@ -43,19 +43,20 @@ function* testEditPropertyAndRemove(insp propEditor = ruleEditor.rule.textProps[0].editor; let editor = inplaceEditor(view.styleDocument.activeElement); is(inplaceEditor(propEditor.nameSpan), editor, "Focus should have moved to the next property name"); info("Focus the property value and remove the property"); + let onChanged = view.once("ruleview-changed"); yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["VK_TAB", "VK_DELETE", "VK_RETURN"]); - yield ruleEditor.rule._applyingModifications; + yield onChanged; newValue = yield executeInContent("Test:GetRulePropertyValue", { styleSheetIndex: 0, ruleIndex: 0, name: "color" }); is(newValue, "", "color should have been unset.");
--- a/devtools/client/styleinspector/test/browser_ruleview_edit-property_01.js +++ b/devtools/client/styleinspector/test/browser_ruleview_edit-property_01.js @@ -54,32 +54,31 @@ function* testEditProperty(ruleEditor, n is(inplaceEditor(propEditor.nameSpan), editor, "The property name editor got focused"); let input = editor.input; info("Entering a new property name, including : to commit and " + "focus the value"); let onValueFocus = once(ruleEditor.element, "focus", true); - let onModifications = ruleEditor.rule._applyingModifications; + let onModifications = ruleEditor.ruleView.once("ruleview-changed"); EventUtils.sendString(name + ":", doc.defaultView); yield onValueFocus; yield onModifications; // Getting the value editor after focus editor = inplaceEditor(doc.activeElement); input = editor.input; is(inplaceEditor(propEditor.valueSpan), editor, "Focus moved to the value."); info("Entering a new value, including ; to commit and blur the value"); let onBlur = once(input, "blur"); - onModifications = ruleEditor.rule._applyingModifications; EventUtils.sendString(value + ";", doc.defaultView); yield onBlur; - yield onModifications; + yield ruleEditor.rule._applyingModifications; is(propEditor.isValid(), isValid, value + " is " + isValid ? "valid" : "invalid"); info("Checking that the style property was changed on the content page"); let propValue = yield executeInContent("Test:GetRulePropertyValue", { styleSheetIndex: 0, ruleIndex: 0,
--- a/devtools/client/styleinspector/test/browser_ruleview_edit-property_03.js +++ b/devtools/client/styleinspector/test/browser_ruleview_edit-property_03.js @@ -28,29 +28,29 @@ add_task(function*() { yield selectNode("#testid", inspector); let ruleEditor = getRuleViewRuleEditor(view, 1); let propEditor = ruleEditor.rule.textProps[1].editor; yield focusEditableField(view, propEditor.valueSpan); info("Deleting all the text out of a value field"); + let waitForUpdates = view.once("ruleview-changed"); yield sendCharsAndWaitForFocus(view, ruleEditor.element, ["VK_DELETE", "VK_RETURN"]); + yield waitForUpdates; info("Pressing enter a couple times to cycle through editors"); yield sendCharsAndWaitForFocus(view, ruleEditor.element, ["VK_RETURN"]); yield sendCharsAndWaitForFocus(view, ruleEditor.element, ["VK_RETURN"]); isnot(ruleEditor.rule.textProps[1].editor.nameSpan.style.display, "none", "The name span is visible"); is(ruleEditor.rule.textProps.length, 2, "Correct number of props"); }); function* sendCharsAndWaitForFocus(view, element, chars) { let onFocus = once(element, "focus", true); for (let ch of chars) { - let onRuleViewChanged = view.once("ruleview-changed"); EventUtils.sendChar(ch, element.ownerDocument.defaultView); - yield onRuleViewChanged; } yield onFocus; }
--- a/devtools/client/styleinspector/test/browser_ruleview_edit-property_07.js +++ b/devtools/client/styleinspector/test/browser_ruleview_edit-property_07.js @@ -5,17 +5,17 @@ "use strict"; // Tests that adding multiple values will enable the property even if the // property does not change, and that the extra values are added correctly. const TEST_URI = ` <style type='text/css'> #testid { - background-color: red; + background-color: #f00; } </style> <div id='testid'>Styled Node</div> `; add_task(function*() { yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); let {inspector, view} = yield openRuleView();
--- a/devtools/client/styleinspector/test/browser_ruleview_edit-selector_05.js +++ b/devtools/client/styleinspector/test/browser_ruleview_edit-selector_05.js @@ -93,16 +93,16 @@ function* testAddProperty(view) { is(ruleEditor.rule.textProps.length, 1, "Created a new text property."); is(ruleEditor.propertyList.children.length, 1, "Created a property editor."); is(editor, inplaceEditor(textProp.editor.valueSpan), "Editing the value span now."); info("Entering a value and bluring the field to expect a rule change"); editor.input.value = "center"; let onBlur = once(editor.input, "blur"); - onRuleViewChanged = view.once("ruleview-changed"); + onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2); editor.input.blur(); yield onBlur; yield onRuleViewChanged; is(textProp.value, "center", "Text prop should have been changed."); is(textProp.overridden, false, "Property should not be overridden"); }
new file mode 100644 --- /dev/null +++ b/devtools/client/styleinspector/test/browser_ruleview_guessIndentation.js @@ -0,0 +1,76 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that we can guess indentation from a style sheet, not just a +// rule. + +// Needed for openStyleEditor. +Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleeditor/test/head.js", this); + +// Use a weird indentation depth to avoid accidental success. +const TEST_URI = ` + <style type='text/css'> +div { + background-color: blue; +} + +* { +} +</style> + <div id='testid' class='testclass'>Styled Node</div> +`; + +const expectedText = ` +div { + background-color: blue; +} + +* { + color: chartreuse; +} +`; + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testIndentation(inspector, view); +}); + +function* testIndentation(inspector, view) { + let ruleEditor = getRuleViewRuleEditor(view, 2); + + info("Focusing a new property name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.closeBrace); + + let input = editor.input; + + info("Entering color in the property name editor"); + input.value = "color"; + + info("Pressing return to commit and focus the new value field"); + let onValueFocus = once(ruleEditor.element, "focus", true); + let onModifications = ruleEditor.rule._applyingModifications; + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onValueFocus; + yield onModifications; + + // Getting the new value editor after focus + editor = inplaceEditor(view.styleDocument.activeElement); + info("Entering a value and bluring the field to expect a rule change"); + editor.input.value = "chartreuse"; + let onBlur = once(editor.input, "blur"); + onModifications = ruleEditor.rule._applyingModifications; + editor.input.blur(); + yield onBlur; + yield onModifications; + + let { ui } = yield openStyleEditor(); + + let styleEditor = yield ui.editors[0].getSourceEditor(); + let text = styleEditor.sourceEditor.getText(); + is(text, expectedText, "style inspector changes are synced"); +}
--- a/devtools/client/styleinspector/test/browser_ruleview_inherited-properties_01.js +++ b/devtools/client/styleinspector/test/browser_ruleview_inherited-properties_01.js @@ -33,13 +33,17 @@ function* simpleInherit(inspector, view) let elementRule = elementStyle.rules[0]; ok(!elementRule.inherited, "Element style attribute should not consider itself inherited."); let inheritRule = elementStyle.rules[1]; is(inheritRule.selectorText, "#test2", "Inherited rule should be the one that includes inheritable properties."); ok(!!inheritRule.inherited, "Rule should consider itself inherited."); - is(inheritRule.textProps.length, 1, - "Should only display one inherited style"); - let inheritProp = inheritRule.textProps[0]; + is(inheritRule.textProps.length, 2, + "Rule should have two styles"); + let bgcProp = inheritRule.textProps[0]; + is(bgcProp.name, "background-color", + "background-color property should exist"); + ok(bgcProp.invisible, "background-color property should be invisible"); + let inheritProp = inheritRule.textProps[1]; is(inheritProp.name, "color", "color should have been inherited."); }
new file mode 100644 --- /dev/null +++ b/devtools/client/styleinspector/test/browser_ruleview_lineNumbers.js @@ -0,0 +1,52 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that editing a rule will update the line numbers of subsequent +// rules in the rule view. + +const TESTCASE_URI = TEST_URL_ROOT + "doc_ruleLineNumbers.html"; + +add_task(function*() { + yield addTab(TESTCASE_URI); + let { inspector, view } = yield openRuleView(); + yield selectNode("#testid", inspector); + let elementRuleEditor = getRuleViewRuleEditor(view, 1); + + let bodyRuleEditor = getRuleViewRuleEditor(view, 3); + let value = getRuleViewLinkTextByIndex(view, 2); + // Note that this is relative to the <style>. + is(value.slice(-2), ":6", "initial rule line number is 6"); + + info("Focusing a new property name in the rule-view"); + let editor = yield focusEditableField(view, elementRuleEditor.closeBrace); + + is(inplaceEditor(elementRuleEditor.newPropSpan), editor, + "The new property editor got focused"); + let input = editor.input; + + info("Entering font-size in the property name editor"); + input.value = "font-size"; + + info("Pressing return to commit and focus the new value field"); + let onLocationChanged = once(bodyRuleEditor.rule.domRule, "location-changed"); + let onValueFocus = once(elementRuleEditor.element, "focus", true); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onValueFocus; + yield elementRuleEditor.rule._applyingModifications; + + // Getting the new value editor after focus + editor = inplaceEditor(view.styleDocument.activeElement); + info("Entering a value and bluring the field to expect a rule change"); + editor.input.value = "23px"; + editor.input.blur(); + yield elementRuleEditor.rule._applyingModifications; + + yield onLocationChanged; + + let newBodyTitle = getRuleViewLinkTextByIndex(view, 2); + // Note that this is relative to the <style>. + is(newBodyTitle.slice(-2), ":7", "updated rule line number is 7"); +});
--- a/devtools/client/styleinspector/test/browser_ruleview_mark_overridden_05.js +++ b/devtools/client/styleinspector/test/browser_ruleview_mark_overridden_05.js @@ -22,15 +22,16 @@ add_task(function*() { yield selectNode("#testid", inspector); yield testMarkOverridden(inspector, view); }); function* testMarkOverridden(inspector, view) { let ruleEditor = getRuleViewRuleEditor(view, 1); yield createNewRuleViewProperty(ruleEditor, "background-color: red;"); + yield ruleEditor.rule._applyingModifications; let firstProp = ruleEditor.rule.textProps[0]; let secondProp = ruleEditor.rule.textProps[1]; ok(firstProp.overridden, "First property should be overridden."); ok(!secondProp.overridden, "Second property should not be overridden."); }
new file mode 100644 --- /dev/null +++ b/devtools/client/styleinspector/test/browser_ruleview_mark_overridden_06.js @@ -0,0 +1,60 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the rule view marks overridden rules correctly after +// editing the selector. + +const TEST_URI = ` + <style type='text/css'> + div { + background-color: blue; + background-color: chartreuse; + } + </style> + <div id='testid' class='testclass'>Styled Node</div> +`; + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testMarkOverridden(inspector, view); +}); + +function* testMarkOverridden(inspector, view) { + let elementStyle = view._elementStyle; + let rule = elementStyle.rules[1]; + checkProperties(rule); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + info("Entering a new selector name and committing"); + editor.input.value = "div[class]"; + + let onRuleViewChanged = once(view, "ruleview-changed"); + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + view.searchField.focus(); + checkProperties(rule); +} + +// A helper to perform a repeated set of checks. +function checkProperties(rule) { + let prop = rule.textProps[0]; + is(prop.name, "background-color", + "First property should be background-color"); + is(prop.value, "blue", "First property value should be blue"); + ok(prop.overridden, "prop should be overridden."); + prop = rule.textProps[1]; + is(prop.name, "background-color", + "Second property should be background-color"); + is(prop.value, "chartreuse", "First property value should be chartreuse"); + ok(!prop.overridden, "prop should not be overridden."); +}
--- a/devtools/client/styleinspector/test/browser_ruleview_mark_overridden_07.js +++ b/devtools/client/styleinspector/test/browser_ruleview_mark_overridden_07.js @@ -46,17 +46,18 @@ function* testMarkOverridden(inspector, let RESULTS = [ // We skip the first element [], [{name: "margin-left", value: "23px", overridden: true}], [{name: "margin-right", value: "23px", overridden: false}, {name: "margin-left", value: "1px", overridden: false}], [{name: "font-size", value: "12px", overridden: false}], - [{name: "font-size", value: "79px", overridden: true}] + [{name: "margin-right", value: "1px", overridden: true}, + {name: "font-size", value: "79px", overridden: true}] ]; for (let i = 1; i < RESULTS.length; ++i) { let idRule = elementStyle.rules[i]; for (let propIndex in RESULTS[i]) { let expected = RESULTS[i][propIndex]; let prop = idRule.textProps[propIndex];
--- a/devtools/client/styleinspector/test/browser_ruleview_media-queries.js +++ b/devtools/client/styleinspector/test/browser_ruleview_media-queries.js @@ -19,12 +19,11 @@ add_task(function*() { let _strings = Services.strings .createBundle("chrome://global/locale/devtools/styleinspector.properties"); let inline = _strings.GetStringFromName("rule.sourceInline"); is(elementStyle.rules.length, 3, "Should have 3 rules."); is(elementStyle.rules[0].title, inline, "check rule 0 title"); is(elementStyle.rules[1].title, inline + - ":15 @media screen and (min-width: 1px)", "check rule 1 title"); - is(elementStyle.rules[2].title, inline + ":8", "check rule 2 title"); + ":9 @media screen and (min-width: 1px)", "check rule 1 title"); + is(elementStyle.rules[2].title, inline + ":2", "check rule 2 title"); }); -
--- a/devtools/client/styleinspector/test/browser_ruleview_multiple-properties-unfinished_01.js +++ b/devtools/client/styleinspector/test/browser_ruleview_multiple-properties-unfinished_01.js @@ -1,44 +1,32 @@ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; -// Test that the rule-view behaves correctly when entering mutliple and/or +// Test that the rule-view behaves correctly when entering multiple and/or // unfinished properties/values in inplace-editors const TEST_URI = "<div>Test Element</div>"; add_task(function*() { yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); let {inspector, view} = yield openRuleView(); yield selectNode("div", inspector); yield testCreateNewMultiUnfinished(inspector, view); }); -function waitRuleViewChanged(view, n) { - let deferred = promise.defer(); - let count = 0; - let listener = function() { - if (++count == n) { - view.off("ruleview-changed", listener); - deferred.resolve(); - } - }; - view.on("ruleview-changed", listener); - return deferred.promise; -} function* testCreateNewMultiUnfinished(inspector, view) { let ruleEditor = getRuleViewRuleEditor(view, 0); let onMutation = inspector.once("markupmutation"); - // There is 5 rule-view updates, one for the rule view creation, - // one for each new property - let onRuleViewChanged = waitRuleViewChanged(view, 5); + // There are 2 rule-view updates: one for the preview and one for + // the final commit. + let onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2); yield createNewRuleViewProperty(ruleEditor, "color:blue;background : orange ; text-align:center; border-color: "); yield onMutation; yield onRuleViewChanged; is(ruleEditor.rule.textProps.length, 4, "Should have created new text properties."); is(ruleEditor.propertyList.children.length, 4,
--- a/devtools/client/styleinspector/test/browser_ruleview_pseudo-element_01.js +++ b/devtools/client/styleinspector/test/browser_ruleview_pseudo-element_01.js @@ -161,27 +161,27 @@ function* testParagraph(inspector, view) firstLetterRulesNb: 1, selectionRulesNb: 1 }); assertGutters(view); let elementFirstLineRule = rules.firstLineRules[0]; is(convertTextPropsToString(elementFirstLineRule.textProps), - "background: blue none repeat scroll 0% 0%", + "background: blue", "Paragraph first-line properties are correct"); let elementFirstLetterRule = rules.firstLetterRules[0]; is(convertTextPropsToString(elementFirstLetterRule.textProps), "color: red; font-size: 130%", "Paragraph first-letter properties are correct"); let elementSelectionRule = rules.selectionRules[0]; is(convertTextPropsToString(elementSelectionRule.textProps), - "color: white; background: black none repeat scroll 0% 0%", + "color: white; background: black", "Paragraph first-letter properties are correct"); } function* testBody(inspector, view) { yield testNode("body", inspector, view); let gutters = getGutters(view); is(gutters.length, 0, "There are no gutter headings");
--- a/devtools/client/styleinspector/test/browser_ruleview_refresh-on-attribute-change_02.js +++ b/devtools/client/styleinspector/test/browser_ruleview_refresh-on-attribute-change_02.js @@ -126,30 +126,28 @@ function* testPropertyChange6(inspector, "url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%", inspector); let rule = ruleView._elementStyle.rules[0]; is(rule.editor.element.querySelectorAll(".ruleview-property").length, 5, "Added a property"); validateTextProp(rule.textProps[4], true, "background", "red url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%", - "shortcut property correctly set", - "#F00 url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%"); + "shortcut property correctly set"); } function* changeElementStyle(testElement, style, inspector) { let onRefreshed = inspector.once("rule-view-refreshed"); testElement.setAttribute("style", style); yield onRefreshed; } -function validateTextProp(aProp, aEnabled, aName, aValue, aDesc, - valueSpanText) { +function validateTextProp(aProp, aEnabled, aName, aValue, aDesc) { is(aProp.enabled, aEnabled, aDesc + ": enabled."); is(aProp.name, aName, aDesc + ": name."); is(aProp.value, aValue, aDesc + ": value."); is(aProp.editor.enable.hasAttribute("checked"), aEnabled, aDesc + ": enabled checkbox."); is(aProp.editor.nameSpan.textContent, aName, aDesc + ": name span."); is(aProp.editor.valueSpan.textContent, - valueSpanText || aValue, aDesc + ": value span."); + aValue, aDesc + ": value span."); }
--- a/devtools/client/styleinspector/test/browser_ruleview_search-filter-computed-list_03.js +++ b/devtools/client/styleinspector/test/browser_ruleview_search-filter-computed-list_03.js @@ -2,22 +2,24 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Tests that the rule view search filter works properly in the computed list // for color values. -const SEARCH = "background-color: #F3F3F3"; +// The color format here is chosen to match the default returned by +// CssColor.toString. +const SEARCH = "background-color: rgb(243, 243, 243)"; const TEST_URI = ` <style type="text/css"> .testclass { - background: #F3F3F3 none repeat scroll 0% 0%; + background: rgb(243, 243, 243) none repeat scroll 0% 0%; } </style> <div class="testclass">Styled Node</h1> `; add_task(function*() { yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); let {inspector, view} = yield openRuleView();
--- a/devtools/client/styleinspector/test/browser_ruleview_search-filter-computed-list_04.js +++ b/devtools/client/styleinspector/test/browser_ruleview_search-filter-computed-list_04.js @@ -38,17 +38,17 @@ function* testModifyPropertyValueFilter( is(rule.selectorText, "#testid", "Second rule is #testid."); ok(!propEditor.container.classList.contains("ruleview-highlight"), "margin text property is not highlighted."); ok(rule.textProps[1].editor.container.classList .contains("ruleview-highlight"), "top text property is correctly highlighted."); let onBlur = once(editor.input, "blur"); - let onModification = rule._applyingModifications; + let onModification = view.once("ruleview-changed"); EventUtils.sendString("4px 0px", view.styleWindow); EventUtils.synthesizeKey("VK_RETURN", {}); yield onBlur; yield onModification; ok(propEditor.container.classList.contains("ruleview-highlight"), "margin text property is correctly highlighted."); ok(!computed.hasAttribute("filter-open"), "margin computed list is closed.");
--- a/devtools/client/styleinspector/test/browser_ruleview_select-and-copy-styles.js +++ b/devtools/client/styleinspector/test/browser_ruleview_select-and-copy-styles.js @@ -59,20 +59,20 @@ function* checkCopySelection(view) { range.setEnd(values[4], 2); win.getSelection().addRange(range); info("Checking that _Copy() returns the correct clipboard value"); let expectedPattern = " margin: 10em;[\\r\\n]+" + " font-size: 14pt;[\\r\\n]+" + " font-family: helvetica,sans-serif;[\\r\\n]+" + - " color: #AAA;[\\r\\n]+" + + " color: rgb\\(170, 170, 170\\);[\\r\\n]+" + "}[\\r\\n]+" + "html {[\\r\\n]+" + - " color: #000;[\\r\\n]*"; + " color: #000000;[\\r\\n]*"; let onPopup = once(view._contextmenu._menupopup, "popupshown"); EventUtils.synthesizeMouseAtCenter(prop, {button: 2, type: "contextmenu"}, win); yield onPopup; ok(!view._contextmenu.menuitemCopy.hidden, "Copy menu item is not hidden as expected"); @@ -97,20 +97,20 @@ function* checkSelectAll(view) { info("Checking that _SelectAll() then copy returns the correct " + "clipboard value"); view._contextmenu._onSelectAll(); let expectedPattern = "[\\r\\n]+" + "element {[\\r\\n]+" + " margin: 10em;[\\r\\n]+" + " font-size: 14pt;[\\r\\n]+" + " font-family: helvetica,sans-serif;[\\r\\n]+" + - " color: #AAA;[\\r\\n]+" + + " color: rgb\\(170, 170, 170\\);[\\r\\n]+" + "}[\\r\\n]+" + "html {[\\r\\n]+" + - " color: #000;[\\r\\n]+" + + " color: #000000;[\\r\\n]+" + "}[\\r\\n]*"; let onPopup = once(view._contextmenu._menupopup, "popupshown"); EventUtils.synthesizeMouseAtCenter(prop, {button: 2, type: "contextmenu"}, win); yield onPopup; ok(!view._contextmenu.menuitemCopy.hidden,
--- a/devtools/client/styleinspector/test/browser_ruleview_user-agent-styles-uneditable.js +++ b/devtools/client/styleinspector/test/browser_ruleview_user-agent-styles-uneditable.js @@ -35,19 +35,21 @@ function* userAgentStylesUneditable(insp yield selectNode("a", inspector); let uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable); for (let rule of uaRules) { ok(rule.editor.element.hasAttribute("uneditable"), "UA rules have uneditable attribute"); - ok(!rule.textProps[0].editor.nameSpan._editable, + let firstProp = rule.textProps.filter(p => !p.invisible)[0]; + + ok(!firstProp.editor.nameSpan._editable, "nameSpan is not editable"); - ok(!rule.textProps[0].editor.valueSpan._editable, + ok(!firstProp.editor.valueSpan._editable, "valueSpan is not editable"); ok(!rule.editor.closeBrace._editable, "closeBrace is not editable"); let colorswatch = rule.editor.element .querySelector(".ruleview-colorswatch"); if (colorswatch) { ok(!view.tooltips.colorPicker.swatches.has(colorswatch), "The swatch is not editable");
--- a/devtools/client/styleinspector/test/browser_styleinspector_context-menu-copy-color_01.js +++ b/devtools/client/styleinspector/test/browser_styleinspector_context-menu-copy-color_01.js @@ -80,17 +80,17 @@ function testIsColorPopupOnNode(view, no info("Testing node " + node); view.styleDocument.popupNode = node; view._contextmenu._colorToCopy = ""; let result = view._contextmenu._isColorPopup(); let correct = isColorValueNode(node); is(result, correct, "_isColorPopup returned the expected value " + correct); - is(view._contextmenu._colorToCopy, (correct) ? "#123ABC" : "", + is(view._contextmenu._colorToCopy, (correct) ? "rgb(18, 58, 188)" : "", "_colorToCopy was set to the expected value"); } /** * Check if a node is part of color value i.e. it has parent with a 'data-color' * attribute. */ function isColorValueNode(node) {
--- a/devtools/client/styleinspector/test/browser_styleinspector_refresh_when_active.js +++ b/devtools/client/styleinspector/test/browser_styleinspector_refresh_when_active.js @@ -11,17 +11,17 @@ const TEST_URI = ` <div id="two" style="color:blue;">two</div> `; add_task(function*() { yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); let {inspector, view} = yield openRuleView(); yield selectNode("#one", inspector); - is(getRuleViewPropertyValue(view, "element", "color"), "#F00", + is(getRuleViewPropertyValue(view, "element", "color"), "red", "The rule-view shows the properties for test node one"); let cView = inspector.sidebar.getWindowForTab("computedview") .computedview.view; let prop = getComputedViewProperty(cView, "color"); ok(!prop, "The computed-view doesn't show the properties for test node one"); info("Switching to the computed-view"); @@ -33,11 +33,11 @@ add_task(function*() { "The computed-view shows the properties for test node one"); info("Selecting test node two"); yield selectNode("#two", inspector); ok(getComputedViewPropertyValue(cView, "color"), "#00F", "The computed-view shows the properties for test node two"); - is(getRuleViewPropertyValue(view, "element", "color"), "#F00", + is(getRuleViewPropertyValue(view, "element", "color"), "red", "The rule-view doesn't the properties for test node two"); });
new file mode 100644 --- /dev/null +++ b/devtools/client/styleinspector/test/doc_ruleLineNumbers.html @@ -0,0 +1,19 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>simple testcase</title> + <style type="text/css"> + #testid { + background-color: seagreen; + } + + body { + color: chartreuse; + } + </style> +</head> +<body> + <div id="testid">simple testcase</div> +</body> +</html>
--- a/devtools/client/styleinspector/test/head.js +++ b/devtools/client/styleinspector/test/head.js @@ -302,46 +302,66 @@ function openComputedView() { * @return a promise that resolves when the inspector is ready and the rule * view is visible and ready */ function openRuleView() { return openInspectorSideBar("ruleview"); } /** + * Wait for eventName on target to be delivered a number of times. + * + * @param {Object} target + * An observable object that either supports on/off or + * addEventListener/removeEventListener + * @param {String} eventName + * @param {Number} numTimes + * Number of deliveries to wait for. + * @param {Boolean} useCapture + * Optional, for addEventListener/removeEventListener + * @return A promise that resolves when the event has been handled + */ +function waitForNEvents(target, eventName, numTimes, useCapture = false) { + info("Waiting for event: '" + eventName + "' on " + target + "."); + + let deferred = promise.defer(); + let count = 0; + + for (let [add, remove] of [ + ["addEventListener", "removeEventListener"], + ["addListener", "removeListener"], + ["on", "off"] + ]) { + if ((add in target) && (remove in target)) { + target[add](eventName, function onEvent(...aArgs) { + if (++count == numTimes) { + target[remove](eventName, onEvent, useCapture); + deferred.resolve.apply(deferred, aArgs); + } + }, useCapture); + break; + } + } + + return deferred.promise; +} + +/** * Wait for eventName on target. * * @param {Object} target * An observable object that either supports on/off or * addEventListener/removeEventListener * @param {String} eventName * @param {Boolean} useCapture * Optional, for addEventListener/removeEventListener * @return A promise that resolves when the event has been handled */ function once(target, eventName, useCapture=false) { - info("Waiting for event: '" + eventName + "' on " + target + "."); - - let deferred = promise.defer(); - - for (let [add, remove] of [ - ["addEventListener", "removeEventListener"], - ["addListener", "removeListener"], - ["on", "off"] - ]) { - if ((add in target) && (remove in target)) { - target[add](eventName, function onEvent(...aArgs) { - target[remove](eventName, onEvent, useCapture); - deferred.resolve.apply(deferred, aArgs); - }, useCapture); - break; - } - } - - return deferred.promise; + return waitForNEvents(target, eventName, 1, useCapture); } /** * This shouldn't be used in the tests, but is useful when writing new tests or * debugging existing tests in order to introduce delays in the test steps * * @param {Number} ms * The time to wait
--- a/devtools/server/actors/animation.js +++ b/devtools/server/actors/animation.js @@ -409,16 +409,30 @@ var AnimationPlayerActor = ActorClass({ */ setPlaybackRate: method(function(playbackRate) { this.player.playbackRate = playbackRate; }, { request: { currentTime: Arg(0, "number") }, response: {} + }), + + /** + * Get data about the keyframes of this animation player. + * @return {Object} Returns a list of frames, each frame containing the list + * animated properties as well as the frame's offset. + */ + getFrames: method(function() { + return this.player.effect.getFrames(); + }, { + request: {}, + response: { + frames: RetVal("json") + } }) }); var AnimationPlayerFront = FrontClass(AnimationPlayerActor, { initialize: function(conn, form, detail, ctx) { Front.prototype.initialize.call(this, conn, form, detail, ctx); this.state = {};
--- a/devtools/server/actors/styles.js +++ b/devtools/server/actors/styles.js @@ -9,26 +9,31 @@ const {Cc, Ci, Cu} = require("chrome"); const promise = require("promise"); const protocol = require("devtools/server/protocol"); const {Arg, Option, method, RetVal, types} = protocol; const events = require("sdk/event/core"); const {Class} = require("sdk/core/heritage"); const {LongStringActor} = require("devtools/server/actors/string"); const {PSEUDO_ELEMENT_SET} = require("devtools/shared/styleinspector/css-logic"); -// This will add the "stylesheet" actor type for protocol.js to recognize -require("devtools/server/actors/stylesheets"); +// This will also add the "stylesheet" actor type for protocol.js to recognize +const {UPDATE_PRESERVING_RULES, UPDATE_GENERAL} = + require("devtools/server/actors/stylesheets"); loader.lazyGetter(this, "CssLogic", () => { return require("devtools/shared/styleinspector/css-logic").CssLogic; }); loader.lazyGetter(this, "DOMUtils", () => { return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); }); +loader.lazyGetter(this, "RuleRewriter", () => { + return require("devtools/client/styleinspector/css-parsing-utils").RuleRewriter; +}); + // The PageStyle actor flattens the DOM CSS objects a little bit, merging // Rules and their Styles into one actor. For elements (which have a style // but no associated rule) we fake a rule with the following style id. const ELEMENT_STYLE = 100; exports.ELEMENT_STYLE = ELEMENT_STYLE; // Not included since these are uneditable by the user. // See https://hg.mozilla.org/mozilla-central/file/696a4ad5d011/layout/style/nsCSSPseudoElementList.h#l74 @@ -123,16 +128,23 @@ types.addDictType("fontface", { /** * The PageStyle actor lets the client look at the styles on a page, as * they are applied to a given node. */ var PageStyleActor = protocol.ActorClass({ typeName: "pagestyle", + events: { + "stylesheet-updated": { + type: "styleSheetUpdated", + styleSheet: Arg(0, "stylesheet") + } + }, + /** * Create a PageStyleActor. * * @param inspector * The InspectorActor that owns this PageStyleActor. * * @constructor */ @@ -146,29 +158,37 @@ var PageStyleActor = protocol.ActorClass this.walker = inspector.walker; this.cssLogic = new CssLogic(); // Stores the association of DOM objects -> actors this.refMap = new Map(); this.onFrameUnload = this.onFrameUnload.bind(this); events.on(this.inspector.tabActor, "will-navigate", this.onFrameUnload); + + this._styleApplied = this._styleApplied.bind(this); + this._watchedSheets = new Set(); }, - destroy: function () { + destroy: function() { if (!this.walker) { return; } protocol.Actor.prototype.destroy.call(this); events.off(this.inspector.tabActor, "will-navigate", this.onFrameUnload); this.inspector = null; this.walker = null; this.refMap = null; this.cssLogic = null; this._styleElement = null; + + for (let sheet of this._watchedSheets) { + sheet.off("style-applied", this._styleApplied); + } + this._watchedSheets.clear(); }, get conn() { return this.inspector.conn; }, form: function(detail) { if (detail === "actorid") { @@ -177,46 +197,76 @@ var PageStyleActor = protocol.ActorClass return { actor: this.actorID, traits: { // Whether the actor has had bug 1103993 fixed, which means that the // getApplied method calls cssLogic.highlight(node) to recreate the // style cache. Clients requesting getApplied from actors that have not // been fixed must make sure cssLogic.highlight(node) was called before. - getAppliedCreatesStyleCache: true + getAppliedCreatesStyleCache: true, + // Whether addNewRule accepts the editAuthored argument. + authoredStyles: true } }; }, /** + * Called when a style sheet is updated. + */ + _styleApplied: function(kind, styleSheet) { + if (kind === UPDATE_GENERAL) { + events.emit(this, "stylesheet-updated", styleSheet); + } + }, + + /** * Return or create a StyleRuleActor for the given item. * @param item Either a CSSStyleRule or a DOM element. */ _styleRef: function(item) { if (this.refMap.has(item)) { return this.refMap.get(item); } let actor = StyleRuleActor(this, item); this.manage(actor); this.refMap.set(item, actor); return actor; }, /** + * Update the association between a StyleRuleActor and its + * corresponding item. This is used when a StyleRuleActor updates + * as style sheet and starts using a new rule. + * + * @param oldItem The old association; either a CSSStyleRule or a + * DOM element. + * @param item Either a CSSStyleRule or a DOM element. + * @param actor a StyleRuleActor + */ + updateStyleRef: function(oldItem, item, actor) { + this.refMap.delete(oldItem); + this.refMap.set(item, actor); + }, + + /** * Return or create a StyleSheetActor for the given nsIDOMCSSStyleSheet. * @param {DOMStyleSheet} sheet * The style sheet to create an actor for. * @return {StyleSheetActor} * The actor for this style sheet */ _sheetRef: function(sheet) { let tabActor = this.inspector.tabActor; let actor = tabActor.createStyleSheetActor(sheet); + if (!this._watchedSheets.has(actor)) { + this._watchedSheets.add(actor); + actor.on("style-applied", this._styleApplied); + } return actor; }, /** * Get the computed style for a node. * * @param NodeActor node * @param object options @@ -519,26 +569,32 @@ var PageStyleActor = protocol.ActorClass * `filter`: A string filter that affects the "matched" handling. * 'user': Include properties from user style sheets. * 'ua': Include properties from user and user-agent sheets. * Default value is 'ua' * `inherited`: Include styles inherited from parent nodes. * `matchedSelectors`: Include an array of specific selectors that * caused this rule to match its node. */ - getApplied: method(function(node, options) { + getApplied: method(Task.async(function*(node, options) { if (!node) { return {entries: [], rules: [], sheets: []}; } this.cssLogic.highlight(node.rawNode); let entries = []; entries = entries.concat(this._getAllElementRules(node, undefined, options)); - return this.getAppliedProps(node, entries, options); - }, { + + let result = this.getAppliedProps(node, entries, options); + for (let rule of result.rules) { + // See the comment in |form| to understand this. + yield rule.getAuthoredCssText(); + } + return result; + }), { request: { node: Arg(0, "domnode"), inherited: Option(1, "boolean"), matchedSelectors: Option(1, "boolean"), filter: Option(1, "string") }, response: RetVal("appliedStylesReturn") }), @@ -900,22 +956,27 @@ var PageStyleActor = protocol.ActorClass getNewAppliedProps: function(node, rule) { let ruleActor = this._styleRef(rule); return this.getAppliedProps(node, [{ rule: ruleActor }], { matchedSelectors: true }); }, /** * Adds a new rule, and returns the new StyleRuleActor. - * @param NodeActor node - * @param [string] pseudoClasses The list of pseudo classes to append to the - * new selector. - * @returns StyleRuleActor of the new rule + * @param {NodeActor} node + * @param {String} pseudoClasses The list of pseudo classes to append to the + * new selector. + * @param {Boolean} editAuthored + * True if the selector should be updated by editing the + * authored text; false if the selector should be updated via + * CSSOM. + * @returns {StyleRuleActor} the new rule */ - addNewRule: method(function(node, pseudoClasses) { + addNewRule: method(Task.async(function*(node, pseudoClasses, + editAuthored = false) { let style = this.styleElement; let sheet = style.sheet; let cssRules = sheet.cssRules; let rawNode = node.rawNode; let selector; if (rawNode.id) { selector = "#" + CSS.escape(rawNode.id); @@ -925,21 +986,32 @@ var PageStyleActor = protocol.ActorClass selector = rawNode.tagName.toLowerCase(); } if (pseudoClasses && pseudoClasses.length > 0) { selector += pseudoClasses.join(""); } let index = sheet.insertRule(selector + " {}", cssRules.length); - return this.getNewAppliedProps(node, cssRules.item(index)); - }, { + + // If inserting the rule succeeded, go ahead and edit the source + // text if requested. + if (editAuthored) { + let sheetActor = this._sheetRef(sheet); + let {str: authoredText} = yield sheetActor.getText(); + authoredText += "\n" + selector + " {\n" + "}"; + yield sheetActor.update(authoredText, false); + } + + return this.getNewAppliedProps(node, sheet.cssRules.item(index)); + }), { request: { node: Arg(0, "domnode"), - pseudoClasses: Arg(1, "nullable:array:string") + pseudoClasses: Arg(1, "nullable:array:string"), + editAuthored: Arg(2, "boolean") }, response: RetVal("appliedStylesReturn") }), }); exports.PageStyleActor = PageStyleActor; /** * Front object for the PageStyleActor @@ -961,16 +1033,20 @@ var PageStyleFront = protocol.FrontClass destroy: function() { protocol.Front.prototype.destroy.call(this); }, get walker() { return this.inspector.walker; }, + get supportsAuthoredStyles() { + return this._form.traits && this._form.traits.authoredStyles; + }, + getMatchedSelectors: protocol.custom(function(node, property, options) { return this._getMatchedSelectors(node, property, options).then(ret => { return ret.matched; }); }, { impl: "_getMatchedSelectors" }), @@ -984,17 +1060,23 @@ var PageStyleFront = protocol.FrontClass } let ret = yield this._getApplied(node, options); return ret.entries; }), { impl: "_getApplied" }), addNewRule: protocol.custom(function(node, pseudoClasses) { - return this._addNewRule(node, pseudoClasses).then(ret => { + let addPromise; + if (this.supportsAuthoredStyles) { + addPromise = this._addNewRule(node, pseudoClasses, true); + } else { + addPromise = this._addNewRule(node, pseudoClasses); + } + return addPromise.then(ret => { return ret.entries[0]; }); }, { impl: "_addNewRule" }) }); /** @@ -1002,29 +1084,44 @@ var PageStyleFront = protocol.FrontClass * * We slightly flatten the CSSOM for this actor, it represents * both the CSSRule and CSSStyle objects in one actor. For nodes * (which have a CSSStyle but no CSSRule) we create a StyleRuleActor * with a special rule type (100). */ var StyleRuleActor = protocol.ActorClass({ typeName: "domstylerule", + + events: { + "location-changed": { + type: "locationChanged", + line: Arg(0, "number"), + column: Arg(1, "number") + }, + }, + initialize: function(pageStyle, item) { protocol.Actor.prototype.initialize.call(this, null); this.pageStyle = pageStyle; this.rawStyle = item.style; + this._parentSheet = null; + this._onStyleApplied = this._onStyleApplied.bind(this); if (item instanceof (Ci.nsIDOMCSSRule)) { this.type = item.type; this.rawRule = item; if ((this.rawRule instanceof Ci.nsIDOMCSSStyleRule || this.rawRule instanceof Ci.nsIDOMMozCSSKeyframeRule) && - this.rawRule.parentStyleSheet) { - this.line = DOMUtils.getRuleLine(this.rawRule); + this.rawRule.parentStyleSheet) { + this.line = DOMUtils.getRelativeRuleLine(this.rawRule); this.column = DOMUtils.getRuleColumn(this.rawRule); + this._parentSheet = this.rawRule.parentStyleSheet; + this._computeRuleIndex(); + this.sheetActor = this.pageStyle._sheetRef(this._parentSheet); + this.sheetActor.on("style-applied", this._onStyleApplied); } } else { // Fake a rule this.type = ELEMENT_STYLE; this.rawNode = item; this.rawRule = { style: item.style, toString: function() { @@ -1042,24 +1139,38 @@ var StyleRuleActor = protocol.ActorClass if (!this.rawStyle) { return; } protocol.Actor.prototype.destroy.call(this); this.rawStyle = null; this.pageStyle = null; this.rawNode = null; this.rawRule = null; + if (this.sheetActor) { + this.sheetActor.off("style-applied", this._onStyleApplied); + } }, // Objects returned by this actor are owned by the PageStyleActor // to which this rule belongs. get marshallPool() { return this.pageStyle; }, + // True if this rule supports as-authored styles, meaning that the + // rule text can be rewritten using setRuleText. + get canSetRuleText() { + // Special case about:PreferenceStyleSheet, as it is + // generated on the fly and the URI is not registered with the + // about: handler. + // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37 + return !!(this._parentSheet && + this._parentSheet.href !== "about:PreferenceStyleSheet"); + }, + getDocument: function(sheet) { let document; if (sheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) { document = sheet.ownerNode; } else { document = sheet.ownerNode.ownerDocument; } @@ -1080,16 +1191,19 @@ var StyleRuleActor = protocol.ActorClass actor: this.actorID, type: this.type, line: this.line || undefined, column: this.column, traits: { // Whether the style rule actor implements the modifySelector2 method // that allows for unmatched rule to be added modifySelectorUnmatched: true, + // Whether the style rule actor implements the setRuleText + // method. + canSetRuleText: this.canSetRuleText, } }; if (this.rawRule.parentRule) { form.parentRule = this.pageStyle._styleRef(this.rawRule.parentRule).actorID; // CSS rules that we call media rules are STYLE_RULES that are children // of MEDIA_RULEs. We need to check the parentRule to check if a rule is @@ -1097,20 +1211,27 @@ var StyleRuleActor = protocol.ActorClass // below. if (this.rawRule.parentRule.type === Ci.nsIDOMCSSRule.MEDIA_RULE) { form.media = []; for (let i = 0, n = this.rawRule.parentRule.media.length; i < n; i++) { form.media.push(this.rawRule.parentRule.media.item(i)); } } } - if (this.rawRule.parentStyleSheet) { - form.parentStyleSheet = this.pageStyle._sheetRef(this.rawRule.parentStyleSheet).actorID; + if (this._parentSheet) { + form.parentStyleSheet = this.pageStyle._sheetRef(this._parentSheet).actorID; } + // One tricky thing here is that other methods in this actor must + // ensure that authoredText has been set before |form| is called. + // This has to be treated specially, for now, because we cannot + // synchronously compute the authored text, but |form| also cannot + // return a promise. See bug 1205868. + form.authoredText = this.authoredText; + switch (this.type) { case Ci.nsIDOMCSSRule.STYLE_RULE: form.selectors = CssLogic.getSelectors(this.rawRule); form.cssText = this.rawStyle.cssText || ""; break; case ELEMENT_STYLE: // Elements don't have a parent stylesheet, and therefore // don't have an associated URI. Provide a URI for @@ -1134,17 +1255,135 @@ var StyleRuleActor = protocol.ActorClass form.keyText = this.rawRule.keyText || ""; break; } return form; }, /** - * Modify a rule's properties. Passed an array of modifications: + * Send an event notifying that the location of the rule has + * changed. + * + * @param {Number} line the new line number + * @param {Number} column the new column number + */ + _notifyLocationChanged: function(line, column) { + events.emit(this, "location-changed", line, column); + }, + + /** + * Compute the index of this actor's raw rule in its parent style + * sheet. + */ + _computeRuleIndex: function() { + let rule = this.rawRule; + let cssRules = this._parentSheet.cssRules; + this._ruleIndex = -1; + for (let i = 0; i < cssRules.length; i++) { + if (rule === cssRules.item(i)) { + this._ruleIndex = i; + break; + } + } + }, + + /** + * This is attached to the parent style sheet actor's + * "style-applied" event. + */ + _onStyleApplied: function(kind) { + if (kind === UPDATE_GENERAL) { + // A general change means that the rule actors are invalidated, + // so stop listening to events now. + if (this.sheetActor) { + this.sheetActor.off("style-applied", this._onStyleApplied); + } + } else if (this._ruleIndex >= 0) { + // The sheet was updated by this actor, in a way that preserves + // the rules. Now, recompute our new rule from the style sheet, + // so that we aren't left with a reference to a dangling rule. + let oldRule = this.rawRule; + this.rawRule = this._parentSheet.cssRules[this._ruleIndex]; + // Also tell the page style so that future calls to _styleRef + // return the same StyleRuleActor. + this.pageStyle.updateStyleRef(oldRule, this.rawRule, this); + let line = DOMUtils.getRelativeRuleLine(this.rawRule); + let column = DOMUtils.getRuleColumn(this.rawRule); + if (line !== this.line || column !== this.column) { + this._notifyLocationChanged(line, column); + } + this.line = line; + this.column = column; + } + }, + + /** + * Return a promise that resolves to the authored form of a rule's + * text, if available. If the authored form is not available, the + * returned promise simply resolves to the empty string. If the + * authored form is available, this also sets |this.authoredText|. + * The authored text will include invalid and otherwise ignored + * properties. + */ + getAuthoredCssText: function() { + if (!this.canSetRuleText || + (this.type !== Ci.nsIDOMCSSRule.STYLE_RULE && + this.type !== Ci.nsIDOMCSSRule.KEYFRAME_RULE)) { + return promise.resolve(""); + } + + if (typeof this.authoredText === "string") { + return promise.resolve(this.authoredText); + } + + let parentStyleSheet = + this.pageStyle._sheetRef(this._parentSheet); + return parentStyleSheet.getText().then((longStr) => { + let cssText = longStr.str; + let {text} = getRuleText(cssText, this.line, this.column); + + // Cache the result on the rule actor to avoid parsing again next time + this.authoredText = text; + return this.authoredText; + }); + }, + + /** + * Set the contents of the rule. This rewrites the rule in the + * stylesheet and causes it to be re-evaluated. + * + * @param {String} newText the new text of the rule + * @returns the rule with updated properties + */ + setRuleText: method(Task.async(function*(newText) { + if (!this.canSetRuleText || + (this.type !== Ci.nsIDOMCSSRule.STYLE_RULE && + this.type !== Ci.nsIDOMCSSRule.KEYFRAME_RULE)) { + throw new Error("invalid call to setRuleText"); + } + + let parentStyleSheet = this.pageStyle._sheetRef(this._parentSheet); + let {str: cssText} = yield parentStyleSheet.getText(); + + let {offset, text} = getRuleText(cssText, this.line, this.column); + cssText = cssText.substring(0, offset) + newText + + cssText.substring(offset + text.length); + + this.authoredText = newText; + yield parentStyleSheet.update(cssText, false, UPDATE_PRESERVING_RULES); + + return this; + }), { + request: { modification: Arg(0, "string") }, + response: { rule: RetVal("domstylerule") } + }), + + /** + * Modify a rule's properties. Passed an array of modifications: * { * type: "set", * name: <string>, * value: <string>, * priority: <optional string> * } * or * { @@ -1158,17 +1397,17 @@ var StyleRuleActor = protocol.ActorClass // Use a fresh element for each call to this function to prevent side // effects that pop up based on property values that were already set on the // element. let document; if (this.rawNode) { document = this.rawNode.ownerDocument; } else { - let parentStyleSheet = this.rawRule.parentStyleSheet; + let parentStyleSheet = this._parentSheet; while (parentStyleSheet.ownerRule && parentStyleSheet.ownerRule instanceof Ci.nsIDOMCSSImportRule) { parentStyleSheet = parentStyleSheet.ownerRule.parentStyleSheet; } document = this.getDocument(parentStyleSheet); } @@ -1191,136 +1430,178 @@ var StyleRuleActor = protocol.ActorClass }), /** * Helper function for modifySelector and modifySelector2, inserts the new * rule with the new selector into the parent style sheet and removes the * current rule. Returns the newly inserted css rule or null if the rule is * unsuccessfully inserted to the parent style sheet. * - * @param string value + * @param {String} value * The new selector value + * @param {Boolean} editAuthored + * True if the selector should be updated by editing the + * authored text; false if the selector should be updated via + * CSSOM. * - * @returns CSSRule + * @returns {CSSRule} * The new CSS rule added */ - _addNewSelector: function(value) { + _addNewSelector: Task.async(function*(value, editAuthored) { let rule = this.rawRule; - let parentStyleSheet = rule.parentStyleSheet; - let cssRules = parentStyleSheet.cssRules; - let cssText = rule.cssText; - let selectorText = rule.selectorText; + let parentStyleSheet = this._parentSheet; + + // We know the selector modification is ok, so if the client asked + // for the authored text to be edited, do it now. + if (editAuthored) { + let document = this.getDocument(this._parentSheet); + try { + document.querySelector(value); + } catch (e) { + return null; + } - for (let i = 0; i < cssRules.length; i++) { - if (rule === cssRules.item(i)) { - try { - // Inserts the new style rule into the current style sheet and - // delete the current rule - let ruleText = cssText.slice(selectorText.length).trim(); - parentStyleSheet.insertRule(value + " " + ruleText, i); - parentStyleSheet.deleteRule(i + 1); - return cssRules.item(i); - } catch(e) { - // The selector could be invalid, or the rule could fail to insert. - // If that happens, the method returns null. + let sheetActor = this.pageStyle._sheetRef(parentStyleSheet); + let {str: authoredText} = yield sheetActor.getText(); + let [startOffset, endOffset] = getSelectorOffsets(authoredText, this.line, + this.column); + authoredText = authoredText.substring(0, startOffset) + value + + authoredText.substring(endOffset); + yield sheetActor.update(authoredText, false, UPDATE_PRESERVING_RULES); + } else { + let cssRules = parentStyleSheet.cssRules; + let cssText = rule.cssText; + let selectorText = rule.selectorText; + + for (let i = 0; i < cssRules.length; i++) { + if (rule === cssRules.item(i)) { + try { + // Inserts the new style rule into the current style sheet and + // delete the current rule + let ruleText = cssText.slice(selectorText.length).trim(); + parentStyleSheet.insertRule(value + " " + ruleText, i); + parentStyleSheet.deleteRule(i + 1); + break; + } catch(e) { + // The selector could be invalid, or the rule could fail to insert. + return null; + } } - - break; } } - return null; - }, + return parentStyleSheet.cssRules[this._ruleIndex]; + }), /** * Modify the current rule's selector by inserting a new rule with the new * selector value and removing the current rule. * * Note this method was kept for backward compatibility, but unmatched rules * support was added in FF41. * * @param string value * The new selector value * @returns boolean * Returns a boolean if the selector in the stylesheet was modified, * and false otherwise */ - modifySelector: method(function(value) { + modifySelector: method(Task.async(function*(value) { if (this.type === ELEMENT_STYLE) { return false; } - let document = this.getDocument(this.rawRule.parentStyleSheet); + let document = this.getDocument(this._parentSheet); // Extract the selector, and pseudo elements and classes let [selector, pseudoProp] = value.split(/(:{1,2}.+$)/); let selectorElement; try { selectorElement = document.querySelector(selector); } catch (e) { return false; } // Check if the selector is valid and not the same as the original // selector if (selectorElement && this.rawRule.selectorText !== value) { - this._addNewSelector(value); + yield this._addNewSelector(value, false); return true; } return false; - }, { + }), { request: { selector: Arg(0, "string") }, response: { isModified: RetVal("boolean") }, }), /** * Modify the current rule's selector by inserting a new rule with the new * selector value and removing the current rule. * * In contrast with the modifySelector method which was used before FF41, * this method also returns information about the new rule and applied style * so that consumers can immediately display the new rule, whether or not the * selector matches the current element without having to refresh the whole * list. * - * @param DOMNode node + * @param {DOMNode} node * The current selected element - * @param string value + * @param {String} value * The new selector value - * @returns Object + * @param {Boolean} editAuthored + * True if the selector should be updated by editing the + * authored text; false if the selector should be updated via + * CSSOM. + * @returns {Object} * Returns an object that contains the applied style properties of the * new rule and a boolean indicating whether or not the new selector * matches the current selected element */ - modifySelector2: method(function(node, value) { - let isMatching = false; + modifySelector2: method(function(node, value, editAuthored = false) { let ruleProps = null; if (this.type === ELEMENT_STYLE || this.rawRule.selectorText === value) { return { ruleProps, isMatching: true }; } - let newCssRule = this._addNewSelector(value); - if (newCssRule) { - ruleProps = this.pageStyle.getNewAppliedProps(node, newCssRule); + let selectorPromise = this._addNewSelector(value, editAuthored); + + if (editAuthored) { + selectorPromise = selectorPromise.then((newCssRule) => { + if (newCssRule) { + let style = this.pageStyle._styleRef(newCssRule); + // See the comment in |form| to understand this. + return style.getAuthoredCssText().then(() => newCssRule); + } + return newCssRule; + }); } - // Determine if the new selector value matches the current selected element - try { - isMatching = node.rawNode.matches(value); - } catch(e) { - // This fails when value is an invalid selector. - } + return selectorPromise.then((newCssRule) => { + if (newCssRule) { + ruleProps = this.pageStyle.getNewAppliedProps(node, newCssRule); + } - return { ruleProps, isMatching }; + // Determine if the new selector value matches the current + // selected element + let isMatching = false; + try { + isMatching = node.rawNode.matches(value); + } catch(e) { + // This fails when value is an invalid selector. + } + + return { ruleProps, isMatching }; + }); }, { request: { node: Arg(0, "domnode"), - value: Arg(1, "string") + value: Arg(1, "string"), + editAuthored: Arg(2, "boolean") }, response: RetVal("modifiedStylesReturn") }) }); /** * Front for the StyleRule actor. */ @@ -1341,34 +1622,53 @@ var StyleRuleFront = protocol.FrontClass this.actorID = form.actor; this._form = form; if (this._mediaText) { this._mediaText = null; } }, /** - * Return a new RuleModificationList for this node. + * Ensure _form is updated when location-changed is emitted. + */ + _locationChangedPre: protocol.preEvent("location-changed", function(line, + column) { + this._clearOriginalLocation(); + this._form.line = line; + this._form.column = column; + }), + + /** + * Return a new RuleModificationList or RuleRewriter for this node. + * A RuleRewriter will be returned when the rule's canSetRuleText + * trait is true; otherwise a RuleModificationList will be + * returned. */ startModifyingProperties: function() { + if (this.canSetRuleText) { + return new RuleRewriter(this, this.authoredText); + } return new RuleModificationList(this); }, get type() { return this._form.type; }, get line() { return this._form.line || -1; }, get column() { return this._form.column || -1; }, get cssText() { return this._form.cssText; }, + get authoredText() { + return this._form.authoredText || this._form.cssText; + }, get keyText() { return this._form.keyText; }, get name() { return this._form.name; }, get selectors() { return this._form.selectors; @@ -1411,25 +1711,33 @@ var StyleRuleFront = protocol.FrontClass let sheet = this.parentStyleSheet; return sheet ? sheet.nodeHref : ""; }, get supportsModifySelectorUnmatched() { return this._form.traits && this._form.traits.modifySelectorUnmatched; }, + get canSetRuleText() { + return this._form.traits && this._form.traits.canSetRuleText; + }, + get location() { return { source: this.parentStyleSheet, href: this.href, line: this.line, column: this.column }; }, + _clearOriginalLocation: function() { + this._originalLocation = null; + }, + getOriginalLocation: function() { if (this._originalLocation) { return promise.resolve(this._originalLocation); } let parentSheet = this.parentStyleSheet; if (!parentSheet) { // This rule doesn't belong to a stylesheet so it is an inline style. // Inline styles do not have any mediaText so we can return early. @@ -1453,36 +1761,51 @@ var StyleRuleFront = protocol.FrontClass return location; }); }, modifySelector: protocol.custom(Task.async(function*(node, value) { let response; if (this.supportsModifySelectorUnmatched) { // If the debugee supports adding unmatched rules (post FF41) - response = yield this.modifySelector2(node, value); + if (this.canSetRuleText) { + response = yield this.modifySelector2(node, value, true); + } else { + response = yield this.modifySelector2(node, value); + } } else { response = yield this._modifySelector(value); } if (response.ruleProps) { response.ruleProps = response.ruleProps.entries[0]; } return response; }), { impl: "_modifySelector" + }), + + setRuleText: protocol.custom(function(newText) { + this._form.authoredText = newText; + return this._setRuleText(newText); + }, { + impl: "_setRuleText" }) }); /** * Convenience API for building a list of attribute modifications * for the `modifyProperties` request. A RuleModificationList holds a * list of modifications that will be applied to a StyleRuleActor. * The modifications are processed in the order in which they are * added to the RuleModificationList. + * + * Objects of this type expose the same API as @see RuleRewriter. + * This lets the inspector use (mostly) the same code, regardless of + * whether the server implements setRuleText. */ var RuleModificationList = Class({ /** * Initialize a RuleModificationList. * @param {StyleRuleFront} rule the associated rule */ initialize: function(rule) { this.rule = rule; @@ -1497,41 +1820,105 @@ var RuleModificationList = Class({ */ apply: function() { return this.rule.modifyProperties(this.modifications); }, /** * Add a "set" entry to the modification list. * - * @param {string} name the property's name - * @param {string} value the property's value - * @param {string} priority the property's priority, either the empty + * @param {Number} index index of the property in the rule. + * This can be -1 in the case where + * the rule does not support setRuleText; + * generally for setting properties + * on an element's style. + * @param {String} name the property's name + * @param {String} value the property's value + * @param {String} priority the property's priority, either the empty * string or "important" */ - setProperty: function(name, value, priority) { + setProperty: function(index, name, value, priority) { this.modifications.push({ type: "set", name: name, value: value, priority: priority }); }, /** * Add a "remove" entry to the modification list. * - * @param {string} name the name of the property to remove + * @param {Number} index index of the property in the rule. + * This can be -1 in the case where + * the rule does not support setRuleText; + * generally for setting properties + * on an element's style. + * @param {String} name the name of the property to remove */ - removeProperty: function(name) { + removeProperty: function(index, name) { this.modifications.push({ type: "remove", name: name }); - } + }, + + /** + * Rename a property. This implementation acts like + * |removeProperty|, because |setRuleText| is not available. + * + * @param {Number} index index of the property in the rule. + * This can be -1 in the case where + * the rule does not support setRuleText; + * generally for setting properties + * on an element's style. + * @param {String} name current name of the property + * @param {String} newName new name of the property + */ + renameProperty: function(index, name, newName) { + this.removeProperty(index, name); + }, + + /** + * Enable or disable a property. This implementation acts like + * |removeProperty| when disabling, or a no-op when enabling, + * because |setRuleText| is not available. + * + * @param {Number} index index of the property in the rule. + * This can be -1 in the case where + * the rule does not support setRuleText; + * generally for setting properties + * on an element's style. + * @param {String} name current name of the property + * @param {Boolean} isEnabled true if the property should be enabled; + * false if it should be disabled + */ + setPropertyEnabled: function(index, name, isEnabled) { + if (!isEnabled) { + this.removeProperty(index, name); + } + }, + + /** + * Create a new property. This implementation does nothing, because + * |setRuleText| is not available. + * + * @param {Number} index index of the property in the rule. + * This can be -1 in the case where + * the rule does not support setRuleText; + * generally for setting properties + * on an element's style. + * @param {String} name name of the new property + * @param {String} value value of the new property + * @param {String} priority priority of the new property; either + * the empty string or "important" + */ + createProperty: function(index, name, value, priority) { + // Nothing. + }, }); /** * Helper function for getting an image preview of the given font. * * @param font {string} * Name of font to preview * @param doc {Document} @@ -1655,16 +2042,57 @@ function getRuleText(initialText, line, // that cssTokenizer skips them. return {offset: textOffset + startOffset, text: text.substring(startOffset, endOffset)}; } exports.getRuleText = getRuleText; /** + * Compute the start and end offsets of a rule's selector text, given + * the CSS text and the line and column at which the rule begins. + * @param {String} initialText + * @param {Number} line (1-indexed) + * @param {Number} column (1-indexed) + * @return {array} An array with two elements: [startOffset, endOffset]. + * The elements mark the bounds in |initialText| of + * the CSS rule's selector. + */ +function getSelectorOffsets(initialText, line, column) { + if (typeof line === "undefined" || typeof column === "undefined") { + throw new Error("Location information is missing"); + } + + let {offset: textOffset, text} = + getTextAtLineColumn(initialText, line, column); + let lexer = DOMUtils.getCSSLexer(text); + + // Search forward for the opening brace. + let endOffset; + while (true) { + let token = lexer.nextToken(); + if (!token) { + break; + } + if (token.tokenType === "symbol" && token.text === "{") { + if (endOffset === undefined) { + break; + } + return [textOffset, textOffset + endOffset]; + } + // Preserve comments and whitespace just before the "{". + if (token.tokenType !== "comment" && token.tokenType !== "whitespace") { + endOffset = token.endOffset; + } + } + + throw new Error("could not find bounds of rule"); +} + +/** * Return the offset and substring of |text| that starts at the given * line and column. * @param {String} text * @param {Number} line (1-indexed) * @param {Number} column (1-indexed) * @return {object} An object of the form {offset: number, text: string}, * where the offset is the offset into the input string * where the text starts, and where text is the text.
--- a/devtools/server/actors/stylesheets.js +++ b/devtools/server/actors/stylesheets.js @@ -18,16 +18,21 @@ const protocol = require("devtools/serve const {Arg, Option, method, RetVal, types} = protocol; const {LongStringActor, ShortLongString} = require("devtools/server/actors/string"); const {fetch} = require("devtools/shared/DevToolsUtils"); const {listenOnce} = require("devtools/shared/async-utils"); const {SourceMapConsumer} = require("source-map"); loader.lazyGetter(this, "CssLogic", () => require("devtools/shared/styleinspector/css-logic").CssLogic); +const { + getIndentationFromPrefs, + getIndentationFromString +} = require("devtools/shared/shared/indentation"); + var TRANSITION_CLASS = "moz-styleeditor-transitioning"; var TRANSITION_DURATION_MS = 500; var TRANSITION_BUFFER_MS = 1000; var TRANSITION_RULE_SELECTOR = ".moz-styleeditor-transitioning:root, .moz-styleeditor-transitioning:root *"; var TRANSITION_RULE = TRANSITION_RULE_SELECTOR + " {\ transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \ transition-delay: 0ms !important;\ @@ -35,16 +40,32 @@ transition-timing-function: ease-out !im transition-property: all !important;\ }"; var LOAD_ERROR = "error-load"; types.addActorType("stylesheet"); types.addActorType("originalsource"); +// The possible kinds of style-applied events. +// UPDATE_PRESERVING_RULES means that the update is guaranteed to +// preserve the number and order of rules on the style sheet. +// UPDATE_GENERAL covers any other kind of change to the style sheet. +const UPDATE_PRESERVING_RULES = 0; +exports.UPDATE_PRESERVING_RULES = UPDATE_PRESERVING_RULES; +const UPDATE_GENERAL = 1; +exports.UPDATE_GENERAL = UPDATE_GENERAL; + +// If the user edits a style sheet, we stash a copy of the edited text +// here, keyed by the style sheet. This way, if the tools are closed +// and then reopened, the edited text will be available. A weak map +// is used so that navigation by the user will eventually cause the +// edited text to be collected. +let modifiedStyleSheets = new WeakMap(); + /** * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the * stylesheets of a document. */ var StyleSheetsActor = exports.StyleSheetsActor = protocol.ActorClass({ typeName: "stylesheets", /** @@ -380,17 +401,19 @@ var StyleSheetActor = protocol.ActorClas events: { "property-change" : { type: "propertyChange", property: Arg(0, "string"), value: Arg(1, "json") }, "style-applied" : { - type: "styleApplied" + type: "styleApplied", + kind: Arg(0, "number"), + styleSheet: Arg(1, "stylesheet") }, "media-rules-changed" : { type: "mediaRulesChanged", rules: Arg(0, "array:mediarule") } }, /* List of original sources that generated this stylesheet */ @@ -592,16 +615,22 @@ var StyleSheetActor = protocol.ActorClas * @return {Promise} * Promise that resolves with a string text of the stylesheet. */ _getText: function() { if (typeof this.text === "string") { return promise.resolve(this.text); } + let cssText = modifiedStyleSheets.get(this.rawSheet); + if (cssText !== undefined) { + this.text = cssText; + return promise.resolve(cssText); + } + if (!this.href) { // this is an inline <style> sheet let content = this.ownerNode.textContent; this.text = content; return promise.resolve(content); } let options = { @@ -867,73 +896,76 @@ var StyleSheetActor = protocol.ActorClas }, /** * Update the style sheet in place with new text. * * @param {object} request * 'text' - new text * 'transition' - whether to do CSS transition for change. + * 'kind' - either UPDATE_PRESERVING_RULES or UPDATE_GENERAL */ - update: method(function(text, transition) { + update: method(function(text, transition, kind = UPDATE_GENERAL) { DOMUtils.parseStyleSheet(this.rawSheet, text); + modifiedStyleSheets.set(this.rawSheet, text); + this.text = text; this._notifyPropertyChanged("ruleCount"); if (transition) { - this._insertTransistionRule(); + this._insertTransistionRule(kind); } else { - events.emit(this, "style-applied"); + events.emit(this, "style-applied", kind, this); } this._getMediaRules().then((rules) => { events.emit(this, "media-rules-changed", rules); }); }, { request: { text: Arg(0, "string"), transition: Arg(1, "boolean") } }), /** * Insert a catch-all transition rule into the document. Set a timeout * to remove the rule after a certain time. */ - _insertTransistionRule: function() { + _insertTransistionRule: function(kind) { this.document.documentElement.classList.add(TRANSITION_CLASS); // We always add the rule since we've just reset all the rules this.rawSheet.insertRule(TRANSITION_RULE, this.rawSheet.cssRules.length); // Set up clean up and commit after transition duration (+buffer) // @see _onTransitionEnd this.window.clearTimeout(this._transitionTimeout); - this._transitionTimeout = this.window.setTimeout(this._onTransitionEnd.bind(this), + this._transitionTimeout = this.window.setTimeout(this._onTransitionEnd.bind(this, kind), TRANSITION_DURATION_MS + TRANSITION_BUFFER_MS); }, /** * This cleans up class and rule added for transition effect and then * notifies that the style has been applied. */ - _onTransitionEnd: function() + _onTransitionEnd: function(kind) { this.document.documentElement.classList.remove(TRANSITION_CLASS); let index = this.rawSheet.cssRules.length - 1; let rule = this.rawSheet.cssRules[index]; if (rule.selectorText == TRANSITION_RULE_SELECTOR) { this.rawSheet.deleteRule(index); } - events.emit(this, "style-applied"); + events.emit(this, "style-applied", kind, this); } }) /** * StyleSheetFront is the client-side counterpart to a StyleSheetActor. */ var StyleSheetFront = protocol.FrontClass(StyleSheetActor, { initialize: function(conn, form) { @@ -976,16 +1008,39 @@ var StyleSheetFront = protocol.FrontClas get isSystem() { return this._form.system; }, get styleSheetIndex() { return this._form.styleSheetIndex; }, get ruleCount() { return this._form.ruleCount; + }, + + /** + * Get the indentation to use for edits to this style sheet. + * + * @return {Promise} A promise that will resolve to a string that + * should be used to indent a block in this style sheet. + */ + guessIndentation: function() { + let prefIndent = getIndentationFromPrefs(); + if (prefIndent) { + let {indentUnit, indentWithTabs} = prefIndent; + return promise.resolve(indentWithTabs ? "\t" : " ".repeat(indentUnit)); + } + + return Task.spawn(function*() { + let longStr = yield this.getText(); + let source = yield longStr.string(); + + let {indentUnit, indentWithTabs} = getIndentationFromString(source); + + return indentWithTabs ? "\t" : " ".repeat(indentUnit); + }.bind(this)); } }); /** * Actor representing an original source of a style sheet that was specified * in a source map. */ var OriginalSourceActor = protocol.ActorClass({
--- a/devtools/server/tests/browser/browser.ini +++ b/devtools/server/tests/browser/browser.ini @@ -16,32 +16,33 @@ support-files = storage-unsecured-iframe.html storage-updates.html storage-secured-iframe.html stylesheets-nested-iframes.html timeline-iframe-child.html timeline-iframe-parent.html director-script-target.html -[browser_animation_actors_01.js] -[browser_animation_actors_02.js] -[browser_animation_actors_03.js] -[browser_animation_actors_04.js] +[browser_animation_emitMutations.js] +[browser_animation_getFrames.js] +[browser_animation_getMultipleStates.js] +[browser_animation_getPlayers.js] +[browser_animation_getStateAfterFinished.js] +[browser_animation_getSubTreeAnimations.js] +[browser_animation_keepFinished.js] +[browser_animation_playerState.js] +[browser_animation_playPauseIframe.js] +[browser_animation_playPauseSeveral.js] +[browser_animation_reconstructState.js] +[browser_animation_refreshTransitions.js] +[browser_animation_setCurrentTime.js] +[browser_animation_setPlaybackRate.js] +[browser_animation_simple.js] +[browser_animation_updatedState.js] skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S -[browser_animation_actors_06.js] -[browser_animation_actors_07.js] -[browser_animation_actors_08.js] -[browser_animation_actors_09.js] -[browser_animation_actors_10.js] -[browser_animation_actors_11.js] -[browser_animation_actors_12.js] -[browser_animation_actors_13.js] -[browser_animation_actors_14.js] -[browser_animation_actors_15.js] -[browser_animation_actors_16.js] [browser_canvasframe_helper_01.js] skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S [browser_canvasframe_helper_02.js] [browser_canvasframe_helper_03.js] skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S [browser_canvasframe_helper_04.js] skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S [browser_canvasframe_helper_05.js]
rename from devtools/server/tests/browser/browser_animation_actors_13.js rename to devtools/server/tests/browser/browser_animation_emitMutations.js --- a/devtools/server/tests/browser/browser_animation_actors_13.js +++ b/devtools/server/tests/browser/browser_animation_emitMutations.js @@ -2,28 +2,19 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Test that the AnimationsActor emits events about changed animations on a // node after getAnimationPlayersForNode was called on that node. -const {AnimationsFront} = require("devtools/server/actors/animation"); -const {InspectorFront} = require("devtools/server/actors/inspector"); - add_task(function*() { - yield addTab(MAIN_DOMAIN + "animation.html"); - - initDebuggerServer(); - let client = new DebuggerClient(DebuggerServer.connectPipe()); - let form = yield connectDebuggerClient(client); - let inspector = InspectorFront(client, form); - let walker = yield inspector.getWalker(); - let animations = AnimationsFront(client, form); + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); info("Retrieve a non-animated node"); let node = yield walker.querySelector(walker.rootNode, ".not-animated"); info("Retrieve the animation player for the node"); let players = yield animations.getAnimationPlayersForNode(node); is(players.length, 0, "The node has no animation players"); @@ -55,17 +46,17 @@ add_task(function*() { yield node.modifyAttributes([ {attributeName: "class", newValue: "not-animated"} ]); changes = yield onMutations; ok(true, "The mutations event was emitted"); is(changes.length, 2, "There are 2 changes in the mutation event"); - ok(changes.every(({type}) => type === "removed"), "Both changes are removals"); + ok(changes.every(({type}) => type === "removed"), "Both are removals"); ok(changes[0].player === p1 || changes[0].player === p2, "The first removed player was one of the previously added players"); ok(changes[1].player === p1 || changes[1].player === p2, "The second removed player was one of the previously added players"); yield closeDebuggerClient(client); gBrowser.removeCurrentTab(); });
new file mode 100644 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_getFrames.js @@ -0,0 +1,32 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the AnimationPlayerActor exposes a getFrames method that returns +// the list of keyframes in the animation. + +const URL = MAIN_DOMAIN + "animation.html"; + +add_task(function*() { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + info("Get the test node and its animation front"); + let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); + let [player] = yield animations.getAnimationPlayersForNode(node); + + ok(player.getFrames, "The front has the getFrames method"); + + let frames = yield player.getFrames(); + is(frames.length, 2, "The correct number of keyframes was retrieved"); + ok(frames[0].transform, "Frame 0 has the transform property"); + ok(frames[1].transform, "Frame 1 has the transform property"); + // Note that we don't really test the content of the frame object here on + // purpose. This object comes straight out of the web animations API + // unmodified. + + yield closeDebuggerClient(client); + gBrowser.removeCurrentTab(); +});
rename from devtools/server/tests/browser/browser_animation_actors_06.js rename to devtools/server/tests/browser/browser_animation_getMultipleStates.js --- a/devtools/server/tests/browser/browser_animation_actors_06.js +++ b/devtools/server/tests/browser/browser_animation_getMultipleStates.js @@ -2,49 +2,48 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Check that the duration, iterationCount and delay are retrieved correctly for // multiple animations. -const {AnimationsFront} = require("devtools/server/actors/animation"); -const {InspectorFront} = require("devtools/server/actors/inspector"); - add_task(function*() { - let doc = yield addTab(MAIN_DOMAIN + "animation.html"); + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); - initDebuggerServer(); - let client = new DebuggerClient(DebuggerServer.connectPipe()); - let form = yield connectDebuggerClient(client); - let inspector = InspectorFront(client, form); - let walker = yield inspector.getWalker(); - let front = AnimationsFront(client, form); - - yield playerHasAnInitialState(walker, front); + yield playerHasAnInitialState(walker, animations); yield closeDebuggerClient(client); gBrowser.removeCurrentTab(); }); -function* playerHasAnInitialState(walker, front) { - let state = yield getAnimationStateForNode(walker, front, +function* playerHasAnInitialState(walker, animations) { + let state = yield getAnimationStateForNode(walker, animations, ".delayed-multiple-animations", 0); - ok(state.duration, 500, "The duration of the first animation is correct"); - ok(state.iterationCount, 10, "The iterationCount of the first animation is correct"); - ok(state.delay, 1000, "The delay of the first animation is correct"); + + ok(state.duration, 500, + "The duration of the first animation is correct"); + ok(state.iterationCount, 10, + "The iterationCount of the first animation is correct"); + ok(state.delay, 1000, + "The delay of the first animation is correct"); - state = yield getAnimationStateForNode(walker, front, + state = yield getAnimationStateForNode(walker, animations, ".delayed-multiple-animations", 1); - ok(state.duration, 1000, "The duration of the secon animation is correct"); - ok(state.iterationCount, 30, "The iterationCount of the secon animation is correct"); - ok(state.delay, 750, "The delay of the secon animation is correct"); + + ok(state.duration, 1000, + "The duration of the secon animation is correct"); + ok(state.iterationCount, 30, + "The iterationCount of the secon animation is correct"); + ok(state.delay, 750, + "The delay of the secon animation is correct"); } -function* getAnimationStateForNode(walker, front, nodeSelector, playerIndex) { - let node = yield walker.querySelector(walker.rootNode, nodeSelector); - let players = yield front.getAnimationPlayersForNode(node); +function* getAnimationStateForNode(walker, animations, selector, playerIndex) { + let node = yield walker.querySelector(walker.rootNode, selector); + let players = yield animations.getAnimationPlayersForNode(node); let player = players[playerIndex]; yield player.ready(); let state = yield player.getCurrentState(); return state; }
rename from devtools/server/tests/browser/browser_animation_actors_02.js rename to devtools/server/tests/browser/browser_animation_getPlayers.js --- a/devtools/server/tests/browser/browser_animation_actors_02.js +++ b/devtools/server/tests/browser/browser_animation_getPlayers.js @@ -1,63 +1,63 @@ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Check the output of getAnimationPlayersForNode -const {AnimationsFront} = require("devtools/server/actors/animation"); -const {InspectorFront} = require("devtools/server/actors/inspector"); - add_task(function*() { - let doc = yield addTab(MAIN_DOMAIN + "animation.html"); + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); - initDebuggerServer(); - let client = new DebuggerClient(DebuggerServer.connectPipe()); - let form = yield connectDebuggerClient(client); - let inspector = InspectorFront(client, form); - let walker = yield inspector.getWalker(); - let front = AnimationsFront(client, form); - - yield theRightNumberOfPlayersIsReturned(walker, front); - yield playersCanBePausedAndResumed(walker, front); + yield theRightNumberOfPlayersIsReturned(walker, animations); + yield playersCanBePausedAndResumed(walker, animations); yield closeDebuggerClient(client); gBrowser.removeCurrentTab(); }); -function* theRightNumberOfPlayersIsReturned(walker, front) { +function* theRightNumberOfPlayersIsReturned(walker, animations) { let node = yield walker.querySelector(walker.rootNode, ".not-animated"); - let players = yield front.getAnimationPlayersForNode(node); - is(players.length, 0, "0 players were returned for the unanimated node"); + let players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 0, + "0 players were returned for the unanimated node"); node = yield walker.querySelector(walker.rootNode, ".simple-animation"); - players = yield front.getAnimationPlayersForNode(node); - is(players.length, 1, "One animation player was returned"); + players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 1, + "One animation player was returned"); node = yield walker.querySelector(walker.rootNode, ".multiple-animations"); - players = yield front.getAnimationPlayersForNode(node); - is(players.length, 2, "Two animation players were returned"); + players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 2, + "Two animation players were returned"); node = yield walker.querySelector(walker.rootNode, ".transition"); - players = yield front.getAnimationPlayersForNode(node); - is(players.length, 1, "One animation player was returned for the transitioned node"); + players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 1, + "One animation player was returned for the transitioned node"); } -function* playersCanBePausedAndResumed(walker, front) { +function* playersCanBePausedAndResumed(walker, animations) { let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); - let [player] = yield front.getAnimationPlayersForNode(node); + let [player] = yield animations.getAnimationPlayersForNode(node); yield player.ready(); - ok(player.initialState, "The player has an initialState"); - ok(player.getCurrentState, "The player has the getCurrentState method"); - is(player.initialState.playState, "running", "The animation is currently running"); + ok(player.initialState, + "The player has an initialState"); + ok(player.getCurrentState, + "The player has the getCurrentState method"); + is(player.initialState.playState, "running", + "The animation is currently running"); yield player.pause(); let state = yield player.getCurrentState(); - is(state.playState, "paused", "The animation is now paused"); + is(state.playState, "paused", + "The animation is now paused"); yield player.play(); state = yield player.getCurrentState(); - is(state.playState, "running", "The animation is now running again"); + is(state.playState, "running", + "The animation is now running again"); }
rename from devtools/server/tests/browser/browser_animation_actors_10.js rename to devtools/server/tests/browser/browser_animation_getStateAfterFinished.js --- a/devtools/server/tests/browser/browser_animation_actors_10.js +++ b/devtools/server/tests/browser/browser_animation_getStateAfterFinished.js @@ -5,47 +5,38 @@ "use strict"; // Check that the right duration/iterationCount/delay are retrieved even when // the node has multiple animations and one of them already ended before getting // the player objects. // See devtools/server/actors/animation.js |getPlayerIndex| for more // information. -const {AnimationsFront} = require("devtools/server/actors/animation"); -const {InspectorFront} = require("devtools/server/actors/inspector"); - add_task(function*() { - yield addTab(MAIN_DOMAIN + "animation.html"); - - initDebuggerServer(); - let client = new DebuggerClient(DebuggerServer.connectPipe()); - let form = yield connectDebuggerClient(client); - let inspector = InspectorFront(client, form); - let walker = yield inspector.getWalker(); - let front = AnimationsFront(client, form); + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); info("Retrieve a non animated node"); let node = yield walker.querySelector(walker.rootNode, ".not-animated"); info("Apply the multiple-animations-2 class to start the animations"); yield node.modifyAttributes([ {attributeName: "class", newValue: "multiple-animations-2"} ]); info("Get the list of players, by the time this executes, the first, " + "short, animation should have ended."); - let players = yield front.getAnimationPlayersForNode(node); + let players = yield animations.getAnimationPlayersForNode(node); if (players.length === 3) { info("The short animation hasn't ended yet, wait for a bit."); // The animation lasts for 500ms, so 1000ms should do it. yield new Promise(resolve => setTimeout(resolve, 1000)); info("And get the list again"); - players = yield front.getAnimationPlayersForNode(node); + players = yield animations.getAnimationPlayersForNode(node); } is(players.length, 2, "2 animations remain on the node"); is(players[0].state.duration, 1000, "The duration of the first animation is correct"); is(players[0].state.delay, 2000, "The delay of the first animation is correct");
rename from devtools/server/tests/browser/browser_animation_actors_16.js rename to devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js --- a/devtools/server/tests/browser/browser_animation_actors_16.js +++ b/devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js @@ -2,43 +2,35 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Check that the AnimationsActor can retrieve all animations inside a node's // subtree (but not going into iframes). -const {AnimationsFront} = require("devtools/server/actors/animation"); -const {InspectorFront} = require("devtools/server/actors/inspector"); - const URL = MAIN_DOMAIN + "animation.html"; add_task(function*() { info("Creating a test document with 2 iframes containing animated nodes"); - yield addTab("data:text/html;charset=utf-8," + - "<iframe id='iframe' src='" + URL + "'></iframe>"); - initDebuggerServer(); - let client = new DebuggerClient(DebuggerServer.connectPipe()); - let form = yield connectDebuggerClient(client); - let inspector = InspectorFront(client, form); - let walker = yield inspector.getWalker(); - let front = AnimationsFront(client, form); + let {client, walker, animations} = yield initAnimationsFrontForUrl( + "data:text/html;charset=utf-8," + + "<iframe id='iframe' src='" + URL + "'></iframe>"); info("Try retrieving all animations from the root doc's <body> node"); let rootBody = yield walker.querySelector(walker.rootNode, "body"); - let players = yield front.getAnimationPlayersForNode(rootBody); + let players = yield animations.getAnimationPlayersForNode(rootBody); is(players.length, 0, "The node has no animation players"); info("Retrieve all animations from the iframe's <body> node"); let iframe = yield walker.querySelector(walker.rootNode, "#iframe"); let {nodes} = yield walker.children(iframe); let frameBody = yield walker.querySelector(nodes[0], "body"); - players = yield front.getAnimationPlayersForNode(frameBody); + players = yield animations.getAnimationPlayersForNode(frameBody); // Testing for a hard-coded number of animations here would intermittently // fail depending on how fast or slow the test is (indeed, the test page // contains short transitions, and delayed animations). So just make sure we // at least have the infinitely running animations. ok(players.length >= 4, "All subtree animations were retrieved"); yield closeDebuggerClient(client);
rename from devtools/server/tests/browser/browser_animation_actors_14.js rename to devtools/server/tests/browser/browser_animation_keepFinished.js --- a/devtools/server/tests/browser/browser_animation_actors_14.js +++ b/devtools/server/tests/browser/browser_animation_keepFinished.js @@ -4,28 +4,19 @@ "use strict"; // Test that the AnimationsActor doesn't report finished animations as removed. // Indeed, animations that only have the "finished" playState can be modified // still, so we want the AnimationsActor to preserve the corresponding // AnimationPlayerActor. -const {AnimationsFront} = require("devtools/server/actors/animation"); -const {InspectorFront} = require("devtools/server/actors/inspector"); - add_task(function*() { - yield addTab(MAIN_DOMAIN + "animation.html"); - - initDebuggerServer(); - let client = new DebuggerClient(DebuggerServer.connectPipe()); - let form = yield connectDebuggerClient(client); - let inspector = InspectorFront(client, form); - let walker = yield inspector.getWalker(); - let animations = AnimationsFront(client, form); + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); info("Retrieve a non-animated node"); let node = yield walker.querySelector(walker.rootNode, ".not-animated"); info("Retrieve the animation player for the node"); let players = yield animations.getAnimationPlayersForNode(node); is(players.length, 0, "The node has no animation players");
rename from devtools/server/tests/browser/browser_animation_actors_09.js rename to devtools/server/tests/browser/browser_animation_playPauseIframe.js --- a/devtools/server/tests/browser/browser_animation_actors_09.js +++ b/devtools/server/tests/browser/browser_animation_playPauseIframe.js @@ -2,55 +2,47 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Check that the AnimationsActor can pause/play all animations even those // within iframes. -const {AnimationsFront} = require("devtools/server/actors/animation"); -const {InspectorFront} = require("devtools/server/actors/inspector"); - const URL = MAIN_DOMAIN + "animation.html"; add_task(function*() { info("Creating a test document with 2 iframes containing animated nodes"); - yield addTab("data:text/html;charset=utf-8," + - "<iframe id='i1' src='" + URL + "'></iframe>" + - "<iframe id='i2' src='" + URL + "'></iframe>"); - initDebuggerServer(); - let client = new DebuggerClient(DebuggerServer.connectPipe()); - let form = yield connectDebuggerClient(client); - let inspector = InspectorFront(client, form); - let walker = yield inspector.getWalker(); - let front = AnimationsFront(client, form); + let {client, walker, animations} = yield initAnimationsFrontForUrl( + "data:text/html;charset=utf-8," + + "<iframe id='i1' src='" + URL + "'></iframe>" + + "<iframe id='i2' src='" + URL + "'></iframe>"); info("Getting the 2 iframe container nodes and animated nodes in them"); let nodeInFrame1 = yield getNodeInFrame(walker, "#i1", ".simple-animation"); let nodeInFrame2 = yield getNodeInFrame(walker, "#i2", ".simple-animation"); info("Pause all animations in the test document"); - yield front.pauseAll(); - yield checkState(front, nodeInFrame1, "paused"); - yield checkState(front, nodeInFrame2, "paused"); + yield animations.pauseAll(); + yield checkState(animations, nodeInFrame1, "paused"); + yield checkState(animations, nodeInFrame2, "paused"); info("Play all animations in the test document"); - yield front.playAll(); - yield checkState(front, nodeInFrame1, "running"); - yield checkState(front, nodeInFrame2, "running"); + yield animations.playAll(); + yield checkState(animations, nodeInFrame1, "running"); + yield checkState(animations, nodeInFrame2, "running"); yield closeDebuggerClient(client); gBrowser.removeCurrentTab(); }); -function* checkState(front, nodeFront, playState) { +function* checkState(animations, nodeFront, playState) { info("Getting the AnimationPlayerFront for the test node"); - let [player] = yield front.getAnimationPlayersForNode(nodeFront); + let [player] = yield animations.getAnimationPlayersForNode(nodeFront); yield player.ready; let state = yield player.getCurrentState(); is(state.playState, playState, "The playState of the test node is " + playState); } function* getNodeInFrame(walker, frameSelector, nodeSelector) { let iframe = yield walker.querySelector(walker.rootNode, frameSelector);
rename from devtools/server/tests/browser/browser_animation_actors_08.js rename to devtools/server/tests/browser/browser_animation_playPauseSeveral.js --- a/devtools/server/tests/browser/browser_animation_actors_08.js +++ b/devtools/server/tests/browser/browser_animation_playPauseSeveral.js @@ -2,87 +2,78 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Check that the AnimationsActor can pause/play all animations at once, and // check that it can also pause/play a given list of animations at once. -const {AnimationsFront} = require("devtools/server/actors/animation"); -const {InspectorFront} = require("devtools/server/actors/inspector"); - // List of selectors that match "all" animated nodes in the test page. // This list misses a bunch of animated nodes on purpose. Only the ones that // have infinite animations are listed. This is done to avoid intermittents // caused when finite animations are already done playing by the time the test // runs. const ALL_ANIMATED_NODES = [".simple-animation", ".multiple-animations", ".delayed-animation"]; // List of selectors that match some animated nodes in the test page only. const SOME_ANIMATED_NODES = [".simple-animation", ".delayed-animation"]; add_task(function*() { - yield addTab(MAIN_DOMAIN + "animation.html"); - - initDebuggerServer(); - let client = new DebuggerClient(DebuggerServer.connectPipe()); - let form = yield connectDebuggerClient(client); - let inspector = InspectorFront(client, form); - let walker = yield inspector.getWalker(); - let front = AnimationsFront(client, form); + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); info("Pause all animations in the test document"); - yield front.pauseAll(); - yield checkAnimationsStates(walker, front, ALL_ANIMATED_NODES, "paused"); + yield animations.pauseAll(); + yield checkStates(walker, animations, ALL_ANIMATED_NODES, "paused"); info("Play all animations in the test document"); - yield front.playAll(); - yield checkAnimationsStates(walker, front, ALL_ANIMATED_NODES, "running"); + yield animations.playAll(); + yield checkStates(walker, animations, ALL_ANIMATED_NODES, "running"); info("Pause all animations in the test document using toggleAll"); - yield front.toggleAll(); - yield checkAnimationsStates(walker, front, ALL_ANIMATED_NODES, "paused"); + yield animations.toggleAll(); + yield checkStates(walker, animations, ALL_ANIMATED_NODES, "paused"); info("Play all animations in the test document using toggleAll"); - yield front.toggleAll(); - yield checkAnimationsStates(walker, front, ALL_ANIMATED_NODES, "running"); + yield animations.toggleAll(); + yield checkStates(walker, animations, ALL_ANIMATED_NODES, "running"); info("Pause a given list of animations only"); let players = []; for (let selector of SOME_ANIMATED_NODES) { - let [player] = yield getPlayersFor(walker, front, selector); + let [player] = yield getPlayersFor(walker, animations, selector); players.push(player); } - yield front.toggleSeveral(players, true); - yield checkAnimationsStates(walker, front, SOME_ANIMATED_NODES, "paused"); - yield checkAnimationsStates(walker, front, [".multiple-animations"], "running"); + yield animations.toggleSeveral(players, true); + yield checkStates(walker, animations, SOME_ANIMATED_NODES, "paused"); + yield checkStates(walker, animations, [".multiple-animations"], "running"); info("Play the same list of animations"); - yield front.toggleSeveral(players, false); - yield checkAnimationsStates(walker, front, ALL_ANIMATED_NODES, "running"); + yield animations.toggleSeveral(players, false); + yield checkStates(walker, animations, ALL_ANIMATED_NODES, "running"); yield closeDebuggerClient(client); gBrowser.removeCurrentTab(); }); -function* checkAnimationsStates(walker, front, selectors, playState) { +function* checkStates(walker, animations, selectors, playState) { info("Checking the playState of all the nodes that have infinite running " + "animations"); for (let selector of selectors) { info("Getting the AnimationPlayerFront for node " + selector); - let [player] = yield getPlayersFor(walker, front, selector); + let [player] = yield getPlayersFor(walker, animations, selector); yield player.ready(); yield checkPlayState(player, selector, playState); } } -function* getPlayersFor(walker, front, selector) { +function* getPlayersFor(walker, animations, selector) { let node = yield walker.querySelector(walker.rootNode, selector); - return front.getAnimationPlayersForNode(node); + return animations.getAnimationPlayersForNode(node); } function* checkPlayState(player, selector, expectedState) { let state = yield player.getCurrentState(); is(state.playState, expectedState, "The playState of node " + selector + " is " + expectedState); }
rename from devtools/server/tests/browser/browser_animation_actors_03.js rename to devtools/server/tests/browser/browser_animation_playerState.js --- a/devtools/server/tests/browser/browser_animation_actors_03.js +++ b/devtools/server/tests/browser/browser_animation_playerState.js @@ -1,98 +1,96 @@ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Check the animation player's initial state -const {AnimationsFront} = require("devtools/server/actors/animation"); -const {InspectorFront} = require("devtools/server/actors/inspector"); - add_task(function*() { - let doc = yield addTab(MAIN_DOMAIN + "animation.html"); + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); - initDebuggerServer(); - let client = new DebuggerClient(DebuggerServer.connectPipe()); - let form = yield connectDebuggerClient(client); - let inspector = InspectorFront(client, form); - let walker = yield inspector.getWalker(); - let front = AnimationsFront(client, form); - - yield playerHasAnInitialState(walker, front); - yield playerStateIsCorrect(walker, front); + yield playerHasAnInitialState(walker, animations); + yield playerStateIsCorrect(walker, animations); yield closeDebuggerClient(client); gBrowser.removeCurrentTab(); }); -function* playerHasAnInitialState(walker, front) { +function* playerHasAnInitialState(walker, animations) { let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); - let [player] = yield front.getAnimationPlayersForNode(node); + let [player] = yield animations.getAnimationPlayersForNode(node); ok(player.initialState, "The player front has an initial state"); ok("startTime" in player.initialState, "Player's state has startTime"); ok("currentTime" in player.initialState, "Player's state has currentTime"); ok("playState" in player.initialState, "Player's state has playState"); ok("playbackRate" in player.initialState, "Player's state has playbackRate"); ok("name" in player.initialState, "Player's state has name"); ok("duration" in player.initialState, "Player's state has duration"); ok("delay" in player.initialState, "Player's state has delay"); - ok("iterationCount" in player.initialState, "Player's state has iterationCount"); - ok("isRunningOnCompositor" in player.initialState, "Player's state has isRunningOnCompositor"); + ok("iterationCount" in player.initialState, + "Player's state has iterationCount"); + ok("isRunningOnCompositor" in player.initialState, + "Player's state has isRunningOnCompositor"); ok("type" in player.initialState, "Player's state has type"); - ok("documentCurrentTime" in player.initialState, "Player's state has documentCurrentTime"); + ok("documentCurrentTime" in player.initialState, + "Player's state has documentCurrentTime"); } -function* playerStateIsCorrect(walker, front) { +function* playerStateIsCorrect(walker, animations) { info("Checking the state of the simple animation"); - let state = yield getAnimationStateForNode(walker, front, ".simple-animation", 0); + let state = yield getAnimationStateForNode(walker, animations, + ".simple-animation", 0); is(state.name, "move", "Name is correct"); is(state.duration, 2000, "Duration is correct"); // null = infinite count is(state.iterationCount, null, "Iteration count is correct"); is(state.playState, "running", "PlayState is correct"); is(state.playbackRate, 1, "PlaybackRate is correct"); is(state.type, "cssanimation", "Type is correct"); info("Checking the state of the transition"); - state = yield getAnimationStateForNode(walker, front, ".transition", 0); + state = yield getAnimationStateForNode(walker, animations, ".transition", 0); is(state.name, "width", "Transition name matches transition property"); is(state.duration, 5000, "Transition duration is correct"); // transitions run only once is(state.iterationCount, 1, "Transition iteration count is correct"); is(state.playState, "running", "Transition playState is correct"); is(state.playbackRate, 1, "Transition playbackRate is correct"); is(state.type, "csstransition", "Transition type is correct"); info("Checking the state of one of multiple animations on a node"); // Checking the 2nd player - state = yield getAnimationStateForNode(walker, front, ".multiple-animations", 1); + state = yield getAnimationStateForNode(walker, animations, + ".multiple-animations", 1); is(state.name, "glow", "The 2nd animation's name is correct"); is(state.duration, 1000, "The 2nd animation's duration is correct"); is(state.iterationCount, 5, "The 2nd animation's iteration count is correct"); is(state.playState, "running", "The 2nd animation's playState is correct"); is(state.playbackRate, 1, "The 2nd animation's playbackRate is correct"); info("Checking the state of an animation with delay"); - state = yield getAnimationStateForNode(walker, front, ".delayed-animation", 0); + state = yield getAnimationStateForNode(walker, animations, + ".delayed-animation", 0); is(state.delay, 5000, "The animation delay is correct"); info("Checking the state of an transition with delay"); - state = yield getAnimationStateForNode(walker, front, ".delayed-transition", 0); + state = yield getAnimationStateForNode(walker, animations, + ".delayed-transition", 0); is(state.delay, 3000, "The transition delay is correct"); } -function* getAnimationStateForNode(walker, front, nodeSelector, playerIndex) { +function* getAnimationStateForNode(walker, animations, nodeSelector, index) { let node = yield walker.querySelector(walker.rootNode, nodeSelector); - let players = yield front.getAnimationPlayersForNode(node); - let player = players[playerIndex]; + let players = yield animations.getAnimationPlayersForNode(node); + let player = players[index]; yield player.ready(); let state = yield player.getCurrentState(); return state; }
rename from devtools/server/tests/browser/browser_animation_actors_07.js rename to devtools/server/tests/browser/browser_animation_reconstructState.js --- a/devtools/server/tests/browser/browser_animation_actors_07.js +++ b/devtools/server/tests/browser/browser_animation_reconstructState.js @@ -2,38 +2,29 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Check that, even though the AnimationPlayerActor only sends the bits of its // state that change, the front reconstructs the whole state everytime. -const {AnimationsFront} = require("devtools/server/actors/animation"); -const {InspectorFront} = require("devtools/server/actors/inspector"); - add_task(function*() { - yield addTab(MAIN_DOMAIN + "animation.html"); + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); - initDebuggerServer(); - let client = new DebuggerClient(DebuggerServer.connectPipe()); - let form = yield connectDebuggerClient(client); - let inspector = InspectorFront(client, form); - let walker = yield inspector.getWalker(); - let front = AnimationsFront(client, form); - - yield playerHasCompleteStateAtAllTimes(walker, front); + yield playerHasCompleteStateAtAllTimes(walker, animations); yield closeDebuggerClient(client); gBrowser.removeCurrentTab(); }); -function* playerHasCompleteStateAtAllTimes(walker, front) { +function* playerHasCompleteStateAtAllTimes(walker, animations) { let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); - let [player] = yield front.getAnimationPlayersForNode(node); + let [player] = yield animations.getAnimationPlayersForNode(node); yield player.ready(); // Get the list of state key names from the initialstate. let keys = Object.keys(player.initialState); // Get the state over and over again and check that the object returned // contains all keys. // Normally, only the currentTime will have changed in between 2 calls.
rename from devtools/server/tests/browser/browser_animation_actors_15.js rename to devtools/server/tests/browser/browser_animation_refreshTransitions.js --- a/devtools/server/tests/browser/browser_animation_actors_15.js +++ b/devtools/server/tests/browser/browser_animation_refreshTransitions.js @@ -3,28 +3,19 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // When a transition finishes, no "removed" event is sent because it may still // be used, but when it restarts again (transitions back), then a new // AnimationPlayerFront should be sent, and the old one should be removed. -const {AnimationsFront} = require("devtools/server/actors/animation"); -const {InspectorFront} = require("devtools/server/actors/inspector"); - add_task(function*() { - yield addTab(MAIN_DOMAIN + "animation.html"); - - initDebuggerServer(); - let client = new DebuggerClient(DebuggerServer.connectPipe()); - let form = yield connectDebuggerClient(client); - let inspector = InspectorFront(client, form); - let walker = yield inspector.getWalker(); - let animations = AnimationsFront(client, form); + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); info("Retrieve the test node"); let node = yield walker.querySelector(walker.rootNode, ".all-transitions"); info("Retrieve the animation players for the node"); let players = yield animations.getAnimationPlayersForNode(node); is(players.length, 0, "The node has no animation players yet");
rename from devtools/server/tests/browser/browser_animation_actors_11.js rename to devtools/server/tests/browser/browser_animation_setCurrentTime.js --- a/devtools/server/tests/browser/browser_animation_actors_11.js +++ b/devtools/server/tests/browser/browser_animation_setCurrentTime.js @@ -2,28 +2,19 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Check that a player's currentTime can be changed and that the AnimationsActor // allows changing many players' currentTimes at once. -const {AnimationsFront} = require("devtools/server/actors/animation"); -const {InspectorFront} = require("devtools/server/actors/inspector"); - add_task(function*() { - yield addTab(MAIN_DOMAIN + "animation.html"); - - initDebuggerServer(); - let client = new DebuggerClient(DebuggerServer.connectPipe()); - let form = yield connectDebuggerClient(client); - let inspector = InspectorFront(client, form); - let walker = yield inspector.getWalker(); - let animations = AnimationsFront(client, form); + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); yield testSetCurrentTime(walker, animations); yield testSetCurrentTimes(walker, animations); yield closeDebuggerClient(client); gBrowser.removeCurrentTab(); });
rename from devtools/server/tests/browser/browser_animation_actors_12.js rename to devtools/server/tests/browser/browser_animation_setPlaybackRate.js --- a/devtools/server/tests/browser/browser_animation_actors_12.js +++ b/devtools/server/tests/browser/browser_animation_setPlaybackRate.js @@ -1,28 +1,19 @@ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Check that a player's playbackRate can be changed. -const {AnimationsFront} = require("devtools/server/actors/animation"); -const {InspectorFront} = require("devtools/server/actors/inspector"); - add_task(function*() { - yield addTab(MAIN_DOMAIN + "animation.html"); - - initDebuggerServer(); - let client = new DebuggerClient(DebuggerServer.connectPipe()); - let form = yield connectDebuggerClient(client); - let inspector = InspectorFront(client, form); - let walker = yield inspector.getWalker(); - let animations = AnimationsFront(client, form); + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); info("Retrieve an animated node"); let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); info("Retrieve the animation player for the node"); let [player] = yield animations.getAnimationPlayersForNode(node); ok(player.setPlaybackRate, "Player has the setPlaybackRate method");
rename from devtools/server/tests/browser/browser_animation_actors_01.js rename to devtools/server/tests/browser/browser_animation_simple.js --- a/devtools/server/tests/browser/browser_animation_actors_01.js +++ b/devtools/server/tests/browser/browser_animation_simple.js @@ -1,43 +1,35 @@ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Simple checks for the AnimationsActor -const {AnimationsFront} = require("devtools/server/actors/animation"); -const {InspectorFront} = require("devtools/server/actors/inspector"); - add_task(function*() { - let doc = yield addTab("data:text/html;charset=utf-8,<title>test</title><div></div>"); + let {client, walker, animations} = yield initAnimationsFrontForUrl( + "data:text/html;charset=utf-8,<title>test</title><div></div>"); - initDebuggerServer(); - let client = new DebuggerClient(DebuggerServer.connectPipe()); - let form = yield connectDebuggerClient(client); - let inspector = InspectorFront(client, form); - let walker = yield inspector.getWalker(); - let front = AnimationsFront(client, form); - - ok(front, "The AnimationsFront was created"); - ok(front.getAnimationPlayersForNode, "The getAnimationPlayersForNode method exists"); - ok(front.toggleAll, "The toggleAll method exists"); - ok(front.playAll, "The playAll method exists"); - ok(front.pauseAll, "The pauseAll method exists"); + ok(animations, "The AnimationsFront was created"); + ok(animations.getAnimationPlayersForNode, + "The getAnimationPlayersForNode method exists"); + ok(animations.toggleAll, "The toggleAll method exists"); + ok(animations.playAll, "The playAll method exists"); + ok(animations.pauseAll, "The pauseAll method exists"); let didThrow = false; try { - yield front.getAnimationPlayersForNode(null); + yield animations.getAnimationPlayersForNode(null); } catch (e) { didThrow = true; } ok(didThrow, "An exception was thrown for a missing NodeActor"); let invalidNode = yield walker.querySelector(walker.rootNode, "title"); - let players = yield front.getAnimationPlayersForNode(invalidNode); + let players = yield animations.getAnimationPlayersForNode(invalidNode); ok(Array.isArray(players), "An array of players was returned"); is(players.length, 0, "0 players have been returned for the invalid node"); yield closeDebuggerClient(client); gBrowser.removeCurrentTab(); });
rename from devtools/server/tests/browser/browser_animation_actors_04.js rename to devtools/server/tests/browser/browser_animation_updatedState.js --- a/devtools/server/tests/browser/browser_animation_actors_04.js +++ b/devtools/server/tests/browser/browser_animation_updatedState.js @@ -1,56 +1,48 @@ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Check the animation player's updated state -const {AnimationsFront} = require("devtools/server/actors/animation"); -const {InspectorFront} = require("devtools/server/actors/inspector"); - add_task(function*() { - let doc = yield addTab(MAIN_DOMAIN + "animation.html"); + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); - initDebuggerServer(); - let client = new DebuggerClient(DebuggerServer.connectPipe()); - let form = yield connectDebuggerClient(client); - let inspector = InspectorFront(client, form); - let walker = yield inspector.getWalker(); - let front = AnimationsFront(client, form); - - yield playStateIsUpdatedDynamically(walker, front); + yield playStateIsUpdatedDynamically(walker, animations); yield closeDebuggerClient(client); gBrowser.removeCurrentTab(); }); -function* playStateIsUpdatedDynamically(walker, front) { +function* playStateIsUpdatedDynamically(walker, animations) { let node = yield walker.querySelector(walker.rootNode, ".short-animation"); // Restart the animation to make sure we can get the player (it might already - // be finished by now). Do this by toggling the class and forcing a sync reflow - // using the CPOW. + // be finished by now). Do this by toggling the class and forcing a sync + // reflow using the CPOW. let cpow = content.document.querySelector(".short-animation"); cpow.classList.remove("short-animation"); let reflow = cpow.offsetWidth; cpow.classList.add("short-animation"); - let [player] = yield front.getAnimationPlayersForNode(node); + let [player] = yield animations.getAnimationPlayersForNode(node); yield player.ready(); let state = yield player.getCurrentState(); is(state.playState, "running", "The playState is running while the transition is running"); info("Wait until the animation stops (more than 1000ms)"); - yield wait(1500); // Waiting 1.5sec for good measure + // Waiting 1.5sec for good measure + yield wait(1500); state = yield player.getCurrentState(); is(state.playState, "finished", "The animation has ended and the state has been updated"); ok(state.currentTime > player.initialState.currentTime, "The currentTime has been updated"); }
--- a/devtools/server/tests/browser/head.js +++ b/devtools/server/tests/browser/head.js @@ -40,16 +40,32 @@ var addTab = Task.async(function* (url) yield new Promise(resolve => { let isBlank = url == "about:blank"; waitForFocus(resolve, content, isBlank); }); return tab.linkedBrowser.contentWindow.document; }); +function* initAnimationsFrontForUrl(url) { + const {AnimationsFront} = require("devtools/server/actors/animation"); + const {InspectorFront} = require("devtools/server/actors/inspector"); + + yield addTab(url); + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let inspector = InspectorFront(client, form); + let walker = yield inspector.getWalker(); + let animations = AnimationsFront(client, form); + + return {inspector, walker, animations, client}; +} + function initDebuggerServer() { try { // Sometimes debugger server does not get destroyed correctly by previous // tests. DebuggerServer.destroy(); } catch (ex) { } DebuggerServer.init(); DebuggerServer.addBrowserActors();
--- a/devtools/server/tests/mochitest/test_styles-modify.html +++ b/devtools/server/tests/mochitest/test_styles-modify.html @@ -47,34 +47,34 @@ addTest(function modifyProperties() { }).then(applied => { elementStyle = applied[0].rule; is(elementStyle.cssText, localNode.style.cssText, "Got expected css text"); // Will start with "color:blue" let changes = elementStyle.startModifyingProperties(); // Change an existing property... - changes.setProperty("color", "black"); + changes.setProperty(-1, "color", "black"); // Create a new property - changes.setProperty("background-color", "green"); + changes.setProperty(-1, "background-color", "green"); // Create a new property and then change it immediately. - changes.setProperty("border", "1px solid black"); - changes.setProperty("border", "2px solid black"); + changes.setProperty(-1, "border", "1px solid black"); + changes.setProperty(-1, "border", "2px solid black"); return changes.apply(); }).then(() => { is(elementStyle.cssText, "color: black; background-color: green; border: 2px solid black;", "Should have expected cssText"); is(elementStyle.cssText, localNode.style.cssText, "Local node and style front match."); // Remove all the properties let changes = elementStyle.startModifyingProperties(); - changes.removeProperty("color"); - changes.removeProperty("background-color"); - changes.removeProperty("border"); + changes.removeProperty(-1, "color"); + changes.removeProperty(-1, "background-color"); + changes.removeProperty(-1, "border"); return changes.apply(); }).then(() => { is(elementStyle.cssText, "", "Should have expected cssText"); is(elementStyle.cssText, localNode.style.cssText, "Local node and style front match."); }).then(runNextTest)); });
--- a/mobile/android/base/home/TabMenuStripLayout.java +++ b/mobile/android/base/home/TabMenuStripLayout.java @@ -1,15 +1,16 @@ /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.home; +import android.view.ViewGroup; import android.widget.LinearLayout; import android.content.res.ColorStateList; import org.mozilla.gecko.R; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -37,24 +38,27 @@ class TabMenuStripLayout extends LinearL private View fromTab; private int fromPosition; private int toPosition; private float progress; // This variable is used to predict the direction of scroll. private float prevProgress; private int tabContentStart; + private boolean titlebarFill; private int activeTextColor; private ColorStateList inactiveTextColor; TabMenuStripLayout(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabMenuStrip); final int stripResId = a.getResourceId(R.styleable.TabMenuStrip_strip, -1); + + titlebarFill = a.getBoolean(R.styleable.TabMenuStrip_titlebarFill, false); tabContentStart = a.getDimensionPixelSize(R.styleable.TabMenuStrip_tabContentStart, 0); activeTextColor = a.getColor(R.styleable.TabMenuStrip_activeTextColor, R.color.text_and_tabs_tray_grey); inactiveTextColor = a.getColorStateList(R.styleable.TabMenuStrip_inactiveTextColor); a.recycle(); if (stripResId != -1) { strip = getResources().getDrawable(stripResId); } @@ -62,16 +66,23 @@ class TabMenuStripLayout extends LinearL setWillNotDraw(false); } void onAddPagerView(String title) { final TextView button = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.tab_menu_strip, this, false); button.setText(title.toUpperCase()); button.setTextColor(inactiveTextColor); + // Set titles width to weight, or wrap text width. + if (titlebarFill) { + button.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f)); + } else { + button.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); + } + if (getChildCount() == 0) { button.setPadding(button.getPaddingLeft() + tabContentStart, button.getPaddingTop(), button.getPaddingRight(), button.getPaddingBottom()); } addView(button);
--- a/mobile/android/base/resources/layout/firstrun_pane.xml +++ b/mobile/android/base/resources/layout/firstrun_pane.xml @@ -17,13 +17,14 @@ android:background="@color/firstrun_pager_background"> <org.mozilla.gecko.home.TabMenuStrip android:layout_width="match_parent" android:layout_height="@dimen/tabs_strip_height" android:background="@color/firstrun_tabstrip" android:visibility="visible" android:layout_gravity="top" gecko:strip="@drawable/home_tab_menu_strip" + gecko:titlebarFill="true" gecko:activeTextColor="@android:color/white" gecko:inactiveTextColor="@color/divider_light" /> </org.mozilla.gecko.firstrun.FirstrunPager> </org.mozilla.gecko.firstrun.FirstrunPane>
--- a/mobile/android/base/resources/layout/tab_menu_strip.xml +++ b/mobile/android/base/resources/layout/tab_menu_strip.xml @@ -1,16 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <!-- 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/. --> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="0dp" android:layout_height="match_parent" - android:layout_weight="1" android:minWidth="@dimen/tabs_strip_button_width" android:background="@drawable/tabs_strip_indicator" android:paddingLeft="@dimen/tabs_strip_button_padding" android:paddingRight="@dimen/tabs_strip_button_padding" android:gravity="center" android:focusable="true" style="@style/TextAppearance.Widget.HomePagerTabMenuStrip"/>
--- a/mobile/android/base/resources/values/attrs.xml +++ b/mobile/android/base/resources/values/attrs.xml @@ -160,16 +160,17 @@ <attr name="android:verticalSpacing"/> </declare-styleable> <declare-styleable name="TabMenuStrip"> <attr name="strip" format="reference"/> <attr name="tabContentStart" format="dimension" /> <attr name="activeTextColor" format="color" /> <attr name="inactiveTextColor" format="color" /> + <attr name="titlebarFill" format="boolean" /> </declare-styleable> <declare-styleable name="TabPanelBackButton"> <attr name="rightDivider" format="reference"/> <attr name="dividerVerticalPadding" format="dimension"/> </declare-styleable> <declare-styleable name="EllipsisTextView">
--- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -849,17 +849,17 @@ pref("devtools.debugger.force-local", tr // Display a prompt when a new connection starts to accept/reject it pref("devtools.debugger.prompt-connection", true); // Block tools from seeing / interacting with certified apps pref("devtools.debugger.forbid-certified-apps", true); // List of permissions that a sideloaded app can't ask for pref("devtools.apps.forbidden-permissions", "embed-apps,engineering-mode,embed-widgets"); // DevTools default color unit -pref("devtools.defaultColorUnit", "hex"); +pref("devtools.defaultColorUnit", "authored"); // Used for devtools debugging pref("devtools.dump.emit", false); // Disable device discovery logging pref("devtools.discovery.log", false); // Whether to scan for DevTools devices via WiFi pref("devtools.remote.wifi.scan", true);
--- a/toolkit/components/alerts/moz.build +++ b/toolkit/components/alerts/moz.build @@ -7,18 +7,23 @@ MOCHITEST_MANIFESTS += ['test/mochitest.ini'] XPIDL_SOURCES += [ 'nsIAlertsService.idl', ] XPIDL_MODULE = 'alerts' +EXPORTS += [ + 'nsAlertsUtils.h', +] + UNIFIED_SOURCES += [ 'nsAlertsService.cpp', + 'nsAlertsUtils.cpp', 'nsXULAlerts.cpp', ] include('/ipc/chromium/chromium-config.mozbuild') FINAL_LIBRARY = 'xul' JAR_MANIFESTS += ['jar.mn']
--- a/toolkit/components/alerts/nsAlertsService.cpp +++ b/toolkit/components/alerts/nsAlertsService.cpp @@ -115,17 +115,17 @@ NS_IMETHODIMP nsAlertsService::ShowAlert if (aAlertListener) aAlertListener->Observe(nullptr, "alertfinished", PromiseFlatString(aAlertCookie).get()); return NS_OK; } // Use XUL notifications as a fallback if above methods have failed. rv = mXULAlerts.ShowAlertNotification(aImageUrl, aAlertTitle, aAlertText, aAlertTextClickable, aAlertCookie, aAlertListener, aAlertName, - aBidi, aLang, aInPrivateBrowsing); + aBidi, aLang, aPrincipal, aInPrivateBrowsing); return rv; #endif // !MOZ_WIDGET_ANDROID } NS_IMETHODIMP nsAlertsService::CloseAlert(const nsAString& aAlertName, nsIPrincipal* aPrincipal) { if (XRE_IsContentProcess()) {
new file mode 100644 --- /dev/null +++ b/toolkit/components/alerts/nsAlertsUtils.cpp @@ -0,0 +1,74 @@ +/* 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/. */ + +#include "nsAlertsUtils.h" + +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIStringBundle.h" +#include "nsIURI.h" +#include "nsXPIDLString.h" + +#define ALERTS_BUNDLE "chrome://alerts/locale/alert.properties" + +/* static */ +bool +nsAlertsUtils::IsActionablePrincipal(nsIPrincipal* aPrincipal) +{ + return aPrincipal && + !nsContentUtils::IsSystemOrExpandedPrincipal(aPrincipal) && + !aPrincipal->GetIsNullPrincipal(); +} + +/* static */ +void +nsAlertsUtils::GetSource(nsIPrincipal* aPrincipal, nsAString& aSource) +{ + nsAutoString hostPort; + GetSourceHostPort(aPrincipal, hostPort); + if (hostPort.IsEmpty()) { + return; + } + nsCOMPtr<nsIStringBundleService> stringService( + mozilla::services::GetStringBundleService()); + if (!stringService) { + return; + } + nsCOMPtr<nsIStringBundle> alertsBundle; + if (NS_WARN_IF(NS_FAILED(stringService->CreateBundle(ALERTS_BUNDLE, + getter_AddRefs(alertsBundle))))) { + return; + } + const char16_t* params[1] = { hostPort.get() }; + nsXPIDLString result; + if (NS_WARN_IF(NS_FAILED( + alertsBundle->FormatStringFromName(MOZ_UTF16("source.label"), params, 1, + getter_Copies(result))))) { + return; + } + aSource = result; +} + +/* static */ +void +nsAlertsUtils::GetSourceHostPort(nsIPrincipal* aPrincipal, + nsAString& aHostPort) +{ + if (!IsActionablePrincipal(aPrincipal)) { + return; + } + nsCOMPtr<nsIURI> principalURI; + if (NS_WARN_IF(NS_FAILED( + aPrincipal->GetURI(getter_AddRefs(principalURI))))) { + return; + } + if (!principalURI) { + return; + } + nsAutoCString hostPort; + if (NS_WARN_IF(NS_FAILED(principalURI->GetHostPort(hostPort)))) { + return; + } + CopyUTF8toUTF16(hostPort, aHostPort); +}
new file mode 100644 --- /dev/null +++ b/toolkit/components/alerts/nsAlertsUtils.h @@ -0,0 +1,39 @@ +/* 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/. */ + +#ifndef nsAlertsUtils_h +#define nsAlertsUtils_h + +#include "nsIPrincipal.h" +#include "nsString.h" + +class nsAlertsUtils final +{ +private: + nsAlertsUtils() = delete; + +public: + /** + * Indicates whether an alert from |aPrincipal| should include the source + * string and action buttons. Returns false if |aPrincipal| is |nullptr|, or + * a system, expanded, or null principal. + */ + static bool + IsActionablePrincipal(nsIPrincipal* aPrincipal); + + /** + * Sets |aSource| to the localized notification source string, or an empty + * string if |aPrincipal| is not actionable. + */ + static void + GetSource(nsIPrincipal* aPrincipal, nsAString& aSource); + + /** + * Sets |aHostPort| to the host and port from |aPrincipal|'s URI, or an + * empty string if |aPrincipal| is not actionable. + */ + static void + GetSourceHostPort(nsIPrincipal* aPrincipal, nsAString& aHostPort); +}; +#endif /* nsAlertsUtils_h */
--- a/toolkit/components/alerts/nsXULAlerts.cpp +++ b/toolkit/components/alerts/nsXULAlerts.cpp @@ -3,16 +3,17 @@ * 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/. */ #include "nsXULAlerts.h" #include "nsAutoPtr.h" #include "mozilla/LookAndFeel.h" #include "nsIServiceManager.h" +#include "nsAlertsUtils.h" #include "nsISupportsArray.h" #include "nsISupportsPrimitives.h" #include "nsPIDOMWindow.h" #include "nsIWindowWatcher.h" using namespace mozilla; #define ALERT_CHROME_URL "chrome://global/content/alerts/alert.xul" @@ -39,17 +40,18 @@ nsXULAlertObserver::Observe(nsISupports* return rv; } nsresult nsXULAlerts::ShowAlertNotification(const nsAString& aImageUrl, const nsAString& aAlertTitle, const nsAString& aAlertText, bool aAlertTextClickable, const nsAString& aAlertCookie, nsIObserver* aAlertListener, const nsAString& aAlertName, const nsAString& aBidi, - const nsAString& aLang, bool aInPrivateBrowsing) + const nsAString& aLang, nsIPrincipal* aPrincipal, + bool aInPrivateBrowsing) { nsCOMPtr<nsIWindowWatcher> wwatch(do_GetService(NS_WINDOWWATCHER_CONTRACTID)); nsCOMPtr<nsISupportsArray> argsArray; nsresult rv = NS_NewISupportsArray(getter_AddRefs(argsArray)); NS_ENSURE_SUCCESS(rv, rv); // create scriptable versions of our strings that we can store in our nsISupportsArray.... @@ -129,16 +131,26 @@ nsXULAlerts::ShowAlertNotification(const NS_ENSURE_SUCCESS(rv, rv); nsRefPtr<nsXULAlertObserver> alertObserver = new nsXULAlertObserver(this, aAlertName, aAlertListener); nsCOMPtr<nsISupports> iSupports(do_QueryInterface(alertObserver)); ifptr->SetData(iSupports); ifptr->SetDataIID(&NS_GET_IID(nsIObserver)); rv = argsArray->AppendElement(ifptr); NS_ENSURE_SUCCESS(rv, rv); + // The source contains the host and port of the site that sent the + // notification. It is empty for system alerts. + nsCOMPtr<nsISupportsString> scriptableAlertSource (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableAlertSource, NS_ERROR_FAILURE); + nsAutoString source; + nsAlertsUtils::GetSource(aPrincipal, source); + scriptableAlertSource->SetData(source); + rv = argsArray->AppendElement(scriptableAlertSource); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIDOMWindow> newWindow; nsAutoCString features("chrome,dialog=yes,titlebar=no,popup=yes"); if (aInPrivateBrowsing) { features.AppendLiteral(",private"); } rv = wwatch->OpenWindow(0, ALERT_CHROME_URL, "_blank", features.get(), argsArray, getter_AddRefs(newWindow)); NS_ENSURE_SUCCESS(rv, rv);
--- a/toolkit/components/alerts/nsXULAlerts.h +++ b/toolkit/components/alerts/nsXULAlerts.h @@ -20,17 +20,18 @@ public: } virtual ~nsXULAlerts() {} nsresult ShowAlertNotification(const nsAString& aImageUrl, const nsAString& aAlertTitle, const nsAString& aAlertText, bool aAlertTextClickable, const nsAString& aAlertCookie, nsIObserver* aAlertListener, const nsAString& aAlertName, const nsAString& aBidi, - const nsAString& aLang, bool aInPrivateBrowsing); + const nsAString& aLang, nsIPrincipal* aPrincipal, + bool aInPrivateBrowsing); nsresult CloseAlert(const nsAString& aAlertName); protected: nsInterfaceHashtable<nsStringHashKey, nsIDOMWindow> mNamedWindows; }; /** * This class wraps observers for alerts and watches
--- a/toolkit/components/alerts/resources/content/alert.js +++ b/toolkit/components/alerts/resources/content/alert.js @@ -27,19 +27,29 @@ function prefillAlertInfo() { // arguments[2] --> the alert text // arguments[3] --> is the text clickable? // arguments[4] --> the alert cookie to be passed back to the listener // arguments[5] --> the alert origin reported by the look and feel // arguments[6] --> bidi // arguments[7] --> lang // arguments[8] --> replaced alert window (nsIDOMWindow) // arguments[9] --> an optional callback listener (nsIObserver) + // arguments[10] -> the localized alert source string switch (window.arguments.length) { default: + case 11: { + let label = document.getElementById('alertSourceLabel'); + if (window.arguments[10]) { + label.hidden = false; + label.setAttribute('value', window.arguments[10]); + } else { + label.hidden = true; + } + } case 10: gAlertListener = window.arguments[9]; case 9: gReplacedWindow = window.arguments[8]; case 8: if (window.arguments[7]) { document.getElementById('alertTitleLabel').setAttribute('lang', window.arguments[7]); document.getElementById('alertTextLabel').setAttribute('lang', window.arguments[7]);
--- a/toolkit/components/alerts/resources/content/alert.xul +++ b/toolkit/components/alerts/resources/content/alert.xul @@ -27,16 +27,18 @@ <box> <hbox id="alertImageBox" class="alertImageBox" align="center" pack="center"> <image id="alertImage"/> </hbox> <vbox id="alertTextBox" class="alertTextBox"> <label id="alertTitleLabel" class="alertTitle plain"/> <label id="alertTextLabel" class="alertText plain"/> + <spacer flex="1"/> + <label id="alertSourceLabel" class="alertSource plain"/> </vbox> </box> <vbox class="alertCloseBox"> <toolbarbutton class="alertCloseButton close-icon" tooltiptext="&closeAlert.tooltip;" onclick="event.stopPropagation();" oncommand="close();"/>
--- a/toolkit/components/alerts/test/mochitest.ini +++ b/toolkit/components/alerts/test/mochitest.ini @@ -1,10 +1,12 @@ [DEFAULT] -skip-if = buildapp == 'b2g' +skip-if = buildapp == 'b2g' || buildapp == 'mulet' # Synchronous tests like test_alerts.html must come before # asynchronous tests like test_alerts_noobserve.html! [test_alerts.html] skip-if = toolkit == 'android' [test_alerts_noobserve.html] skip-if = (toolkit == 'android' && processor == 'x86') #x86 only [test_multiple_alerts.html] +[test_principal.html] +skip-if = toolkit == 'android'
--- a/toolkit/components/alerts/test/test_alerts.html +++ b/toolkit/components/alerts/test/test_alerts.html @@ -17,26 +17,26 @@ <br>If so, the test will finish once the notification disappears. <pre id="test"> <script class="testbody" type="text/javascript"> var observer = { alertShow: false, observe: function (aSubject, aTopic, aData) { - if (aTopic == "alertclickcallback") { + is(aData, "foobarcookie", "Checking whether the alert cookie was passed correctly"); + if (aTopic == "alertclickcallback") { todo(false, "Did someone click the notification while running mochitests? (Please don't.)"); } else if (aTopic == "alertshow") { ok(!this.alertShow, "Alert should not be shown more than once"); this.alertShow = true; } else { is(aTopic, "alertfinished", "Checking the topic for a finished notification"); SimpleTest.finish(); } - is(aData, "foobarcookie", "Checking whether the alert cookie was passed correctly"); } }; function runTest() { const Cc = SpecialPowers.Cc; const Ci = SpecialPowers.Ci; if (!("@mozilla.org/alerts-service;1" in Cc)) { @@ -57,19 +57,20 @@ function runTest() { return; } try { var alertName = "fiorello"; SimpleTest.waitForExplicitFinish(); notifier.showAlertNotification(null, "Notification test", "Surprise! I'm here to test notifications!", - false, "foobarcookie", observer, alertname); + false, "foobarcookie", observer, alertName); ok(true, "showAlertNotification() succeeded. Waiting for notification..."); - if (MAC) { + + if (SpecialPowers.Services.appinfo.OS == "Darwin") { // Notifications are native on OS X 10.8 and later, and when they are they // persist in the Notification Center. We need to close explicitly to avoid a hang. // This also works for XUL notifications when running this test on OS X < 10.8. notifier.closeAlert(alertName); } } catch (ex) { todo(false, "showAlertNotification() failed.", ex); SimpleTest.finish();
new file mode 100644 --- /dev/null +++ b/toolkit/components/alerts/test/test_principal.html @@ -0,0 +1,111 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 1202933</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body> +<p id="display"></p> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const Cc = SpecialPowers.Cc; +const Ci = SpecialPowers.Ci; +const Services = SpecialPowers.Services; + +const notifier = Cc["@mozilla.org/alerts-service;1"] + .getService(Ci.nsIAlertsService); + +function notify(alertName, principal) { + return new Promise((resolve, reject) => { + var source; + function observe(subject, topic, data) { + if (topic == "alertclickcallback") { + reject(new Error("Alerts should not be clicked during test")); + } else if (topic == "alertshow") { + var alertWindows = Services.wm.getEnumerator("alert:alert"); + ok(alertWindows.hasMoreElements(), "Should show alert"); + var alertWindow = alertWindows.getNext(); + source = alertWindow.document.getElementById("alertSourceLabel").getAttribute("value"); + } else { + is(topic, "alertfinished", "Should hide alert"); + resolve(source); + } + } + notifier.showAlertNotification(null, "Notification test", + "Surprise! I'm here to test notifications!", + false, alertName, observe, alertName, + null, null, null, principal); + if (SpecialPowers.Services.appinfo.OS == "Darwin") { + notifier.closeAlert(alertName); + } + }); +} + +function* testNoPrincipal() { + var source = yield notify("noPrincipal", null); + ok(!source, "Should omit source without principal"); +} + +function* testSystemPrincipal() { + var principal = Services.scriptSecurityManager.getSystemPrincipal(); + var source = yield notify("systemPrincipal", principal); + ok(!source, "Should omit source for system principal"); +} + +function* testExpandedPrincipal() { + var principal = Services.scriptSecurityManager.createExpandedPrincipal([], 0); + var source = yield notify("expandedPrincipal", principal); + ok(!source, "Should omit source for expanded principal"); +} + +function* testNullPrincipal() { + var principal = Services.scriptSecurityManager.createNullPrincipal({}); + var source = yield notify("nullPrincipal", principal); + ok(!source, "Should omit source for null principal"); +} + +function* testNodePrincipal() { + var principal = SpecialPowers.wrap(document).nodePrincipal; + var source = yield notify("nodePrincipal", principal); + + var stringBundle = Services.strings.createBundle( + "chrome://alerts/locale/alert.properties" + ); + var localizedSource = stringBundle.formatStringFromName( + "source.label", [principal.URI.hostPort], 1); + is(source, localizedSource, "Should include source for node principal"); +} + +function runTest() { + if (!("@mozilla.org/alerts-service;1" in Cc)) { + todo(false, "Alerts service does not exist in this application"); + return; + } + + if ("@mozilla.org/system-alerts-service;1" in Cc) { + todo(false, "Native alerts service exists in this application"); + return; + } + + ok(true, "Alerts service exists in this application"); + + ok(!Services.wm.getEnumerator("alert:alert").hasMoreElements(), + "Alerts should not be present at the start of the test."); + + add_task(testNoPrincipal); + add_task(testSystemPrincipal); + add_task(testExpandedPrincipal); + add_task(testNullPrincipal); + add_task(testNodePrincipal); +} + +runTest(); +</script> +</pre> +</body> +</html>
--- a/toolkit/locales/en-US/chrome/alerts/alert.properties +++ b/toolkit/locales/en-US/chrome/alerts/alert.properties @@ -4,9 +4,13 @@ # LOCALIZATION NOTE(closeButton.title): Used as the close button text for web notifications on OS X. # This should ideally match the string that OS X uses for the close button on alert-type # notifications. OS X will truncate the value if it's too long. closeButton.title = Close # LOCALIZATION NOTE(actionButton.label): Used as the button label to provide more actions on OS X notifications. OS X will truncate this if it's too long. actionButton.label = … webActions.disable.label = Disable notifications from this site +# LOCALIZATION NOTE(source.label): Used to show the URL of the site that +# sent the notification (e.g., "via mozilla.org"). "%1$S" is the source host +# and port. +source.label=via %1$S webActions.settings.label = Notification settings
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm @@ -2803,35 +2803,39 @@ this.XPIProvider = { getService(Ci.nsIWindowWatcher); ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant); // Ensure any changes to the add-ons list are flushed to disk Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, !XPIDatabase.writeAddonsList()); }, - updateSystemAddons: Task.async(function XPI_updateSystemAddons() { + updateSystemAddons: Task.async(function* XPI_updateSystemAddons() { + let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS]; + if (!systemAddonLocation) + return; + // Don't do anything in safe mode if (Services.appinfo.inSafeMode) return; // Download the list of system add-ons let url = Preferences.get(PREF_SYSTEM_ADDON_UPDATE_URL, null); if (!url) - return; + return systemAddonLocation.cleanDirectories(); url = UpdateUtils.formatUpdateURL(url); logger.info(`Starting system add-on update check from ${url}.`); let addonList = yield ProductAddonChecker.getProductAddonList(url); // If there was no list then do nothing. if (!addonList) { logger.info("No system add-ons list was returned."); - return; + return systemAddonLocation.cleanDirectories(); } addonList = new Map([for (spec of addonList) [spec.id, { spec, path: null, addon: null }]]); let getAddonsInLocation = (location) => { return new Promise(resolve => { XPIDatabase.getAddonsInLocation(location, resolve); }); @@ -2848,32 +2852,30 @@ this.XPIProvider = { return false; if (wantedInfo.spec.version != addon.version) return false; } return true; }; - let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS]; - // If this matches the current set in the profile location then do nothing. let updatedAddons = addonMap(yield getAddonsInLocation(KEY_APP_SYSTEM_ADDONS)); if (setMatches(addonList, updatedAddons)) { logger.info("Retaining existing updated system add-ons."); - return; + return systemAddonLocation.cleanDirectories(); } // If this matches the current set in the default location then reset the // updated set. let defaultAddons = addonMap(yield getAddonsInLocation(KEY_APP_SYSTEM_DEFAULTS)); if (setMatches(addonList, defaultAddons)) { logger.info("Resetting system add-ons."); systemAddonLocation.resetAddonSet(); - return; + return systemAddonLocation.cleanDirectories(); } // Download all the add-ons // Bug 1204158: If we already have some of these locally then just use those let downloadAddon = Task.async(function*(item) { try { let sourceAddon = updatedAddons.get(item.spec.id); if (sourceAddon && sourceAddon.version == item.spec.version) { @@ -2946,16 +2948,18 @@ this.XPIProvider = { try { yield OS.File.remove(item.path); } catch (e) { logger.warn(`Failed to remove temporary file ${item.path}.`, e); } } } + + yield systemAddonLocation.cleanDirectories(); } }), /** * Verifies that all installed add-ons are still correctly signed. */ verifySignatures: function XPI_verifySignatures() { XPIDatabase.getAddonList(a => true, (addons) => { @@ -7501,16 +7505,17 @@ Object.assign(MutableDirectoryInstallLoc * The nsIFile directory for the install location * @param aScope * The scope of add-ons installed in this location * @param aResetSet * True to throw away the current add-on set */ function SystemAddonInstallLocation(aName, aDirectory, aScope, aResetSet) { this._baseDir = aDirectory; + this._nextDir = null; if (aResetSet) this.resetAddonSet(); this._addonSet = this._loadAddonSet(); this._directory = null; if (this._addonSet.directory) { @@ -7622,16 +7627,68 @@ Object.assign(SystemAddonInstallLocation /** * Resets the add-on set so on the next startup the default set will be used. */ resetAddonSet: function() { this._saveAddonSet({ schema: 1, addons: {} }); }, /** + * Removes any directories not currently in use or pending use after a + * restart. Any errors that happen here don't really matter as we'll attempt + * to cleanup again next time. + */ + cleanDirectories: Task.async(function*() { + let iterator; + try { + iterator = new OS.File.DirectoryIterator(this._baseDir.path); + } + catch (e) { + logger.error("Failed to clean updated system add-ons directories.", e); + return; + } + + try { + let entries = []; + + yield iterator.forEach(entry => { + // Skip the directory currently in use + if (this._directory && this._directory.path == entry.path) + return; + + // Skip the next directory + if (this._nextDir && this._nextDir.path == entry.path) + return; + + entries.push(entry); + }); + + for (let entry of entries) { + if (entry.isDir) { + yield OS.File.removeDir(entry.path, { + ignoreAbsent: true, + ignorePermissions: true, + }); + } + else { + yield OS.File.remove(entry.path, { + ignoreAbsent: true, + }); + } + } + } + catch (e) { + logger.error("Failed to clean updated system add-ons directories.", e); + } + finally { + iterator.close(); + } + }), + + /** * Installs a new set of system add-ons into the location and updates the * add-on set in prefs. We wait to switch state until a restart. */ installAddonSet: Task.async(function(aAddons) { // Make sure the base dir exists yield OS.File.makeDir(this._baseDir.path, { ignoreExisting: true }); let newDir = this._baseDir.clone(); @@ -7682,16 +7739,17 @@ Object.assign(SystemAddonInstallLocation let state = { schema: 1, directory: newDir.leafName, addons: {} }; for (let addon of aAddons) { state.addons[addon.id] = { version: addon.version } } this._saveAddonSet(state); + this._nextDir = newDir; }), }); #ifdef XP_WIN /** * An object that identifies a registry install location for add-ons. The location * consists of a registry key which contains string values mapping ID to the * path where an add-on is installed
--- a/toolkit/mozapps/extensions/test/xpcshell/test_system_update.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update.js @@ -8,48 +8,61 @@ const PREF_APP_UPDATE_ENABLED = Components.utils.import("resource://testing-common/httpd.js"); const { computeHash } = Components.utils.import("resource://gre/modules/addons/ProductAddonChecker.jsm"); // Enable signature checks for these tests Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true); BootstrapMonitor.init(); -const featureDir = FileUtils.getDir("ProfD", ["features"]); +const featureDir = FileUtils.getDir("ProfD", ["features"], false); function getCurrentFeatureDir() { let dir = featureDir.clone(); let set = JSON.parse(Services.prefs.getCharPref(PREF_SYSTEM_ADDON_SET)); dir.append(set.directory); return dir; } -// Build the test sets -let dir = FileUtils.getDir("ProfD", ["features", "prefilled"], true); -do_get_file("data/system_addons/system2_2.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi"); -do_get_file("data/system_addons/system3_2.xpi").copyTo(dir, "system3@tests.mozilla.org.xpi"); +function clearFeatureDir() { + // Delete any existing directories + if (featureDir.exists()) + featureDir.remove(true); + + Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_SET); +} -// Mark these in the past so the startup file scan notices when files have changed properly -FileUtils.getFile("ProfD", ["features", "prefilled", "system2@tests.mozilla.org.xpi"]).lastModifiedTime -= 10000; -FileUtils.getFile("ProfD", ["features", "prefilled", "system3@tests.mozilla.org.xpi"]).lastModifiedTime -= 10000; +function buildPrefilledFeatureDir() { + clearFeatureDir(); + + // Build the test set + let dir = FileUtils.getDir("ProfD", ["features", "prefilled"], true); + + do_get_file("data/system_addons/system2_2.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi"); + do_get_file("data/system_addons/system3_2.xpi").copyTo(dir, "system3@tests.mozilla.org.xpi"); -const prefilledSet = { - schema: 1, - directory: dir.leafName, - addons: { - "system2@tests.mozilla.org": { - version: "2.0" - }, - "system3@tests.mozilla.org": { - version: "2.0" - }, - } -}; + // Mark these in the past so the startup file scan notices when files have changed properly + FileUtils.getFile("ProfD", ["features", "prefilled", "system2@tests.mozilla.org.xpi"]).lastModifiedTime -= 10000; + FileUtils.getFile("ProfD", ["features", "prefilled", "system3@tests.mozilla.org.xpi"]).lastModifiedTime -= 10000; -dir = FileUtils.getDir("ProfD", ["sysfeatures", "hidden"], true); + Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify({ + schema: 1, + directory: dir.leafName, + addons: { + "system2@tests.mozilla.org": { + version: "2.0" + }, + "system3@tests.mozilla.org": { + version: "2.0" + }, + } + })); +} + +let dir = FileUtils.getDir("ProfD", ["sysfeatures", "hidden"], true); do_get_file("data/system_addons/system1_1.xpi").copyTo(dir, "system1@tests.mozilla.org.xpi"); do_get_file("data/system_addons/system2_1.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi"); dir = FileUtils.getDir("ProfD", ["sysfeatures", "prefilled"], true); do_get_file("data/system_addons/system2_2.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi"); do_get_file("data/system_addons/system3_2.xpi").copyTo(dir, "system3@tests.mozilla.org.xpi"); const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"], true); @@ -197,44 +210,44 @@ function* check_installed(inProfile, ... * * setup: A task to setup the profile into the initial state. * initialState: The initial expected system add-on state after setup has run. */ const TEST_CONDITIONS = { // Runs tests with no updated or default system add-ons initially installed blank: { setup: function*() { - Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_SET); + clearFeatureDir(); distroDir.leafName = "empty"; }, initialState: [false, null, null, null, null, null], }, // Runs tests with default system add-ons installed withAppSet: { setup: function*() { - Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_SET); + clearFeatureDir(); distroDir.leafName = "prefilled"; }, initialState: [false, null, "2.0", "2.0", null, null], }, // Runs tests with updated system add-ons installed withProfileSet: { setup: function*() { - Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify(prefilledSet)); + buildPrefilledFeatureDir(); distroDir.leafName = "empty"; }, initialState: [true, null, "2.0", "2.0", null, null], }, // Runs tests with both default and updated system add-ons installed withBothSets: { setup: function*() { - Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify(prefilledSet)); + buildPrefilledFeatureDir(); distroDir.leafName = "hidden"; }, initialState: [true, null, "2.0", "2.0", null, null], }, }; /** @@ -342,16 +355,32 @@ const TESTS = { } add_task(function* setup() { // Initialise the profile startupManager(); yield promiseShutdownManager(); }) +function* get_directories() { + let subdirs = []; + + if (yield OS.File.exists(featureDir.path)) { + let iterator = new OS.File.DirectoryIterator(featureDir.path); + yield iterator.forEach(entry => { + if (entry.isDir) { + subdirs.push(entry); + } + }); + iterator.close(); + } + + return subdirs; +} + function* setup_conditions(setup) { do_print("Clearing existing database."); Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_SET); distroDir.leafName = "empty"; startupManager(false); yield promiseShutdownManager(); do_print("Setting up conditions."); @@ -359,19 +388,38 @@ function* setup_conditions(setup) { startupManager(false); // Make sure the initial state is correct do_print("Checking initial state."); yield check_installed(...setup.initialState); } -function* verify_state(finalState) { +function* verify_state(initialState, finalState = undefined) { + let expectedDirs = 0; + + // If the initial state was using the profile set then that directory will + // still exist. + if (initialState[0]) + expectedDirs++; + + if (finalState == undefined) { + finalState = initialState; + } + else { + // If the new state is using the profile then that directory will exist. + if (finalState[0]) + expectedDirs++; + } + do_print("Checking final state."); + let dirs = yield get_directories(); + do_check_eq(dirs.length, expectedDirs); + // Bug 1204156: Currently switching to the new state requires a restart // yield check_installed(...finalState); // Check that the new state is active after a restart yield promiseRestartManager(); yield check_installed(...finalState); } @@ -391,17 +439,17 @@ function* exec_test(setup, test) { } } catch (e) { if (!test.fails) { do_throw(e); } } - yield verify_state(test.finalState ? test.finalState : setup.initialState); + yield verify_state(setup.initialState, test.finalState); yield promiseShutdownManager(); } for (let setup of Object.keys(TEST_CONDITIONS)) { for (let test of Object.keys(TESTS)) { add_task(function*() { do_print("Running test " + setup + " " + test); @@ -418,17 +466,18 @@ for (let setup of Object.keys(TEST_CONDI add_task(function* test_addon_update() { yield setup_conditions(TEST_CONDITIONS.blank); yield update_all_addons(yield build_xml([ { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" }, { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" } ])); - yield verify_state([true, null, "2.0", "2.0", null, null]); + yield verify_state(TEST_CONDITIONS.blank.initialState, + [true, null, "2.0", "2.0", null, null]); yield promiseShutdownManager(); }); // Disabling app updates should block system add-on updates add_task(function* test_app_update_disabled() { yield setup_conditions(TEST_CONDITIONS.blank); @@ -485,17 +534,18 @@ add_task(function* test_match_default_re yield install_system_addons(yield build_xml([ { id: "system1@tests.mozilla.org", version: "1.0", path: "system1_1.xpi" }, { id: "system2@tests.mozilla.org", version: "1.0", path: "system2_1.xpi" } ])); // This should revert to the default set instead of installing new versions // into an updated set. - yield verify_state([false, "1.0", "1.0", null, null, null]); + yield verify_state(TEST_CONDITIONS.withBothSets.initialState, + [false, "1.0", "1.0", null, null, null]); yield promiseShutdownManager(); }); // Tests that a set that matches the current set works add_task(function* test_match_current() { yield setup_conditions(TEST_CONDITIONS.withBothSets); @@ -518,12 +568,54 @@ add_task(function* test_no_download() { yield setup_conditions(TEST_CONDITIONS.withBothSets); // The missing file here is unneeded since there is a local version already yield install_system_addons(yield build_xml([ { id: "system2@tests.mozilla.org", version: "2.0", path: "missing.xpi" }, { id: "system4@tests.mozilla.org", version: "1.0", path: "system4_1.xpi" } ])); - yield verify_state([true, null, "2.0", null, "1.0", null]); + yield verify_state(TEST_CONDITIONS.withBothSets.initialState, + [true, null, "2.0", null, "1.0", null]); yield promiseShutdownManager(); }); + +// Tests that a second update before a restart works +add_task(function* test_double_update() { + yield setup_conditions(TEST_CONDITIONS.withAppSet); + + yield install_system_addons(yield build_xml([ + { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" }, + { id: "system3@tests.mozilla.org", version: "1.0", path: "system3_1.xpi" } + ])); + + yield install_system_addons(yield build_xml([ + { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }, + { id: "system4@tests.mozilla.org", version: "1.0", path: "system4_1.xpi" } + ])); + + yield verify_state(TEST_CONDITIONS.withAppSet.initialState, + [true, null, null, "2.0", "1.0", null]); + + yield promiseShutdownManager(); +}); + +// A second update after a restart will delete the original unused set +add_task(function* test_update_purges() { + yield setup_conditions(TEST_CONDITIONS.withBothSets); + + yield install_system_addons(yield build_xml([ + { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" }, + { id: "system3@tests.mozilla.org", version: "1.0", path: "system3_1.xpi" } + ])); + + yield verify_state(TEST_CONDITIONS.withBothSets.initialState, + [true, null, "2.0", "1.0", null, null]); + + yield install_system_addons(yield build_xml(null)); + + let dirs = yield get_directories(); + do_check_eq(dirs.length, 1); + + yield promiseShutdownManager(); +}); +
--- a/toolkit/themes/shared/alert-common.css +++ b/toolkit/themes/shared/alert-common.css @@ -33,16 +33,20 @@ background-image: linear-gradient(rgba(255,255,255,0.4), rgba(255,255,255,0.3)); } .alertTitle { font-weight: bold; font-size: 110%; } +.alertSource { + color: gray; +} + #alertImage { max-width: 48px; max-height: 48px; list-style-image: url(chrome://global/skin/alerts/notification-48.png); } #alertNotification[clickable="true"] { cursor: pointer;
--- a/widget/cocoa/OSXNotificationCenter.mm +++ b/widget/cocoa/OSXNotificationCenter.mm @@ -12,16 +12,17 @@ #include "imgLoader.h" #import "nsCocoaUtils.h" #include "nsContentUtils.h" #include "nsObjCExceptions.h" #include "nsString.h" #include "nsCOMPtr.h" #include "nsIObserver.h" #include "nsIContentPolicy.h" +#include "nsAlertsUtils.h" #include "imgRequestProxy.h" using namespace mozilla; #if !defined(MAC_OS_X_VERSION_10_8) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_8) @protocol NSUserNotificationCenterDelegate @end static NSString * const NSUserNotificationDefaultSoundName = @"DefaultSoundName"; @@ -235,26 +236,30 @@ OSXNotificationCenter::ShowAlertNotifica const nsAString & aData, nsIPrincipal * aPrincipal, bool aInPrivateBrowsing) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; Class unClass = NSClassFromString(@"NSUserNotification"); id<FakeNSUserNotification> notification = [[unClass alloc] init]; - notification.title = [NSString stringWithCharacters:(const unichar *)aAlertTitle.BeginReading() - length:aAlertTitle.Length()]; - notification.informativeText = [NSString stringWithCharacters:(const unichar *)aAlertText.BeginReading() - length:aAlertText.Length()]; + notification.title = nsCocoaUtils::ToNSString(aAlertTitle); + + nsAutoString hostPort; + nsAlertsUtils::GetSourceHostPort(aPrincipal, hostPort); + if (!hostPort.IsEmpty()) { + notification.subtitle = nsCocoaUtils::ToNSString(hostPort); + } + + notification.informativeText = nsCocoaUtils::ToNSString(aAlertText); notification.soundName = NSUserNotificationDefaultSoundName; notification.hasActionButton = NO; // If this is not an application/extension alert, show additional actions dealing with permissions. - if (aPrincipal && !nsContentUtils::IsSystemOrExpandedPrincipal(aPrincipal) - && !aPrincipal->GetIsNullPrincipal()) { + if (nsAlertsUtils::IsActionablePrincipal(aPrincipal)) { nsCOMPtr<nsIStringBundleService> sbs = do_GetService(NS_STRINGBUNDLE_CONTRACTID); nsCOMPtr<nsIStringBundle> bundle; nsresult rv = sbs->CreateBundle("chrome://alerts/locale/alert.properties", getter_AddRefs(bundle)); if (NS_SUCCEEDED(rv)) { nsXPIDLString closeButtonTitle, actionButtonTitle, disableButtonTitle, settingsButtonTitle; bundle->GetStringFromName(NS_LITERAL_STRING("closeButton.title").get(), getter_Copies(closeButtonTitle)); bundle->GetStringFromName(NS_LITERAL_STRING("actionButton.label").get(), @@ -271,17 +276,17 @@ OSXNotificationCenter::ShowAlertNotifica [(NSObject*)notification setValue:@(YES) forKey:@"_alwaysShowAlternateActionMenu"]; [(NSObject*)notification setValue:@[ nsCocoaUtils::ToNSString(disableButtonTitle), nsCocoaUtils::ToNSString(settingsButtonTitle) ] forKey:@"_alternateActionButtonTitles"]; } } - NSString *alertName = [NSString stringWithCharacters:(const unichar *)aAlertName.BeginReading() length:aAlertName.Length()]; + NSString *alertName = nsCocoaUtils::ToNSString(aAlertName); if (!alertName) { return NS_ERROR_FAILURE; } notification.userInfo = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:alertName, nil] forKeys:[NSArray arrayWithObjects:@"name", nil]]; OSXNotificationInfo *osxni = new OSXNotificationInfo(alertName, aAlertListener, aAlertCookie); @@ -331,17 +336,17 @@ OSXNotificationCenter::ShowAlertNotifica } NS_IMETHODIMP OSXNotificationCenter::CloseAlert(const nsAString& aAlertName, nsIPrincipal* aPrincipal) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; - NSString *alertName = [NSString stringWithCharacters:(const unichar *)aAlertName.BeginReading() length:aAlertName.Length()]; + NSString *alertName = nsCocoaUtils::ToNSString(aAlertName); CloseAlertCocoaString(alertName); return NS_OK; NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; } void OSXNotificationCenter::CloseAlertCocoaString(NSString *aAlertName)