diff --git a/browser/devtools/styleinspector/rule-view.js b/browser/devtools/styleinspector/rule-view.js index 141312588ca..260fbf98139 100644 --- a/browser/devtools/styleinspector/rule-view.js +++ b/browser/devtools/styleinspector/rule-view.js @@ -302,10 +302,13 @@ ElementStyle.prototype = { */ markOverridden: function(pseudo="") { // Gather all the text properties applied by these rules, ordered - // from more- to less-specific. + // from more- to less-specific. Text properties from keyframes rule are + // excluded from being marked as overridden since a number of criteria such + // as time, and animation overlay are required to be check in order to + // determine if the property is overridden. let textProps = []; for (let rule of this.rules) { - if (rule.pseudoElement == pseudo) { + if (rule.pseudoElement == pseudo && !rule.keyframes) { textProps = textProps.concat(rule.textProps.slice(0).reverse()); } } @@ -419,6 +422,7 @@ function Rule(aElementStyle, aOptions) { this.isSystem = aOptions.isSystem; this.inherited = aOptions.inherited || null; + this.keyframes = aOptions.keyframes || null; this._modificationDepth = 0; if (this.domRule) { @@ -466,6 +470,18 @@ Rule.prototype = { return this._inheritedSource; }, + get keyframesName() { + if (this._keyframesName) { + return this._keyframesName; + } + this._keyframesName = ""; + if (this.keyframes) { + this._keyframesName = + CssLogic._strings.formatStringFromName("rule.keyframe", [this.keyframes.name], 1); + } + return this._keyframesName; + }, + get selectorText() { return this.domRule.selectors ? this.domRule.selectors.join(", ") : CssLogic.l10n("rule.sourceElement"); }, @@ -1627,25 +1643,6 @@ CssRuleView.prototype = { return this._pseudoElementLabel; }, - togglePseudoElementVisibility: function(value) { - this._showPseudoElements = !!value; - let isOpen = this.showPseudoElements; - - Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements", - isOpen); - - this.element.classList.toggle("show-pseudo-elements", isOpen); - - if (this.pseudoElementTwisty) { - if (isOpen) { - this.pseudoElementTwisty.setAttribute("open", "true"); - } - else { - this.pseudoElementTwisty.removeAttribute("open"); - } - } - }, - get showPseudoElements() { if (this._showPseudoElements === undefined) { this._showPseudoElements = @@ -1654,6 +1651,68 @@ CssRuleView.prototype = { return this._showPseudoElements; }, + /** + * Creates an expandable container in the rule view + * @param {String} aLabel The label for the container header + * @param {Boolean} isPseudo Whether or not the container will hold + * pseudo element rules + * @return {DOMNode} The container element + */ + createExpandableContainer: function(aLabel, isPseudo = false) { + let header = this.doc.createElementNS(HTML_NS, "div"); + header.className = this._getRuleViewHeaderClassName(true); + header.classList.add("show-expandable-container"); + header.textContent = aLabel; + + let twisty = this.doc.createElementNS(HTML_NS, "span"); + twisty.className = "ruleview-expander theme-twisty"; + twisty.setAttribute("open", "true"); + + header.insertBefore(twisty, header.firstChild); + this.element.appendChild(header); + + let container = this.doc.createElementNS(HTML_NS, "div"); + container.classList.add("ruleview-expandable-container"); + this.element.appendChild(container); + + let toggleContainerVisibility = (isPseudo, showPseudo) => { + let isOpen = twisty.getAttribute("open"); + + if (isPseudo) { + this._showPseudoElements = !!showPseudo; + + Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements", + this.showPseudoElements); + + header.classList.toggle("show-expandable-container", + this.showPseudoElements); + + isOpen = !this.showPseudoElements; + } else { + header.classList.toggle("show-expandable-container"); + } + + if (isOpen) { + twisty.removeAttribute("open"); + } else { + twisty.setAttribute("open", "true"); + } + }; + + header.addEventListener("dblclick", () => { + toggleContainerVisibility(isPseudo, !this.showPseudoElements); + }, false); + twisty.addEventListener("click", () => { + toggleContainerVisibility(isPseudo, !this.showPseudoElements); + }, false); + + if (isPseudo) { + toggleContainerVisibility(isPseudo, this.showPseudoElements); + } + + return container; + }, + _getRuleViewHeaderClassName: function(isPseudo) { let baseClassName = "theme-gutter ruleview-header"; return isPseudo ? baseClassName + " ruleview-expandable-header" : baseClassName; @@ -1666,8 +1725,10 @@ CssRuleView.prototype = { // Run through the current list of rules, attaching // their editors in order. Create editors if needed. let lastInheritedSource = ""; + let lastKeyframes = null; let seenPseudoElement = false; let seenNormalElement = false; + let container = null; if (!this._elementStyle.rules) { return; @@ -1688,7 +1749,7 @@ CssRuleView.prototype = { } let inheritedSource = rule.inheritedSource; - if (inheritedSource != lastInheritedSource) { + if (inheritedSource && inheritedSource != lastInheritedSource) { let div = this.doc.createElementNS(HTML_NS, "div"); div.className = this._getRuleViewHeaderClassName(); div.textContent = inheritedSource; @@ -1698,33 +1759,25 @@ CssRuleView.prototype = { if (!seenPseudoElement && rule.pseudoElement) { seenPseudoElement = true; + container = this.createExpandableContainer(this.pseudoElementLabel, true); + } - let div = this.doc.createElementNS(HTML_NS, "div"); - div.className = this._getRuleViewHeaderClassName(true); - div.textContent = this.pseudoElementLabel; - div.addEventListener("dblclick", () => { - this.togglePseudoElementVisibility(!this.showPseudoElements); - }, false); - - let twisty = this.pseudoElementTwisty = - this.doc.createElementNS(HTML_NS, "span"); - twisty.className = "ruleview-expander theme-twisty"; - twisty.addEventListener("click", () => { - this.togglePseudoElementVisibility(!this.showPseudoElements); - }, false); - - div.insertBefore(twisty, div.firstChild); - this.element.appendChild(div); + let keyframes = rule.keyframes; + if (keyframes && keyframes != lastKeyframes) { + lastKeyframes = keyframes; + container = this.createExpandableContainer(rule.keyframesName); } if (!rule.editor) { rule.editor = new RuleEditor(this, rule); } - this.element.appendChild(rule.editor.element); + if (container && (rule.pseudoElement || keyframes)) { + container.appendChild(rule.editor.element); + } else { + this.element.appendChild(rule.editor.element); + } } - - this.togglePseudoElementVisibility(this.showPseudoElements); } }; @@ -1756,7 +1809,10 @@ function RuleEditor(aRuleView, aRule) { RuleEditor.prototype = { get isSelectorEditable() { let toolbox = this.ruleView.inspector.toolbox; - return toolbox.target.client.traits.selectorEditable; + return this.isEditable && + toolbox.target.client.traits.selectorEditable && + this.rule.domRule.type !== ELEMENT_STYLE && + this.rule.domRule.type !== Ci.nsIDOMCSSRule.KEYFRAME_RULE }, _create: function() { @@ -1764,9 +1820,6 @@ RuleEditor.prototype = { this.element.className = "ruleview-rule theme-separator"; this.element.setAttribute("uneditable", !this.isEditable); this.element._ruleEditor = this; - if (this.rule.pseudoElement) { - this.element.classList.add("ruleview-rule-pseudo-element"); - } // Give a relative position for the inplace editor's measurement // span to be placed absolutely against. @@ -1808,8 +1861,7 @@ RuleEditor.prototype = { class: "ruleview-selector theme-fg-color3" }); - if (this.isEditable && this.rule.domRule.type !== ELEMENT_STYLE && - this.isSelectorEditable) { + if (this.isSelectorEditable) { this.selectorContainer.addEventListener("click", aEvent => { // Clicks within the selector shouldn't propagate any further. aEvent.stopPropagation(); @@ -1925,6 +1977,8 @@ RuleEditor.prototype = { // style, just show the text directly. if (this.rule.domRule.type === ELEMENT_STYLE) { this.selectorText.textContent = this.rule.selectorText; + } else if (this.rule.domRule.type === Ci.nsIDOMCSSRule.KEYFRAME_RULE) { + this.selectorText.textContent = this.rule.domRule.keyText; } else { this.rule.domRule.selectors.forEach((selector, i) => { if (i != 0) { diff --git a/browser/devtools/styleinspector/ruleview.css b/browser/devtools/styleinspector/ruleview.css index a1ef3b148aa..2f86a8839c1 100644 --- a/browser/devtools/styleinspector/ruleview.css +++ b/browser/devtools/styleinspector/ruleview.css @@ -38,11 +38,11 @@ display: none; } -.ruleview-rule-pseudo-element { +.ruleview-expandable-container { display: none; } -.show-pseudo-elements .ruleview-rule-pseudo-element { +.show-expandable-container + .ruleview-expandable-container { display: block; } diff --git a/browser/devtools/styleinspector/test/browser.ini b/browser/devtools/styleinspector/test/browser.ini index 506bb6d73e7..1b92732c3f7 100644 --- a/browser/devtools/styleinspector/test/browser.ini +++ b/browser/devtools/styleinspector/test/browser.ini @@ -9,6 +9,8 @@ support-files = doc_content_stylesheet_linked.css doc_content_stylesheet_script.css doc_content_stylesheet_xul.css + doc_keyframeanimation.html + doc_keyframeanimation.css doc_matched_selectors.html doc_media_queries.html doc_pseudoelement.html @@ -75,6 +77,8 @@ support-files = skip-if = os == "win" && debug # bug 963492 [browser_ruleview_inherit.js] [browser_ruleview_keybindings.js] +[browser_ruleview_keyframes-rule_01.js] +[browser_ruleview_keyframes-rule_02.js] [browser_ruleview_livepreview.js] [browser_ruleview_mathml-element.js] [browser_ruleview_media-queries.js] diff --git a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js index cf5f4735b6b..e5264ee01da 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js @@ -51,7 +51,8 @@ let test = asyncTest(function*() { function* testEditSelector(view, name) { info("Test editing existing selector fields"); - let idRuleEditor = getRuleViewRuleEditor(view, 1); + let idRuleEditor = getRuleViewRuleEditor(view, 1) || + getRuleViewRuleEditor(view, 1, 0); info("Focusing an existing selector name in the rule-view"); let editor = yield focusEditableField(idRuleEditor.selectorText); diff --git a/browser/devtools/styleinspector/test/browser_ruleview_keyframes-rule_01.js b/browser/devtools/styleinspector/test/browser_ruleview_keyframes-rule_01.js new file mode 100644 index 00000000000..d84b50a00fc --- /dev/null +++ b/browser/devtools/styleinspector/test/browser_ruleview_keyframes-rule_01.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 that keyframe rules and gutters are displayed correctly in the rule view + +const TEST_URI = TEST_URL_ROOT + "doc_keyframeanimation.html"; + +let test = asyncTest(function*() { + yield addTab(TEST_URI); + + let {toolbox, inspector, view} = yield openRuleView(); + + yield testPacman(inspector, view); + yield testBoxy(inspector, view); + yield testMoxy(inspector, view); +}); + +function* testPacman(inspector, view) { + info("Test content and gutter in the keyframes rule of #pacman"); + + let { + rules, + element, + elementStyle + } = yield assertKeyframeRules("#pacman", inspector, view, { + elementRulesNb: 2, + keyframeRulesNb: 2, + keyframesRules: ["pacman", "pacman"], + keyframeRules: ["100%", "100%"] + }); + + let gutters = assertGutters(view, { + guttersNbs: 2, + gutterHeading: ["Keyframes pacman", "Keyframes pacman"] + }); +} + +function* testBoxy(inspector, view) { + info("Test content and gutter in the keyframes rule of #boxy"); + + let { + rules, + element, + elementStyle + } = yield assertKeyframeRules("#boxy", inspector, view, { + elementRulesNb: 3, + keyframeRulesNb: 3, + keyframesRules: ["boxy", "boxy", "boxy"], + keyframeRules: ["10%", "20%", "100%"] + }); + + let gutters = assertGutters(view, { + guttersNbs: 1, + gutterHeading: ["Keyframes boxy"] + }); +} + +function testMoxy(inspector, view) { + info("Test content and gutter in the keyframes rule of #moxy"); + + let { + rules, + element, + elementStyle + } = yield assertKeyframeRules("#moxy", inspector, view, { + elementRulesNb: 3, + keyframeRulesNb: 4, + keyframesRules: ["boxy", "boxy", "boxy", "moxy"], + keyframeRules: ["10%", "20%", "100%", "100%"] + }); + + let gutters = assertGutters(view, { + guttersNbs: 2, + gutterHeading: ["Keyframes boxy", "Keyframes moxy"] + }); +} + +function* testNode(selector, inspector, view) { + let element = getNode(selector); + yield selectNode(element, inspector); + let elementStyle = view._elementStyle; + return {element, elementStyle}; +} + +function* assertKeyframeRules(selector, inspector, view, expected) { + let {element, elementStyle} = yield testNode(selector, inspector, view); + + let rules = { + elementRules: elementStyle.rules.filter(rule => !rule.keyframes), + keyframeRules: elementStyle.rules.filter(rule => rule.keyframes) + }; + + is(rules.elementRules.length, expected.elementRulesNb, selector + + " has the correct number of non keyframe element rules"); + is(rules.keyframeRules.length, expected.keyframeRulesNb, selector + + " has the correct number of keyframe rules"); + + let i = 0; + for (let keyframeRule of rules.keyframeRules) { + ok(keyframeRule.keyframes.name == expected.keyframesRules[i], + keyframeRule.keyframes.name + " has the correct keyframes name"); + ok(keyframeRule.domRule.keyText == expected.keyframeRules[i], + keyframeRule.domRule.keyText + " selector heading is correct"); + i++; + } + + return {rules, element, elementStyle}; +} + +function assertGutters(view, expected) { + let gutters = view.element.querySelectorAll(".theme-gutter"); + + is(gutters.length, expected.guttersNbs, + "There are " + gutters.length + " gutter headings"); + + let i = 0; + for (let gutter of gutters) { + is(gutter.textContent, expected.gutterHeading[i], + "Correct " + gutter.textContent + " gutter headings"); + i++; + } + + return gutters; +} diff --git a/browser/devtools/styleinspector/test/browser_ruleview_keyframes-rule_02.js b/browser/devtools/styleinspector/test/browser_ruleview_keyframes-rule_02.js new file mode 100644 index 00000000000..5c3b53f954a --- /dev/null +++ b/browser/devtools/styleinspector/test/browser_ruleview_keyframes-rule_02.js @@ -0,0 +1,111 @@ +/* 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 verifies the content of the keyframes rule and property changes +// to keyframe rules + +const TEST_URI = TEST_URL_ROOT + "doc_keyframeanimation.html"; + +let test = asyncTest(function*() { + yield addTab(TEST_URI); + + let {toolbox, inspector, view} = yield openRuleView(); + + yield testPacman(inspector, view); + yield testBoxy(inspector, view); +}); + +function* testPacman(inspector, view) { + info("Test content in the keyframes rule of #pacman"); + + let { + rules, + element, + elementStyle + } = yield getKeyframeRules("#pacman", inspector, view); + + info("Test text properties for Keyframes #pacman"); + + is + ( + convertTextPropsToString(rules.keyframeRules[0].textProps), + "left: 750px", + "Keyframe pacman (100%) property is correct" + ); + + info("Test dynamic changes to keyframe rule for #pacman"); + + let defaultView = element.ownerDocument.defaultView; + let ruleEditor = view.element.children[5].childNodes[0]._ruleEditor; + ruleEditor.addProperty("opacity", "0"); + ruleEditor.addProperty("top", "750px"); + + yield ruleEditor._applyingModifications; + yield once(element, "animationend"); + + is + ( + convertTextPropsToString(rules.keyframeRules[1].textProps), + "left: 750px; opacity: 0; top: 750px", + "Keyframe pacman (100%) property is correct" + ); + + is(defaultView.getComputedStyle(element).getPropertyValue("opacity"), "0", + "Added opacity property should have been used."); + is(defaultView.getComputedStyle(element).getPropertyValue("top"), "750px", + "Added top property should have been used."); +} + +function* testBoxy(inspector, view) { + info("Test content in the keyframes rule of #boxy"); + + let { + rules, + element, + elementStyle + } = yield getKeyframeRules("#boxy", inspector, view); + + info("Test text properties for Keyframes #boxy"); + + is + ( + convertTextPropsToString(rules.keyframeRules[0].textProps), + "background-color: blue", + "Keyframe boxy (10%) property is correct" + ); + + is + ( + convertTextPropsToString(rules.keyframeRules[1].textProps), + "background-color: green", + "Keyframe boxy (20%) property is correct" + ); + + is + ( + convertTextPropsToString(rules.keyframeRules[2].textProps), + "opacity: 0", + "Keyframe boxy (100%) property is correct" + ); +} + +function convertTextPropsToString(textProps) { + return textProps.map(t => t.name + ": " + t.value).join("; "); +} + +function* getKeyframeRules(selector, inspector, view) { + let element = getNode(selector); + + yield selectNode(element, inspector); + let elementStyle = view._elementStyle; + + let rules = { + elementRules: elementStyle.rules.filter(rule => !rule.keyframes), + keyframeRules: elementStyle.rules.filter(rule => rule.keyframes) + }; + + return {rules, element, elementStyle}; +} diff --git a/browser/devtools/styleinspector/test/browser_ruleview_pseudo-element.js b/browser/devtools/styleinspector/test/browser_ruleview_pseudo-element.js index a4e86c56312..f2a0853202a 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_pseudo-element.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_pseudo-element.js @@ -38,24 +38,22 @@ function* testTopLeft(inspector, view) { // Make sure that clicking on the twisty hides pseudo elements let expander = gutters[0].querySelector(".ruleview-expander"); - ok (view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are expanded"); + ok (view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are expanded"); expander.click(); - ok (!view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are collapsed by twisty"); + ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are collapsed by twisty"); expander.click(); - ok (view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are expanded again"); + ok (view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are expanded again"); // Make sure that dblclicking on the header container also toggles the pseudo elements EventUtils.synthesizeMouseAtCenter(gutters[0], {clickCount: 2}, inspector.sidebar.getWindowForTab("ruleview")); - ok (!view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are collapsed by dblclicking"); + ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are collapsed by dblclicking"); let defaultView = element.ownerDocument.defaultView; let elementRule = rules.elementRules[0]; - let elementRuleView = [].filter.call(view.element.children, e => { - return e._ruleEditor && e._ruleEditor.rule === elementRule; - })[0]._ruleEditor; + let elementRuleView = getRuleViewRuleEditor(view, 3); let elementAfterRule = rules.afterRules[0]; - let elementAfterRuleView = [].filter.call(view.element.children, (e) => { + let elementAfterRuleView = [].filter.call(view.element.children[1].children, (e) => { return e._ruleEditor && e._ruleEditor.rule === elementAfterRule; })[0]._ruleEditor; @@ -68,7 +66,7 @@ function* testTopLeft(inspector, view) { ); let elementBeforeRule = rules.beforeRules[0]; - let elementBeforeRuleView = [].filter.call(view.element.children, (e) => { + let elementBeforeRuleView = [].filter.call(view.element.children[1].children, (e) => { return e._ruleEditor && e._ruleEditor.rule === elementBeforeRule; })[0]._ruleEditor; @@ -138,10 +136,10 @@ function* testTopRight(inspector, view) { let gutters = assertGutters(view); let expander = gutters[0].querySelector(".ruleview-expander"); - ok (!view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements remain collapsed after switching element"); + ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements remain collapsed after switching element"); expander.scrollIntoView(); expander.click(); - ok (view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are shown again after clicking twisty"); + ok (view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are shown again after clicking twisty"); } function* testBottomRight(inspector, view) { @@ -183,7 +181,7 @@ function* testParagraph(inspector, view) { let gutters = assertGutters(view); let elementFirstLineRule = rules.firstLineRules[0]; - let elementFirstLineRuleView = [].filter.call(view.element.children, (e) => { + let elementFirstLineRuleView = [].filter.call(view.element.children[1].children, (e) => { return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule; })[0]._ruleEditor; @@ -195,7 +193,7 @@ function* testParagraph(inspector, view) { ); let elementFirstLetterRule = rules.firstLetterRules[0]; - let elementFirstLetterRuleView = [].filter.call(view.element.children, (e) => { + let elementFirstLetterRuleView = [].filter.call(view.element.children[1].children, (e) => { return e._ruleEditor && e._ruleEditor.rule === elementFirstLetterRule; })[0]._ruleEditor; @@ -207,7 +205,7 @@ function* testParagraph(inspector, view) { ); let elementSelectionRule = rules.selectionRules[0]; - let elementSelectionRuleView = [].filter.call(view.element.children, (e) => { + let elementSelectionRuleView = [].filter.call(view.element.children[1].children, (e) => { return e._ruleEditor && e._ruleEditor.rule === elementSelectionRule; })[0]._ruleEditor; diff --git a/browser/devtools/styleinspector/test/doc_keyframeanimation.css b/browser/devtools/styleinspector/test/doc_keyframeanimation.css new file mode 100644 index 00000000000..64582ed358f --- /dev/null +++ b/browser/devtools/styleinspector/test/doc_keyframeanimation.css @@ -0,0 +1,84 @@ +/* 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/. */ + +.box { + height: 50px; + width: 50px; +} + +.circle { + width: 20px; + height: 20px; + border-radius: 10px; + background-color: #FFCB01; +} + +#pacman { + width: 0px; + height: 0px; + border-right: 60px solid transparent; + border-top: 60px solid #FFCB01; + border-left: 60px solid #FFCB01; + border-bottom: 60px solid #FFCB01; + border-top-left-radius: 60px; + border-bottom-left-radius: 60px; + border-top-right-radius: 60px; + border-bottom-right-radius: 60px; + top: 120px; + left: 150px; + position: absolute; + animation-name: pacman; + animation-fill-mode: forwards; + animation-timing-function: linear; + animation-duration: 15s; +} + +#boxy { + top: 170px; + left: 450px; + position: absolute; + animation: 4s linear 0s normal none infinite boxy; +} + + +#moxy { + animation-name: moxy, boxy; + animation-delay: 3.5s; + animation-duration: 2s; + top: 170px; + left: 650px; + position: absolute; +} + +@-moz-keyframes pacman { + 100% { + left: 750px; + } +} + +@keyframes pacman { + 100% { + left: 750px; + } +} + +@keyframes boxy { + 10% { + background-color: blue; + } + + 20% { + background-color: green; + } + + 100% { + opacity: 0; + } +} + +@keyframes moxy { + to { + opacity: 0; + } +} diff --git a/browser/devtools/styleinspector/test/doc_keyframeanimation.html b/browser/devtools/styleinspector/test/doc_keyframeanimation.html new file mode 100644 index 00000000000..4e02c32f057 --- /dev/null +++ b/browser/devtools/styleinspector/test/doc_keyframeanimation.html @@ -0,0 +1,13 @@ + + +
+