diff options
Diffstat (limited to 'devtools/client/inspector')
653 files changed, 64650 insertions, 0 deletions
diff --git a/devtools/client/inspector/.eslintrc.js b/devtools/client/inspector/.eslintrc.js new file mode 100644 index 000000000..6f5ff309c --- /dev/null +++ b/devtools/client/inspector/.eslintrc.js @@ -0,0 +1,15 @@ +"use strict"; + +module.exports = { + // Extend from the devtools eslintrc. + "extends": "../../.eslintrc.js", + + "rules": { + // The inspector is being migrated to HTML and cleaned of + // chrome-privileged code, so this rule disallows requiring chrome + // code. Some files in the inspector disable this rule still. The + // goal is to enable the rule globally on all files. + /* eslint-disable max-len */ + "mozilla/reject-some-requires": ["error", "^(chrome|chrome:.*|resource:.*|devtools/server/.*|.*\\.jsm|devtools/shared/platform/(chome|content)/.*)$"], + }, +}; diff --git a/devtools/client/inspector/breadcrumbs.js b/devtools/client/inspector/breadcrumbs.js new file mode 100644 index 000000000..b2041164c --- /dev/null +++ b/devtools/client/inspector/breadcrumbs.js @@ -0,0 +1,921 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const promise = require("promise"); + +const {ELLIPSIS} = require("devtools/shared/l10n"); + +const MAX_LABEL_LENGTH = 40; + +const NS_XHTML = "http://www.w3.org/1999/xhtml"; +const SCROLL_REPEAT_MS = 100; + +const EventEmitter = require("devtools/shared/event-emitter"); +const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts"); + +// Some margin may be required for visible element detection. +const SCROLL_MARGIN = 1; + +/** + * Component to replicate functionality of XUL arrowscrollbox + * for breadcrumbs + * + * @param {Window} win The window containing the breadcrumbs + * @parem {DOMNode} container The element in which to put the scroll box + */ +function ArrowScrollBox(win, container) { + this.win = win; + this.doc = win.document; + this.container = container; + EventEmitter.decorate(this); + this.init(); +} + +ArrowScrollBox.prototype = { + + // Scroll behavior, exposed for testing + scrollBehavior: "smooth", + + /** + * Build the HTML, add to the DOM and start listening to + * events + */ + init: function () { + this.constructHtml(); + + this.onUnderflow(); + + this.onScroll = this.onScroll.bind(this); + this.onStartBtnClick = this.onStartBtnClick.bind(this); + this.onEndBtnClick = this.onEndBtnClick.bind(this); + this.onStartBtnDblClick = this.onStartBtnDblClick.bind(this); + this.onEndBtnDblClick = this.onEndBtnDblClick.bind(this); + this.onUnderflow = this.onUnderflow.bind(this); + this.onOverflow = this.onOverflow.bind(this); + + this.inner.addEventListener("scroll", this.onScroll, false); + this.startBtn.addEventListener("mousedown", this.onStartBtnClick, false); + this.endBtn.addEventListener("mousedown", this.onEndBtnClick, false); + this.startBtn.addEventListener("dblclick", this.onStartBtnDblClick, false); + this.endBtn.addEventListener("dblclick", this.onEndBtnDblClick, false); + + // Overflow and underflow are moz specific events + this.inner.addEventListener("underflow", this.onUnderflow, false); + this.inner.addEventListener("overflow", this.onOverflow, false); + }, + + /** + * Determine whether the current text directionality is RTL + */ + isRtl: function () { + return this.win.getComputedStyle(this.container).direction === "rtl"; + }, + + /** + * Scroll to the specified element using the current scroll behavior + * @param {Element} element element to scroll + * @param {String} block desired alignment of element after scrolling + */ + scrollToElement: function (element, block) { + element.scrollIntoView({ block: block, behavior: this.scrollBehavior }); + }, + + /** + * Call the given function once; then continuously + * while the mouse button is held + * @param {Function} repeatFn the function to repeat while the button is held + */ + clickOrHold: function (repeatFn) { + let timer; + let container = this.container; + + function handleClick() { + cancelHold(); + repeatFn(); + } + + let window = this.win; + function cancelHold() { + window.clearTimeout(timer); + container.removeEventListener("mouseout", cancelHold, false); + container.removeEventListener("mouseup", handleClick, false); + } + + function repeated() { + repeatFn(); + timer = window.setTimeout(repeated, SCROLL_REPEAT_MS); + } + + container.addEventListener("mouseout", cancelHold, false); + container.addEventListener("mouseup", handleClick, false); + timer = window.setTimeout(repeated, SCROLL_REPEAT_MS); + }, + + /** + * When start button is dbl clicked scroll to first element + */ + onStartBtnDblClick: function () { + let children = this.inner.childNodes; + if (children.length < 1) { + return; + } + + let element = this.inner.childNodes[0]; + this.scrollToElement(element, "start"); + }, + + /** + * When end button is dbl clicked scroll to last element + */ + onEndBtnDblClick: function () { + let children = this.inner.childNodes; + if (children.length < 1) { + return; + } + + let element = children[children.length - 1]; + this.scrollToElement(element, "start"); + }, + + /** + * When start arrow button is clicked scroll towards first element + */ + onStartBtnClick: function () { + let scrollToStart = () => { + let element = this.getFirstInvisibleElement(); + if (!element) { + return; + } + + let block = this.isRtl() ? "end" : "start"; + this.scrollToElement(element, block); + }; + + this.clickOrHold(scrollToStart); + }, + + /** + * When end arrow button is clicked scroll towards last element + */ + onEndBtnClick: function () { + let scrollToEnd = () => { + let element = this.getLastInvisibleElement(); + if (!element) { + return; + } + + let block = this.isRtl() ? "start" : "end"; + this.scrollToElement(element, block); + }; + + this.clickOrHold(scrollToEnd); + }, + + /** + * Event handler for scrolling, update the + * enabled/disabled status of the arrow buttons + */ + onScroll: function () { + let first = this.getFirstInvisibleElement(); + if (!first) { + this.startBtn.setAttribute("disabled", "true"); + } else { + this.startBtn.removeAttribute("disabled"); + } + + let last = this.getLastInvisibleElement(); + if (!last) { + this.endBtn.setAttribute("disabled", "true"); + } else { + this.endBtn.removeAttribute("disabled"); + } + }, + + /** + * On underflow, make the arrow buttons invisible + */ + onUnderflow: function () { + this.startBtn.style.visibility = "collapse"; + this.endBtn.style.visibility = "collapse"; + this.emit("underflow"); + }, + + /** + * On overflow, show the arrow buttons + */ + onOverflow: function () { + this.startBtn.style.visibility = "visible"; + this.endBtn.style.visibility = "visible"; + this.emit("overflow"); + }, + + /** + * Check whether the element is to the left of its container but does + * not also span the entire container. + * @param {Number} left the left scroll point of the container + * @param {Number} right the right edge of the container + * @param {Number} elementLeft the left edge of the element + * @param {Number} elementRight the right edge of the element + */ + elementLeftOfContainer: function (left, right, elementLeft, elementRight) { + return elementLeft < (left - SCROLL_MARGIN) + && elementRight < (right - SCROLL_MARGIN); + }, + + /** + * Check whether the element is to the right of its container but does + * not also span the entire container. + * @param {Number} left the left scroll point of the container + * @param {Number} right the right edge of the container + * @param {Number} elementLeft the left edge of the element + * @param {Number} elementRight the right edge of the element + */ + elementRightOfContainer: function (left, right, elementLeft, elementRight) { + return elementLeft > (left + SCROLL_MARGIN) + && elementRight > (right + SCROLL_MARGIN); + }, + + /** + * Get the first (i.e. furthest left for LTR) + * non or partly visible element in the scroll box + */ + getFirstInvisibleElement: function () { + let elementsList = Array.from(this.inner.childNodes).reverse(); + + let predicate = this.isRtl() ? + this.elementRightOfContainer : this.elementLeftOfContainer; + return this.findFirstWithBounds(elementsList, predicate); + }, + + /** + * Get the last (i.e. furthest right for LTR) + * non or partly visible element in the scroll box + */ + getLastInvisibleElement: function () { + let predicate = this.isRtl() ? + this.elementLeftOfContainer : this.elementRightOfContainer; + return this.findFirstWithBounds(this.inner.childNodes, predicate); + }, + + /** + * Find the first element that matches the given predicate, called with bounds + * information + * @param {Array} elements an ordered list of elements + * @param {Function} predicate a function to be called with bounds + * information + */ + findFirstWithBounds: function (elements, predicate) { + let left = this.inner.scrollLeft; + let right = left + this.inner.clientWidth; + for (let element of elements) { + let elementLeft = element.offsetLeft - element.parentElement.offsetLeft; + let elementRight = elementLeft + element.offsetWidth; + + // Check that the starting edge of the element is out of the visible area + // and that the ending edge does not span the whole container + if (predicate(left, right, elementLeft, elementRight)) { + return element; + } + } + + return null; + }, + + /** + * Build the HTML for the scroll box and insert it into the DOM + */ + constructHtml: function () { + this.startBtn = this.createElement("div", "scrollbutton-up", + this.container); + this.createElement("div", "toolbarbutton-icon", this.startBtn); + + this.createElement("div", "arrowscrollbox-overflow-start-indicator", + this.container); + this.inner = this.createElement("div", "html-arrowscrollbox-inner", + this.container); + this.createElement("div", "arrowscrollbox-overflow-end-indicator", + this.container); + + this.endBtn = this.createElement("div", "scrollbutton-down", + this.container); + this.createElement("div", "toolbarbutton-icon", this.endBtn); + }, + + /** + * Create an XHTML element with the given class name, and append it to the + * parent. + * @param {String} tagName name of the tag to create + * @param {String} className class of the element + * @param {DOMNode} parent the parent node to which it should be appended + * @return {DOMNode} The new element + */ + createElement: function (tagName, className, parent) { + let el = this.doc.createElementNS(NS_XHTML, tagName); + el.className = className; + if (parent) { + parent.appendChild(el); + } + + return el; + }, + + /** + * Remove event handlers and clean up + */ + destroy: function () { + this.inner.removeEventListener("scroll", this.onScroll, false); + this.startBtn.removeEventListener("mousedown", + this.onStartBtnClick, false); + this.endBtn.removeEventListener("mousedown", this.onEndBtnClick, false); + this.startBtn.removeEventListener("dblclick", + this.onStartBtnDblClick, false); + this.endBtn.removeEventListener("dblclick", + this.onRightBtnDblClick, false); + + // Overflow and underflow are moz specific events + this.inner.removeEventListener("underflow", this.onUnderflow, false); + this.inner.removeEventListener("overflow", this.onOverflow, false); + }, +}; + +/** + * Display the ancestors of the current node and its children. + * Only one "branch" of children are displayed (only one line). + * + * Mechanism: + * - If no nodes displayed yet: + * then display the ancestor of the selected node and the selected node; + * else select the node; + * - If the selected node is the last node displayed, append its first (if any). + * + * @param {InspectorPanel} inspector The inspector hosting this widget. + */ +function HTMLBreadcrumbs(inspector) { + this.inspector = inspector; + this.selection = this.inspector.selection; + this.win = this.inspector.panelWin; + this.doc = this.inspector.panelDoc; + this._init(); +} + +exports.HTMLBreadcrumbs = HTMLBreadcrumbs; + +HTMLBreadcrumbs.prototype = { + get walker() { + return this.inspector.walker; + }, + + _init: function () { + this.outer = this.doc.getElementById("inspector-breadcrumbs"); + this.arrowScrollBox = new ArrowScrollBox( + this.win, + this.outer); + + this.container = this.arrowScrollBox.inner; + this.scroll = this.scroll.bind(this); + this.arrowScrollBox.on("overflow", this.scroll); + + this.outer.addEventListener("click", this, true); + this.outer.addEventListener("mouseover", this, true); + this.outer.addEventListener("mouseout", this, true); + this.outer.addEventListener("focus", this, true); + + this.shortcuts = new KeyShortcuts({ window: this.win, target: this.outer }); + this.handleShortcut = this.handleShortcut.bind(this); + + this.shortcuts.on("Right", this.handleShortcut); + this.shortcuts.on("Left", this.handleShortcut); + + // We will save a list of already displayed nodes in this array. + this.nodeHierarchy = []; + + // Last selected node in nodeHierarchy. + this.currentIndex = -1; + + // Used to build a unique breadcrumb button Id. + this.breadcrumbsWidgetItemId = 0; + + this.update = this.update.bind(this); + this.updateSelectors = this.updateSelectors.bind(this); + this.selection.on("new-node-front", this.update); + this.selection.on("pseudoclass", this.updateSelectors); + this.selection.on("attribute-changed", this.updateSelectors); + this.inspector.on("markupmutation", this.update); + this.update(); + }, + + /** + + * Build a string that represents the node: tagName#id.class1.class2. + * @param {NodeFront} node The node to pretty-print + * @return {String} + */ + prettyPrintNodeAsText: function (node) { + let text = node.displayName; + if (node.isPseudoElement) { + text = node.isBeforePseudoElement ? "::before" : "::after"; + } + + if (node.id) { + text += "#" + node.id; + } + + if (node.className) { + let classList = node.className.split(/\s+/); + for (let i = 0; i < classList.length; i++) { + text += "." + classList[i]; + } + } + + for (let pseudo of node.pseudoClassLocks) { + text += pseudo; + } + + return text; + }, + + /** + * Build <span>s that represent the node: + * <span class="breadcrumbs-widget-item-tag">tagName</span> + * <span class="breadcrumbs-widget-item-id">#id</span> + * <span class="breadcrumbs-widget-item-classes">.class1.class2</span> + * @param {NodeFront} node The node to pretty-print + * @returns {DocumentFragment} + */ + prettyPrintNodeAsXHTML: function (node) { + let tagLabel = this.doc.createElementNS(NS_XHTML, "span"); + tagLabel.className = "breadcrumbs-widget-item-tag plain"; + + let idLabel = this.doc.createElementNS(NS_XHTML, "span"); + idLabel.className = "breadcrumbs-widget-item-id plain"; + + let classesLabel = this.doc.createElementNS(NS_XHTML, "span"); + classesLabel.className = "breadcrumbs-widget-item-classes plain"; + + let pseudosLabel = this.doc.createElementNS(NS_XHTML, "span"); + pseudosLabel.className = "breadcrumbs-widget-item-pseudo-classes plain"; + + let tagText = node.displayName; + if (node.isPseudoElement) { + tagText = node.isBeforePseudoElement ? "::before" : "::after"; + } + let idText = node.id ? ("#" + node.id) : ""; + let classesText = ""; + + if (node.className) { + let classList = node.className.split(/\s+/); + for (let i = 0; i < classList.length; i++) { + classesText += "." + classList[i]; + } + } + + // Figure out which element (if any) needs ellipsing. + // Substring for that element, then clear out any extras + // (except for pseudo elements). + let maxTagLength = MAX_LABEL_LENGTH; + let maxIdLength = MAX_LABEL_LENGTH - tagText.length; + let maxClassLength = MAX_LABEL_LENGTH - tagText.length - idText.length; + + if (tagText.length > maxTagLength) { + tagText = tagText.substr(0, maxTagLength) + ELLIPSIS; + idText = classesText = ""; + } else if (idText.length > maxIdLength) { + idText = idText.substr(0, maxIdLength) + ELLIPSIS; + classesText = ""; + } else if (classesText.length > maxClassLength) { + classesText = classesText.substr(0, maxClassLength) + ELLIPSIS; + } + + tagLabel.textContent = tagText; + idLabel.textContent = idText; + classesLabel.textContent = classesText; + pseudosLabel.textContent = node.pseudoClassLocks.join(""); + + let fragment = this.doc.createDocumentFragment(); + fragment.appendChild(tagLabel); + fragment.appendChild(idLabel); + fragment.appendChild(classesLabel); + fragment.appendChild(pseudosLabel); + + return fragment; + }, + + /** + * Generic event handler. + * @param {DOMEvent} event. + */ + handleEvent: function (event) { + if (event.type == "click" && event.button == 0) { + this.handleClick(event); + } else if (event.type == "mouseover") { + this.handleMouseOver(event); + } else if (event.type == "mouseout") { + this.handleMouseOut(event); + } else if (event.type == "focus") { + this.handleFocus(event); + } + }, + + /** + * Focus event handler. When breadcrumbs container gets focus, + * aria-activedescendant needs to be updated to currently selected + * breadcrumb. Ensures that the focus stays on the container at all times. + * @param {DOMEvent} event. + */ + handleFocus: function (event) { + event.stopPropagation(); + + let node = this.nodeHierarchy[this.currentIndex]; + if (node) { + this.outer.setAttribute("aria-activedescendant", node.button.id); + } else { + this.outer.removeAttribute("aria-activedescendant"); + } + + this.outer.focus(); + }, + + /** + * On click navigate to the correct node. + * @param {DOMEvent} event. + */ + handleClick: function (event) { + let target = event.originalTarget; + if (target.tagName == "button") { + target.onBreadcrumbsClick(); + } + }, + + /** + * On mouse over, highlight the corresponding content DOM Node. + * @param {DOMEvent} event. + */ + handleMouseOver: function (event) { + let target = event.originalTarget; + if (target.tagName == "button") { + target.onBreadcrumbsHover(); + } + }, + + /** + * On mouse out, make sure to unhighlight. + * @param {DOMEvent} event. + */ + handleMouseOut: function (event) { + this.inspector.toolbox.highlighterUtils.unhighlight(); + }, + + /** + * Handle a keyboard shortcut supported by the breadcrumbs widget. + * + * @param {String} name + * Name of the keyboard shortcut received. + * @param {DOMEvent} event + * Original event that triggered the shortcut. + */ + handleShortcut: function (name, event) { + if (!this.selection.isElementNode()) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + this.keyPromise = (this.keyPromise || promise.resolve(null)).then(() => { + let currentnode; + if (name === "Left" && this.currentIndex != 0) { + currentnode = this.nodeHierarchy[this.currentIndex - 1]; + } else if (name === "Right" && this.currentIndex < this.nodeHierarchy.length - 1) { + currentnode = this.nodeHierarchy[this.currentIndex + 1]; + } else { + return null; + } + + this.outer.setAttribute("aria-activedescendant", currentnode.button.id); + return this.selection.setNodeFront(currentnode.node, "breadcrumbs"); + }); + }, + + /** + * Remove nodes and clean up. + */ + destroy: function () { + this.selection.off("new-node-front", this.update); + this.selection.off("pseudoclass", this.updateSelectors); + this.selection.off("attribute-changed", this.updateSelectors); + this.inspector.off("markupmutation", this.update); + + this.container.removeEventListener("click", this, true); + this.container.removeEventListener("mouseover", this, true); + this.container.removeEventListener("mouseout", this, true); + this.container.removeEventListener("focus", this, true); + this.shortcuts.destroy(); + + this.empty(); + + this.arrowScrollBox.off("overflow", this.scroll); + this.arrowScrollBox.destroy(); + this.arrowScrollBox = null; + this.outer = null; + this.container = null; + this.nodeHierarchy = null; + + this.isDestroyed = true; + }, + + /** + * Empty the breadcrumbs container. + */ + empty: function () { + while (this.container.hasChildNodes()) { + this.container.firstChild.remove(); + } + }, + + /** + * Set which button represent the selected node. + * @param {Number} index Index of the displayed-button to select. + */ + setCursor: function (index) { + // Unselect the previously selected button + if (this.currentIndex > -1 + && this.currentIndex < this.nodeHierarchy.length) { + this.nodeHierarchy[this.currentIndex].button.removeAttribute("checked"); + } + if (index > -1) { + this.nodeHierarchy[index].button.setAttribute("checked", "true"); + } else { + // Unset active active descendant when all buttons are unselected. + this.outer.removeAttribute("aria-activedescendant"); + } + this.currentIndex = index; + }, + + /** + * Get the index of the node in the cache. + * @param {NodeFront} node. + * @returns {Number} The index for this node or -1 if not found. + */ + indexOf: function (node) { + for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) { + if (this.nodeHierarchy[i].node === node) { + return i; + } + } + return -1; + }, + + /** + * Remove all the buttons and their references in the cache after a given + * index. + * @param {Number} index. + */ + cutAfter: function (index) { + while (this.nodeHierarchy.length > (index + 1)) { + let toRemove = this.nodeHierarchy.pop(); + this.container.removeChild(toRemove.button); + } + }, + + /** + * Build a button representing the node. + * @param {NodeFront} node The node from the page. + * @return {DOMNode} The <button> for this node. + */ + buildButton: function (node) { + let button = this.doc.createElementNS(NS_XHTML, "button"); + button.appendChild(this.prettyPrintNodeAsXHTML(node)); + button.className = "breadcrumbs-widget-item"; + button.id = "breadcrumbs-widget-item-" + this.breadcrumbsWidgetItemId++; + + button.setAttribute("tabindex", "-1"); + button.setAttribute("title", this.prettyPrintNodeAsText(node)); + + button.onclick = () => { + button.focus(); + }; + + button.onBreadcrumbsClick = () => { + this.selection.setNodeFront(node, "breadcrumbs"); + }; + + button.onBreadcrumbsHover = () => { + this.inspector.toolbox.highlighterUtils.highlightNodeFront(node); + }; + + return button; + }, + + /** + * Connecting the end of the breadcrumbs to a node. + * @param {NodeFront} node The node to reach. + */ + expand: function (node) { + let fragment = this.doc.createDocumentFragment(); + let lastButtonInserted = null; + let originalLength = this.nodeHierarchy.length; + let stopNode = null; + if (originalLength > 0) { + stopNode = this.nodeHierarchy[originalLength - 1].node; + } + while (node && node != stopNode) { + if (node.tagName) { + let button = this.buildButton(node); + fragment.insertBefore(button, lastButtonInserted); + lastButtonInserted = button; + this.nodeHierarchy.splice(originalLength, 0, { + node, + button, + currentPrettyPrintText: this.prettyPrintNodeAsText(node) + }); + } + node = node.parentNode(); + } + this.container.appendChild(fragment, this.container.firstChild); + }, + + /** + * Find the "youngest" ancestor of a node which is already in the breadcrumbs. + * @param {NodeFront} node. + * @return {Number} Index of the ancestor in the cache, or -1 if not found. + */ + getCommonAncestor: function (node) { + while (node) { + let idx = this.indexOf(node); + if (idx > -1) { + return idx; + } + node = node.parentNode(); + } + return -1; + }, + + /** + * Ensure the selected node is visible. + */ + scroll: function () { + // FIXME bug 684352: make sure its immediate neighbors are visible too. + if (!this.isDestroyed) { + let element = this.nodeHierarchy[this.currentIndex].button; + this.arrowScrollBox.scrollToElement(element, "end"); + } + }, + + /** + * Update all button outputs. + */ + updateSelectors: function () { + if (this.isDestroyed) { + return; + } + + for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) { + let {node, button, currentPrettyPrintText} = this.nodeHierarchy[i]; + + // If the output of the node doesn't change, skip the update. + let textOutput = this.prettyPrintNodeAsText(node); + if (currentPrettyPrintText === textOutput) { + continue; + } + + // Otherwise, update the whole markup for the button. + while (button.hasChildNodes()) { + button.firstChild.remove(); + } + button.appendChild(this.prettyPrintNodeAsXHTML(node)); + button.setAttribute("title", textOutput); + + this.nodeHierarchy[i].currentPrettyPrintText = textOutput; + } + }, + + /** + * Given a list of mutation changes (passed by the markupmutation event), + * decide whether or not they are "interesting" to the current state of the + * breadcrumbs widget, i.e. at least one of them should cause part of the + * widget to be updated. + * @param {Array} mutations The mutations array. + * @return {Boolean} + */ + _hasInterestingMutations: function (mutations) { + if (!mutations || !mutations.length) { + return false; + } + + for (let {type, added, removed, target, attributeName} of mutations) { + if (type === "childList") { + // Only interested in childList mutations if the added or removed + // nodes are currently displayed. + return added.some(node => this.indexOf(node) > -1) || + removed.some(node => this.indexOf(node) > -1); + } else if (type === "attributes" && this.indexOf(target) > -1) { + // Only interested in attributes mutations if the target is + // currently displayed, and the attribute is either id or class. + return attributeName === "class" || attributeName === "id"; + } + } + + // Catch all return in case the mutations array was empty, or in case none + // of the changes iterated above were interesting. + return false; + }, + + /** + * Update the breadcrumbs display when a new node is selected. + * @param {String} reason The reason for the update, if any. + * @param {Array} mutations An array of mutations in case this was called as + * the "markupmutation" event listener. + */ + update: function (reason, mutations) { + if (this.isDestroyed) { + return; + } + + let hasInterestingMutations = this._hasInterestingMutations(mutations); + if (reason === "markupmutation" && !hasInterestingMutations) { + return; + } + + if (!this.selection.isConnected()) { + // remove all the crumbs + this.cutAfter(-1); + return; + } + + // If this was an interesting deletion; then trim the breadcrumb trail + let trimmed = false; + if (reason === "markupmutation") { + for (let {type, removed} of mutations) { + if (type !== "childList") { + continue; + } + + for (let node of removed) { + let removedIndex = this.indexOf(node); + if (removedIndex > -1) { + this.cutAfter(removedIndex - 1); + trimmed = true; + } + } + } + } + + if (!this.selection.isElementNode()) { + // no selection + this.setCursor(-1); + if (trimmed) { + // Since something changed, notify the interested parties. + this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront); + } + return; + } + + let idx = this.indexOf(this.selection.nodeFront); + + // Is the node already displayed in the breadcrumbs? + // (and there are no mutations that need re-display of the crumbs) + if (idx > -1 && !hasInterestingMutations) { + // Yes. We select it. + this.setCursor(idx); + } else { + // No. Is the breadcrumbs display empty? + if (this.nodeHierarchy.length > 0) { + // No. We drop all the element that are not direct ancestors + // of the selection + let parent = this.selection.nodeFront.parentNode(); + let ancestorIdx = this.getCommonAncestor(parent); + this.cutAfter(ancestorIdx); + } + // we append the missing button between the end of the breadcrumbs display + // and the current node. + this.expand(this.selection.nodeFront); + + // we select the current node button + idx = this.indexOf(this.selection.nodeFront); + this.setCursor(idx); + } + + let doneUpdating = this.inspector.updating("breadcrumbs"); + + this.updateSelectors(); + + // Make sure the selected node and its neighbours are visible. + setTimeout(() => { + try { + this.scroll(); + this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront); + doneUpdating(); + } catch (e) { + // Only log this as an error if we haven't been destroyed in the meantime. + if (!this.isDestroyed) { + console.error(e); + } + } + }, 0); + } +}; diff --git a/devtools/client/inspector/components/box-model.js b/devtools/client/inspector/components/box-model.js new file mode 100644 index 000000000..fc36fac71 --- /dev/null +++ b/devtools/client/inspector/components/box-model.js @@ -0,0 +1,841 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {Task} = require("devtools/shared/task"); +const {InplaceEditor, editableItem} = + require("devtools/client/shared/inplace-editor"); +const {ReflowFront} = require("devtools/shared/fronts/reflow"); +const {LocalizationHelper} = require("devtools/shared/l10n"); +const {getCssProperties} = require("devtools/shared/fronts/css-properties"); + +const STRINGS_URI = "devtools/client/locales/shared.properties"; +const STRINGS_INSPECTOR = "devtools/shared/locales/styleinspector.properties"; +const SHARED_L10N = new LocalizationHelper(STRINGS_URI); +const INSPECTOR_L10N = new LocalizationHelper(STRINGS_INSPECTOR); +const NUMERIC = /^-?[\d\.]+$/; +const LONG_TEXT_ROTATE_LIMIT = 3; + +/** + * An instance of EditingSession tracks changes that have been made during the + * modification of box model values. All of these changes can be reverted by + * calling revert. The main parameter is the BoxModelView that created it. + * + * @param inspector The inspector panel. + * @param doc A DOM document that can be used to test style rules. + * @param rules An array of the style rules defined for the node being + * edited. These should be in order of priority, least + * important first. + */ +function EditingSession({inspector, doc, elementRules}) { + this._doc = doc; + this._rules = elementRules; + this._modifications = new Map(); + this._cssProperties = getCssProperties(inspector.toolbox); +} + +EditingSession.prototype = { + /** + * Gets the value of a single property from the CSS rule. + * + * @param {StyleRuleFront} rule The CSS rule. + * @param {String} property The name of the property. + * @return {String} The value. + */ + getPropertyFromRule: function (rule, property) { + // Use the parsed declarations in the StyleRuleFront object if available. + let index = this.getPropertyIndex(property, rule); + if (index !== -1) { + return rule.declarations[index].value; + } + + // Fallback to parsing the cssText locally otherwise. + let dummyStyle = this._element.style; + dummyStyle.cssText = rule.cssText; + return dummyStyle.getPropertyValue(property); + }, + + /** + * Returns the current value for a property as a string or the empty string if + * no style rules affect the property. + * + * @param property The name of the property as a string + */ + getProperty: function (property) { + // Create a hidden element for getPropertyFromRule to use + let div = this._doc.createElement("div"); + div.setAttribute("style", "display: none"); + this._doc.getElementById("sidebar-panel-computedview").appendChild(div); + this._element = this._doc.createElement("p"); + div.appendChild(this._element); + + // As the rules are in order of priority we can just iterate until we find + // the first that defines a value for the property and return that. + for (let rule of this._rules) { + let value = this.getPropertyFromRule(rule, property); + if (value !== "") { + div.remove(); + return value; + } + } + div.remove(); + return ""; + }, + + /** + * Get the index of a given css property name in a CSS rule. + * Or -1, if there are no properties in the rule yet. + * @param {String} name The property name. + * @param {StyleRuleFront} rule Optional, defaults to the element style rule. + * @return {Number} The property index in the rule. + */ + getPropertyIndex: function (name, rule = this._rules[0]) { + let elementStyleRule = this._rules[0]; + if (!elementStyleRule.declarations.length) { + return -1; + } + + return elementStyleRule.declarations.findIndex(p => p.name === name); + }, + + /** + * Sets a number of properties on the node. + * @param properties An array of properties, each is an object with name and + * value properties. If the value is "" then the property + * is removed. + * @return {Promise} Resolves when the modifications are complete. + */ + setProperties: Task.async(function* (properties) { + for (let property of properties) { + // Get a RuleModificationList or RuleRewriter helper object from the + // StyleRuleActor to make changes to CSS properties. + // Note that RuleRewriter doesn't support modifying several properties at + // once, so we do this in a sequence here. + let modifications = this._rules[0].startModifyingProperties( + this._cssProperties); + + // Remember the property so it can be reverted. + if (!this._modifications.has(property.name)) { + this._modifications.set(property.name, + this.getPropertyFromRule(this._rules[0], property.name)); + } + + // Find the index of the property to be changed, or get the next index to + // insert the new property at. + let index = this.getPropertyIndex(property.name); + if (index === -1) { + index = this._rules[0].declarations.length; + } + + if (property.value == "") { + modifications.removeProperty(index, property.name); + } else { + modifications.setProperty(index, property.name, property.value, ""); + } + + yield modifications.apply(); + } + }), + + /** + * Reverts all of the property changes made by this instance. + * @return {Promise} Resolves when all properties have been reverted. + */ + revert: Task.async(function* () { + // Revert each property that we modified previously, one by one. See + // setProperties for information about why. + for (let [property, value] of this._modifications) { + let modifications = this._rules[0].startModifyingProperties( + this._cssProperties); + + // Find the index of the property to be reverted. + let index = this.getPropertyIndex(property); + + if (value != "") { + // If the property doesn't exist anymore, insert at the beginning of the + // rule. + if (index === -1) { + index = 0; + } + modifications.setProperty(index, property, value, ""); + } else { + // If the property doesn't exist anymore, no need to remove it. It had + // not been added after all. + if (index === -1) { + continue; + } + modifications.removeProperty(index, property); + } + + yield modifications.apply(); + } + }), + + destroy: function () { + this._doc = null; + this._rules = null; + this._modifications.clear(); + } +}; + +/** + * The box model view + * @param {InspectorPanel} inspector + * An instance of the inspector-panel currently loaded in the toolbox + * @param {Document} document + * The document that will contain the box model view. + */ +function BoxModelView(inspector, document) { + this.inspector = inspector; + this.doc = document; + this.wrapper = this.doc.getElementById("boxmodel-wrapper"); + this.container = this.doc.getElementById("boxmodel-container"); + this.expander = this.doc.getElementById("boxmodel-expander"); + this.sizeLabel = this.doc.querySelector(".boxmodel-size > span"); + this.sizeHeadingLabel = this.doc.getElementById("boxmodel-element-size"); + this._geometryEditorHighlighter = null; + this._cssProperties = getCssProperties(inspector.toolbox); + + this.init(); +} + +BoxModelView.prototype = { + init: function () { + this.update = this.update.bind(this); + + this.onNewSelection = this.onNewSelection.bind(this); + this.inspector.selection.on("new-node-front", this.onNewSelection); + + this.onNewNode = this.onNewNode.bind(this); + this.inspector.sidebar.on("computedview-selected", this.onNewNode); + + this.onSidebarSelect = this.onSidebarSelect.bind(this); + this.inspector.sidebar.on("select", this.onSidebarSelect); + + this.onToggleExpander = this.onToggleExpander.bind(this); + this.expander.addEventListener("click", this.onToggleExpander); + let header = this.doc.getElementById("boxmodel-header"); + header.addEventListener("dblclick", this.onToggleExpander); + + this.onFilterComputedView = this.onFilterComputedView.bind(this); + this.inspector.on("computed-view-filtered", + this.onFilterComputedView); + + this.onPickerStarted = this.onPickerStarted.bind(this); + this.onMarkupViewLeave = this.onMarkupViewLeave.bind(this); + this.onMarkupViewNodeHover = this.onMarkupViewNodeHover.bind(this); + this.onWillNavigate = this.onWillNavigate.bind(this); + + this.initBoxModelHighlighter(); + + // Store for the different dimensions of the node. + // 'selector' refers to the element that holds the value; + // 'property' is what we are measuring; + // 'value' is the computed dimension, computed in update(). + this.map = { + position: { + selector: "#boxmodel-element-position", + property: "position", + value: undefined + }, + marginTop: { + selector: ".boxmodel-margin.boxmodel-top > span", + property: "margin-top", + value: undefined + }, + marginBottom: { + selector: ".boxmodel-margin.boxmodel-bottom > span", + property: "margin-bottom", + value: undefined + }, + marginLeft: { + selector: ".boxmodel-margin.boxmodel-left > span", + property: "margin-left", + value: undefined + }, + marginRight: { + selector: ".boxmodel-margin.boxmodel-right > span", + property: "margin-right", + value: undefined + }, + paddingTop: { + selector: ".boxmodel-padding.boxmodel-top > span", + property: "padding-top", + value: undefined + }, + paddingBottom: { + selector: ".boxmodel-padding.boxmodel-bottom > span", + property: "padding-bottom", + value: undefined + }, + paddingLeft: { + selector: ".boxmodel-padding.boxmodel-left > span", + property: "padding-left", + value: undefined + }, + paddingRight: { + selector: ".boxmodel-padding.boxmodel-right > span", + property: "padding-right", + value: undefined + }, + borderTop: { + selector: ".boxmodel-border.boxmodel-top > span", + property: "border-top-width", + value: undefined + }, + borderBottom: { + selector: ".boxmodel-border.boxmodel-bottom > span", + property: "border-bottom-width", + value: undefined + }, + borderLeft: { + selector: ".boxmodel-border.boxmodel-left > span", + property: "border-left-width", + value: undefined + }, + borderRight: { + selector: ".boxmodel-border.boxmodel-right > span", + property: "border-right-width", + value: undefined + } + }; + + // Make each element the dimensions editable + for (let i in this.map) { + if (i == "position") { + continue; + } + + let dimension = this.map[i]; + editableItem({ + element: this.doc.querySelector(dimension.selector) + }, (element, event) => { + this.initEditor(element, event, dimension); + }); + } + + this.onNewNode(); + + let nodeGeometry = this.doc.getElementById("layout-geometry-editor"); + this.onGeometryButtonClick = this.onGeometryButtonClick.bind(this); + nodeGeometry.addEventListener("click", this.onGeometryButtonClick); + }, + + initBoxModelHighlighter: function () { + let highlightElts = this.doc.querySelectorAll("#boxmodel-container *[title]"); + this.onHighlightMouseOver = this.onHighlightMouseOver.bind(this); + this.onHighlightMouseOut = this.onHighlightMouseOut.bind(this); + + for (let element of highlightElts) { + element.addEventListener("mouseover", this.onHighlightMouseOver, true); + element.addEventListener("mouseout", this.onHighlightMouseOut, true); + } + }, + + /** + * Start listening to reflows in the current tab. + */ + trackReflows: function () { + if (!this.reflowFront) { + let { target } = this.inspector; + if (target.form.reflowActor) { + this.reflowFront = ReflowFront(target.client, + target.form); + } else { + return; + } + } + + this.reflowFront.on("reflows", this.update); + this.reflowFront.start(); + }, + + /** + * Stop listening to reflows in the current tab. + */ + untrackReflows: function () { + if (!this.reflowFront) { + return; + } + + this.reflowFront.off("reflows", this.update); + this.reflowFront.stop(); + }, + + /** + * Called when the user clicks on one of the editable values in the box model view + */ + initEditor: function (element, event, dimension) { + let { property } = dimension; + let session = new EditingSession(this); + let initialValue = session.getProperty(property); + + let editor = new InplaceEditor({ + element: element, + initial: initialValue, + contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE, + property: { + name: dimension.property + }, + start: self => { + self.elt.parentNode.classList.add("boxmodel-editing"); + }, + change: value => { + if (NUMERIC.test(value)) { + value += "px"; + } + + let properties = [ + { name: property, value: value } + ]; + + if (property.substring(0, 7) == "border-") { + let bprop = property.substring(0, property.length - 5) + "style"; + let style = session.getProperty(bprop); + if (!style || style == "none" || style == "hidden") { + properties.push({ name: bprop, value: "solid" }); + } + } + + session.setProperties(properties).catch(e => console.error(e)); + }, + done: (value, commit) => { + editor.elt.parentNode.classList.remove("boxmodel-editing"); + if (!commit) { + session.revert().then(() => { + session.destroy(); + }, e => console.error(e)); + } + }, + contextMenu: this.inspector.onTextBoxContextMenu, + cssProperties: this._cssProperties + }, event); + }, + + /** + * Is the BoxModelView visible in the sidebar. + * @return {Boolean} + */ + isViewVisible: function () { + return this.inspector && + this.inspector.sidebar.getCurrentTabID() == "computedview"; + }, + + /** + * Is the BoxModelView visible in the sidebar and is the current node valid to + * be displayed in the view. + * @return {Boolean} + */ + isViewVisibleAndNodeValid: function () { + return this.isViewVisible() && + this.inspector.selection.isConnected() && + this.inspector.selection.isElementNode(); + }, + + /** + * Destroy the nodes. Remove listeners. + */ + destroy: function () { + let highlightElts = this.doc.querySelectorAll("#boxmodel-container *[title]"); + + for (let element of highlightElts) { + element.removeEventListener("mouseover", this.onHighlightMouseOver, true); + element.removeEventListener("mouseout", this.onHighlightMouseOut, true); + } + + this.expander.removeEventListener("click", this.onToggleExpander); + let header = this.doc.getElementById("boxmodel-header"); + header.removeEventListener("dblclick", this.onToggleExpander); + + let nodeGeometry = this.doc.getElementById("layout-geometry-editor"); + nodeGeometry.removeEventListener("click", this.onGeometryButtonClick); + + this.inspector.off("picker-started", this.onPickerStarted); + + // Inspector Panel will destroy `markup` object on "will-navigate" event, + // therefore we have to check if it's still available in case BoxModelView + // is destroyed immediately after. + if (this.inspector.markup) { + this.inspector.markup.off("leave", this.onMarkupViewLeave); + this.inspector.markup.off("node-hover", this.onMarkupViewNodeHover); + } + + this.inspector.sidebar.off("computedview-selected", this.onNewNode); + this.inspector.selection.off("new-node-front", this.onNewSelection); + this.inspector.sidebar.off("select", this.onSidebarSelect); + this.inspector.target.off("will-navigate", this.onWillNavigate); + this.inspector.off("computed-view-filtered", this.onFilterComputedView); + + this.inspector = null; + this.doc = null; + this.wrapper = null; + this.container = null; + this.expander = null; + this.sizeLabel = null; + this.sizeHeadingLabel = null; + + if (this.reflowFront) { + this.untrackReflows(); + this.reflowFront.destroy(); + this.reflowFront = null; + } + }, + + onSidebarSelect: function (e, sidebar) { + this.setActive(sidebar === "computedview"); + }, + + /** + * Selection 'new-node-front' event handler. + */ + onNewSelection: function () { + let done = this.inspector.updating("computed-view"); + this.onNewNode() + .then(() => this.hideGeometryEditor()) + .then(done, (err) => { + console.error(err); + done(); + }).catch(console.error); + }, + + /** + * @return a promise that resolves when the view has been updated + */ + onNewNode: function () { + this.setActive(this.isViewVisibleAndNodeValid()); + return this.update(); + }, + + onHighlightMouseOver: function (e) { + let region = e.target.getAttribute("data-box"); + if (!region) { + return; + } + + this.showBoxModel({ + region, + showOnly: region, + onlyRegionArea: true + }); + }, + + onHighlightMouseOut: function () { + this.hideBoxModel(); + }, + + onGeometryButtonClick: function ({target}) { + if (target.hasAttribute("checked")) { + target.removeAttribute("checked"); + this.hideGeometryEditor(); + } else { + target.setAttribute("checked", "true"); + this.showGeometryEditor(); + } + }, + + onPickerStarted: function () { + this.hideGeometryEditor(); + }, + + onToggleExpander: function () { + let isOpen = this.expander.hasAttribute("open"); + + if (isOpen) { + this.container.hidden = true; + this.expander.removeAttribute("open"); + } else { + this.container.hidden = false; + this.expander.setAttribute("open", ""); + } + }, + + onMarkupViewLeave: function () { + this.showGeometryEditor(true); + }, + + onMarkupViewNodeHover: function () { + this.hideGeometryEditor(false); + }, + + onWillNavigate: function () { + this._geometryEditorHighlighter.release().catch(console.error); + this._geometryEditorHighlighter = null; + }, + + /** + * Event handler that responds to the computed view being filtered + * @param {String} reason + * @param {Boolean} hidden + * Whether or not to hide the box model wrapper + */ + onFilterComputedView: function (reason, hidden) { + this.wrapper.hidden = hidden; + }, + + /** + * Stop tracking reflows and hide all values when no node is selected or the + * box model view is hidden, otherwise track reflows and show values. + * @param {Boolean} isActive + */ + setActive: function (isActive) { + if (isActive === this.isActive) { + return; + } + this.isActive = isActive; + + if (isActive) { + this.trackReflows(); + } else { + this.untrackReflows(); + } + }, + + /** + * Compute the dimensions of the node and update the values in + * the inspector.xul document. + * @return a promise that will be resolved when complete. + */ + update: function () { + let lastRequest = Task.spawn((function* () { + if (!this.isViewVisibleAndNodeValid()) { + this.wrapper.hidden = true; + this.inspector.emit("boxmodel-view-updated"); + return null; + } + + let node = this.inspector.selection.nodeFront; + let layout = yield this.inspector.pageStyle.getLayout(node, { + autoMargins: this.isActive + }); + let styleEntries = yield this.inspector.pageStyle.getApplied(node, {}); + + yield this.updateGeometryButton(); + + // If a subsequent request has been made, wait for that one instead. + if (this._lastRequest != lastRequest) { + return this._lastRequest; + } + + this._lastRequest = null; + let width = layout.width; + let height = layout.height; + let newLabel = SHARED_L10N.getFormatStr("dimensions", width, height); + + if (this.sizeHeadingLabel.textContent != newLabel) { + this.sizeHeadingLabel.textContent = newLabel; + } + + for (let i in this.map) { + let property = this.map[i].property; + if (!(property in layout)) { + // Depending on the actor version, some properties + // might be missing. + continue; + } + let parsedValue = parseFloat(layout[property]); + if (Number.isNaN(parsedValue)) { + // Not a number. We use the raw string. + // Useful for "position" for example. + this.map[i].value = layout[property]; + } else { + this.map[i].value = parsedValue; + } + } + + let margins = layout.autoMargins; + if ("top" in margins) { + this.map.marginTop.value = "auto"; + } + if ("right" in margins) { + this.map.marginRight.value = "auto"; + } + if ("bottom" in margins) { + this.map.marginBottom.value = "auto"; + } + if ("left" in margins) { + this.map.marginLeft.value = "auto"; + } + + for (let i in this.map) { + let selector = this.map[i].selector; + let span = this.doc.querySelector(selector); + this.updateSourceRuleTooltip(span, this.map[i].property, styleEntries); + if (span.textContent.length > 0 && + span.textContent == this.map[i].value) { + continue; + } + span.textContent = this.map[i].value; + this.manageOverflowingText(span); + } + + width -= this.map.borderLeft.value + this.map.borderRight.value + + this.map.paddingLeft.value + this.map.paddingRight.value; + width = parseFloat(width.toPrecision(6)); + height -= this.map.borderTop.value + this.map.borderBottom.value + + this.map.paddingTop.value + this.map.paddingBottom.value; + height = parseFloat(height.toPrecision(6)); + + let newValue = width + "\u00D7" + height; + if (this.sizeLabel.textContent != newValue) { + this.sizeLabel.textContent = newValue; + } + + this.elementRules = styleEntries.map(e => e.rule); + + this.wrapper.hidden = false; + + this.inspector.emit("boxmodel-view-updated"); + return null; + }).bind(this)).catch(console.error); + + this._lastRequest = lastRequest; + return this._lastRequest; + }, + + /** + * Update the text in the tooltip shown when hovering over a value to provide + * information about the source CSS rule that sets this value. + * @param {DOMNode} el The element that will receive the tooltip. + * @param {String} property The name of the CSS property for the tooltip. + * @param {Array} rules An array of applied rules retrieved by + * styleActor.getApplied. + */ + updateSourceRuleTooltip: function (el, property, rules) { + // Dummy element used to parse the cssText of applied rules. + let dummyEl = this.doc.createElement("div"); + + // Rules are in order of priority so iterate until we find the first that + // defines a value for the property. + let sourceRule, value; + for (let {rule} of rules) { + dummyEl.style.cssText = rule.cssText; + value = dummyEl.style.getPropertyValue(property); + if (value !== "") { + sourceRule = rule; + break; + } + } + + let title = property; + if (sourceRule && sourceRule.selectors) { + title += "\n" + sourceRule.selectors.join(", "); + } + if (sourceRule && sourceRule.parentStyleSheet) { + if (sourceRule.parentStyleSheet.href) { + title += "\n" + sourceRule.parentStyleSheet.href + ":" + sourceRule.line; + } else { + title += "\n" + INSPECTOR_L10N.getStr("rule.sourceInline") + + ":" + sourceRule.line; + } + } + + el.setAttribute("title", title); + }, + + /** + * Show the box-model highlighter on the currently selected element + * @param {Object} options Options passed to the highlighter actor + */ + showBoxModel: function (options = {}) { + let toolbox = this.inspector.toolbox; + let nodeFront = this.inspector.selection.nodeFront; + + toolbox.highlighterUtils.highlightNodeFront(nodeFront, options); + }, + + /** + * Hide the box-model highlighter on the currently selected element + */ + hideBoxModel: function () { + let toolbox = this.inspector.toolbox; + + toolbox.highlighterUtils.unhighlight(); + }, + + /** + * Show the geometry editor highlighter on the currently selected element + * @param {Boolean} [showOnlyIfActive=false] + * Indicates if the Geometry Editor should be shown only if it's active but + * hidden. + */ + showGeometryEditor: function (showOnlyIfActive = false) { + let toolbox = this.inspector.toolbox; + let nodeFront = this.inspector.selection.nodeFront; + let nodeGeometry = this.doc.getElementById("layout-geometry-editor"); + let isActive = nodeGeometry.hasAttribute("checked"); + + if (showOnlyIfActive && !isActive) { + return; + } + + if (this._geometryEditorHighlighter) { + this._geometryEditorHighlighter.show(nodeFront).catch(console.error); + return; + } + + // instantiate Geometry Editor highlighter + toolbox.highlighterUtils + .getHighlighterByType("GeometryEditorHighlighter").then(highlighter => { + highlighter.show(nodeFront).catch(console.error); + this._geometryEditorHighlighter = highlighter; + + // Hide completely the geometry editor if the picker is clicked + toolbox.on("picker-started", this.onPickerStarted); + + // Temporary hide the geometry editor + this.inspector.markup.on("leave", this.onMarkupViewLeave); + this.inspector.markup.on("node-hover", this.onMarkupViewNodeHover); + + // Release the actor on will-navigate event + this.inspector.target.once("will-navigate", this.onWillNavigate); + }); + }, + + /** + * Hide the geometry editor highlighter on the currently selected element + * @param {Boolean} [updateButton=true] + * Indicates if the Geometry Editor's button needs to be unchecked too + */ + hideGeometryEditor: function (updateButton = true) { + if (this._geometryEditorHighlighter) { + this._geometryEditorHighlighter.hide().catch(console.error); + } + + if (updateButton) { + let nodeGeometry = this.doc.getElementById("layout-geometry-editor"); + nodeGeometry.removeAttribute("checked"); + } + }, + + /** + * Update the visibility and the state of the geometry editor button, + * based on the selected node. + */ + updateGeometryButton: Task.async(function* () { + let node = this.inspector.selection.nodeFront; + let isEditable = false; + + if (node) { + isEditable = yield this.inspector.pageStyle.isPositionEditable(node); + } + + let nodeGeometry = this.doc.getElementById("layout-geometry-editor"); + nodeGeometry.style.visibility = isEditable ? "visible" : "hidden"; + }), + + manageOverflowingText: function (span) { + let classList = span.parentNode.classList; + + if (classList.contains("boxmodel-left") || + classList.contains("boxmodel-right")) { + let force = span.textContent.length > LONG_TEXT_ROTATE_LIMIT; + classList.toggle("boxmodel-rotate", force); + } + } +}; + +exports.BoxModelView = BoxModelView; diff --git a/devtools/client/inspector/components/inspector-tab-panel.css b/devtools/client/inspector/components/inspector-tab-panel.css new file mode 100644 index 000000000..e85e5daed --- /dev/null +++ b/devtools/client/inspector/components/inspector-tab-panel.css @@ -0,0 +1,15 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +.devtools-inspector-tab-frame { + border: none; + height: 100%; + width: 100%; +} + +.devtools-inspector-tab-panel { + width: 100%; + height: 100%; +} diff --git a/devtools/client/inspector/components/inspector-tab-panel.js b/devtools/client/inspector/components/inspector-tab-panel.js new file mode 100644 index 000000000..68db7781e --- /dev/null +++ b/devtools/client/inspector/components/inspector-tab-panel.js @@ -0,0 +1,67 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { DOM, createClass, PropTypes } = require("devtools/client/shared/vendor/react"); + +// Shortcuts +const { div } = DOM; + +/** + * Helper panel component that is using an existing DOM node + * as the content. It's used by Sidebar as well as SplitBox + * components. + */ +var InspectorTabPanel = createClass({ + displayName: "InspectorTabPanel", + + propTypes: { + // ID of the node that should be rendered as the content. + id: PropTypes.string.isRequired, + // Optional prefix for panel IDs. + idPrefix: PropTypes.string, + // Optional mount callback + onMount: PropTypes.func, + }, + + getDefaultProps: function () { + return { + idPrefix: "", + }; + }, + + componentDidMount: function () { + let doc = this.refs.content.ownerDocument; + let panel = doc.getElementById(this.props.idPrefix + this.props.id); + + // Append existing DOM node into panel's content. + this.refs.content.appendChild(panel); + + if (this.props.onMount) { + this.props.onMount(this.refs.content, this.props); + } + }, + + componentWillUnmount: function () { + let doc = this.refs.content.ownerDocument; + let panels = doc.getElementById("tabpanels"); + + // Move panel's content node back into list of tab panels. + panels.appendChild(this.refs.content.firstChild); + }, + + render: function () { + return ( + div({ + ref: "content", + className: "devtools-inspector-tab-panel", + }) + ); + } +}); + +module.exports = InspectorTabPanel; diff --git a/devtools/client/inspector/components/moz.build b/devtools/client/inspector/components/moz.build new file mode 100644 index 000000000..5e4dd40ed --- /dev/null +++ b/devtools/client/inspector/components/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DevToolsModules( + 'box-model.js', + 'inspector-tab-panel.css', + 'inspector-tab-panel.js', +) + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/devtools/client/inspector/components/test/.eslintrc.js b/devtools/client/inspector/components/test/.eslintrc.js new file mode 100644 index 000000000..698ae9181 --- /dev/null +++ b/devtools/client/inspector/components/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../../.eslintrc.mochitests.js" +}; diff --git a/devtools/client/inspector/components/test/browser.ini b/devtools/client/inspector/components/test/browser.ini new file mode 100644 index 000000000..42eb352d6 --- /dev/null +++ b/devtools/client/inspector/components/test/browser.ini @@ -0,0 +1,29 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + doc_boxmodel_iframe1.html + doc_boxmodel_iframe2.html + head.js + !/devtools/client/commandline/test/helpers.js + !/devtools/client/framework/test/shared-head.js + !/devtools/client/inspector/test/head.js + !/devtools/client/inspector/test/shared-head.js + !/devtools/client/shared/test/test-actor.js + !/devtools/client/shared/test/test-actor-registry.js + +[browser_boxmodel.js] +[browser_boxmodel_editablemodel.js] +# [browser_boxmodel_editablemodel_allproperties.js] +# Disabled for too many intermittent failures (bug 1009322) +[browser_boxmodel_editablemodel_bluronclick.js] +[browser_boxmodel_editablemodel_border.js] +[browser_boxmodel_editablemodel_stylerules.js] +[browser_boxmodel_guides.js] +[browser_boxmodel_rotate-labels-on-sides.js] +[browser_boxmodel_sync.js] +[browser_boxmodel_tooltips.js] +[browser_boxmodel_update-after-navigation.js] +[browser_boxmodel_update-after-reload.js] +# [browser_boxmodel_update-in-iframes.js] +# Bug 1020038 boxmodel-view updates for iframe elements changes diff --git a/devtools/client/inspector/components/test/browser_boxmodel.js b/devtools/client/inspector/components/test/browser_boxmodel.js new file mode 100644 index 000000000..f8b87f421 --- /dev/null +++ b/devtools/client/inspector/components/test/browser_boxmodel.js @@ -0,0 +1,168 @@ +/* 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 the box model displays the right values and that it updates when +// the node's style is changed + +// Expected values: +var res1 = [ + { + selector: "#boxmodel-element-size", + value: "160" + "\u00D7" + "160.117" + }, + { + selector: ".boxmodel-size > span", + value: "100" + "\u00D7" + "100.117" + }, + { + selector: ".boxmodel-margin.boxmodel-top > span", + value: 30 + }, + { + selector: ".boxmodel-margin.boxmodel-left > span", + value: "auto" + }, + { + selector: ".boxmodel-margin.boxmodel-bottom > span", + value: 30 + }, + { + selector: ".boxmodel-margin.boxmodel-right > span", + value: "auto" + }, + { + selector: ".boxmodel-padding.boxmodel-top > span", + value: 20 + }, + { + selector: ".boxmodel-padding.boxmodel-left > span", + value: 20 + }, + { + selector: ".boxmodel-padding.boxmodel-bottom > span", + value: 20 + }, + { + selector: ".boxmodel-padding.boxmodel-right > span", + value: 20 + }, + { + selector: ".boxmodel-border.boxmodel-top > span", + value: 10 + }, + { + selector: ".boxmodel-border.boxmodel-left > span", + value: 10 + }, + { + selector: ".boxmodel-border.boxmodel-bottom > span", + value: 10 + }, + { + selector: ".boxmodel-border.boxmodel-right > span", + value: 10 + }, +]; + +var res2 = [ + { + selector: "#boxmodel-element-size", + value: "190" + "\u00D7" + "210" + }, + { + selector: ".boxmodel-size > span", + value: "100" + "\u00D7" + "150" + }, + { + selector: ".boxmodel-margin.boxmodel-top > span", + value: 30 + }, + { + selector: ".boxmodel-margin.boxmodel-left > span", + value: "auto" + }, + { + selector: ".boxmodel-margin.boxmodel-bottom > span", + value: 30 + }, + { + selector: ".boxmodel-margin.boxmodel-right > span", + value: "auto" + }, + { + selector: ".boxmodel-padding.boxmodel-top > span", + value: 20 + }, + { + selector: ".boxmodel-padding.boxmodel-left > span", + value: 20 + }, + { + selector: ".boxmodel-padding.boxmodel-bottom > span", + value: 20 + }, + { + selector: ".boxmodel-padding.boxmodel-right > span", + value: 50 + }, + { + selector: ".boxmodel-border.boxmodel-top > span", + value: 10 + }, + { + selector: ".boxmodel-border.boxmodel-left > span", + value: 10 + }, + { + selector: ".boxmodel-border.boxmodel-bottom > span", + value: 10 + }, + { + selector: ".boxmodel-border.boxmodel-right > span", + value: 10 + }, +]; + +add_task(function* () { + let style = "div { position: absolute; top: 42px; left: 42px; " + + "height: 100.111px; width: 100px; border: 10px solid black; " + + "padding: 20px; margin: 30px auto;}"; + let html = "<style>" + style + "</style><div></div>"; + + yield addTab("data:text/html," + encodeURIComponent(html)); + let {inspector, view, testActor} = yield openBoxModelView(); + yield selectNode("div", inspector); + + yield testInitialValues(inspector, view); + yield testChangingValues(inspector, view, testActor); +}); + +function* testInitialValues(inspector, view) { + info("Test that the initial values of the box model are correct"); + let viewdoc = view.doc; + + for (let i = 0; i < res1.length; i++) { + let elt = viewdoc.querySelector(res1[i].selector); + is(elt.textContent, res1[i].value, + res1[i].selector + " has the right value."); + } +} + +function* testChangingValues(inspector, view, testActor) { + info("Test that changing the document updates the box model"); + let viewdoc = view.doc; + + let onUpdated = waitForUpdate(inspector); + yield testActor.setAttribute("div", "style", + "height:150px;padding-right:50px;"); + yield onUpdated; + + for (let i = 0; i < res2.length; i++) { + let elt = viewdoc.querySelector(res2[i].selector); + is(elt.textContent, res2[i].value, + res2[i].selector + " has the right value after style update."); + } +} diff --git a/devtools/client/inspector/components/test/browser_boxmodel_editablemodel.js b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel.js new file mode 100644 index 000000000..5c32c2029 --- /dev/null +++ b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel.js @@ -0,0 +1,194 @@ +/* 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 editing the box-model values works as expected and test various +// key bindings + +const TEST_URI = "<style>" + + "div { margin: 10px; padding: 3px }" + + "#div1 { margin-top: 5px }" + + "#div2 { border-bottom: 1em solid black; }" + + "#div3 { padding: 2em; }" + + "#div4 { margin: 1px; }" + + "</style>" + + "<div id='div1'></div><div id='div2'></div>" + + "<div id='div3'></div><div id='div4'></div>"; + +add_task(function* () { + yield addTab("data:text/html," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = yield openBoxModelView(); + + yield testEditingMargins(inspector, view, testActor); + yield testKeyBindings(inspector, view, testActor); + yield testEscapeToUndo(inspector, view, testActor); + yield testDeletingValue(inspector, view, testActor); + yield testRefocusingOnClick(inspector, view, testActor); +}); + +function* testEditingMargins(inspector, view, testActor) { + info("Test that editing margin dynamically updates the document, pressing " + + "escape cancels the changes"); + + is((yield getStyle(testActor, "#div1", "margin-top")), "", + "Should be no margin-top on the element."); + yield selectNode("#div1", inspector); + + let span = view.doc.querySelector(".boxmodel-margin.boxmodel-top > span"); + is(span.textContent, 5, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView); + let editor = view.doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("3", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is((yield getStyle(testActor, "#div1", "margin-top")), "3px", + "Should have updated the margin."); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is((yield getStyle(testActor, "#div1", "margin-top")), "", + "Should be no margin-top on the element."); + is(span.textContent, 5, "Should have the right value in the box model."); +} + +function* testKeyBindings(inspector, view, testActor) { + info("Test that arrow keys work correctly and pressing enter commits the " + + "changes"); + + is((yield getStyle(testActor, "#div1", "margin-left")), "", + "Should be no margin-top on the element."); + yield selectNode("#div1", inspector); + + let span = view.doc.querySelector(".boxmodel-margin.boxmodel-left > span"); + is(span.textContent, 10, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView); + let editor = view.doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "10px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_UP", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is(editor.value, "11px", "Should have the right value in the editor."); + is((yield getStyle(testActor, "#div1", "margin-left")), "11px", + "Should have updated the margin."); + + EventUtils.synthesizeKey("VK_DOWN", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is(editor.value, "10px", "Should have the right value in the editor."); + is((yield getStyle(testActor, "#div1", "margin-left")), "10px", + "Should have updated the margin."); + + EventUtils.synthesizeKey("VK_UP", { shiftKey: true }, view.doc.defaultView); + yield waitForUpdate(inspector); + + is(editor.value, "20px", "Should have the right value in the editor."); + is((yield getStyle(testActor, "#div1", "margin-left")), "20px", + "Should have updated the margin."); + EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView); + + is((yield getStyle(testActor, "#div1", "margin-left")), "20px", + "Should be the right margin-top on the element."); + is(span.textContent, 20, "Should have the right value in the box model."); +} + +function* testEscapeToUndo(inspector, view, testActor) { + info("Test that deleting the value removes the property but escape undoes " + + "that"); + + is((yield getStyle(testActor, "#div1", "margin-left")), "20px", + "Should be the right margin-top on the element."); + yield selectNode("#div1", inspector); + + let span = view.doc.querySelector(".boxmodel-margin.boxmodel-left > span"); + is(span.textContent, 20, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView); + let editor = view.doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "20px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_DELETE", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is(editor.value, "", "Should have the right value in the editor."); + is((yield getStyle(testActor, "#div1", "margin-left")), "", + "Should have updated the margin."); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is((yield getStyle(testActor, "#div1", "margin-left")), "20px", + "Should be the right margin-top on the element."); + is(span.textContent, 20, "Should have the right value in the box model."); +} + +function* testDeletingValue(inspector, view, testActor) { + info("Test that deleting the value removes the property"); + + yield setStyle(testActor, "#div1", "marginRight", "15px"); + yield waitForUpdate(inspector); + + yield selectNode("#div1", inspector); + + let span = view.doc.querySelector(".boxmodel-margin.boxmodel-right > span"); + is(span.textContent, 15, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView); + let editor = view.doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "15px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_DELETE", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is(editor.value, "", "Should have the right value in the editor."); + is((yield getStyle(testActor, "#div1", "margin-right")), "", + "Should have updated the margin."); + + EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView); + + is((yield getStyle(testActor, "#div1", "margin-right")), "", + "Should be the right margin-top on the element."); + is(span.textContent, 10, "Should have the right value in the box model."); +} + +function* testRefocusingOnClick(inspector, view, testActor) { + info("Test that clicking in the editor input does not remove focus"); + + yield selectNode("#div4", inspector); + + let span = view.doc.querySelector(".boxmodel-margin.boxmodel-top > span"); + is(span.textContent, 1, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView); + let editor = view.doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + + info("Click in the already opened editor input"); + EventUtils.synthesizeMouseAtCenter(editor, {}, view.doc.defaultView); + is(editor, view.doc.activeElement, + "Inplace editor input should still have focus."); + + info("Check the input can still be used as expected"); + EventUtils.synthesizeKey("VK_UP", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is(editor.value, "2px", "Should have the right value in the editor."); + is((yield getStyle(testActor, "#div4", "margin-top")), "2px", + "Should have updated the margin."); + EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView); + + is((yield getStyle(testActor, "#div4", "margin-top")), "2px", + "Should be the right margin-top on the element."); + is(span.textContent, 2, "Should have the right value in the box model."); +} diff --git a/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_allproperties.js b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_allproperties.js new file mode 100644 index 000000000..464a7b6c5 --- /dev/null +++ b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_allproperties.js @@ -0,0 +1,146 @@ +/* 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 editing box model values when all values are set + +const TEST_URI = "<style>" + + "div { margin: 10px; padding: 3px }" + + "#div1 { margin-top: 5px }" + + "#div2 { border-bottom: 1em solid black; }" + + "#div3 { padding: 2em; }" + + "</style>" + + "<div id='div1'></div><div id='div2'></div><div id='div3'></div>"; + +add_task(function* () { + yield addTab("data:text/html," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = yield openBoxModelView(); + + yield testEditing(inspector, view, testActor); + yield testEditingAndCanceling(inspector, view, testActor); + yield testDeleting(inspector, view, testActor); + yield testDeletingAndCanceling(inspector, view, testActor); +}); + +function* testEditing(inspector, view, testActor) { + info("When all properties are set on the node editing one should work"); + + yield setStyle(testActor, "#div1", "padding", "5px"); + yield waitForUpdate(inspector); + + yield selectNode("#div1", inspector); + + let span = view.doc.querySelector(".boxmodel-padding.boxmodel-bottom > span"); + is(span.textContent, 5, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView); + let editor = view.doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("7", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is(editor.value, "7", "Should have the right value in the editor."); + is((yield getStyle(testActor, "#div1", "padding-bottom")), "7px", + "Should have updated the padding"); + + EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView); + + is((yield getStyle(testActor, "#div1", "padding-bottom")), "7px", + "Should be the right padding."); + is(span.textContent, 7, "Should have the right value in the box model."); +} + +function* testEditingAndCanceling(inspector, view, testActor) { + info("When all properties are set on the node editing one and then " + + "cancelling with ESCAPE should work"); + + yield setStyle(testActor, "#div1", "padding", "5px"); + yield waitForUpdate(inspector); + + yield selectNode("#div1", inspector); + + let span = view.doc.querySelector(".boxmodel-padding.boxmodel-left > span"); + is(span.textContent, 5, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView); + let editor = view.doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("8", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is(editor.value, "8", "Should have the right value in the editor."); + is((yield getStyle(testActor, "#div1", "padding-left")), "8px", + "Should have updated the padding"); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is((yield getStyle(testActor, "#div1", "padding-left")), "5px", + "Should be the right padding."); + is(span.textContent, 5, "Should have the right value in the box model."); +} + +function* testDeleting(inspector, view, testActor) { + info("When all properties are set on the node deleting one should work"); + + yield selectNode("#div1", inspector); + + let span = view.doc.querySelector(".boxmodel-padding.boxmodel-left > span"); + is(span.textContent, 5, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView); + let editor = view.doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_DELETE", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is(editor.value, "", "Should have the right value in the editor."); + is((yield getStyle(testActor, "#div1", "padding-left")), "", + "Should have updated the padding"); + + EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView); + + is((yield getStyle(testActor, "#div1", "padding-left")), "", + "Should be the right padding."); + is(span.textContent, 3, "Should have the right value in the box model."); +} + +function* testDeletingAndCanceling(inspector, view, testActor) { + info("When all properties are set on the node deleting one then cancelling " + + "should work"); + + yield setStyle(testActor, "#div1", "padding", "5px"); + yield waitForUpdate(inspector); + + yield selectNode("#div1", inspector); + + let span = view.doc.querySelector(".boxmodel-padding.boxmodel-left > span"); + is(span.textContent, 5, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView); + let editor = view.doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_DELETE", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is(editor.value, "", "Should have the right value in the editor."); + is((yield getStyle(testActor, "#div1", "padding-left")), "", + "Should have updated the padding"); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is((yield getStyle(testActor, "#div1", "padding-left")), "5px", + "Should be the right padding."); + is(span.textContent, 5, "Should have the right value in the box model."); +} diff --git a/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_bluronclick.js b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_bluronclick.js new file mode 100644 index 000000000..9e65e4dc7 --- /dev/null +++ b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_bluronclick.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 inplace editors can be blurred by clicking outside of the editor. + +const TEST_URI = + `<style> + #div1 { + margin: 10px; + padding: 3px; + } + </style> + <div id="div1"></div>`; + +add_task(function* () { + // Make sure the toolbox is tall enough to have empty space below the + // boxmodel-container. + yield pushPref("devtools.toolbox.footer.height", 500); + + yield addTab("data:text/html," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openBoxModelView(); + + yield selectNode("#div1", inspector); + yield testClickingOutsideEditor(view); + yield testClickingBelowContainer(view); +}); + +function* testClickingOutsideEditor(view) { + info("Test that clicking outside the editor blurs it"); + let span = view.doc.querySelector(".boxmodel-margin.boxmodel-top > span"); + is(span.textContent, 10, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView); + let editor = view.doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + + info("Click next to the opened editor input."); + let onBlur = once(editor, "blur"); + let rect = editor.getBoundingClientRect(); + EventUtils.synthesizeMouse(editor, rect.width + 10, rect.height / 2, {}, + view.doc.defaultView); + yield onBlur; + + is(view.doc.querySelector(".styleinspector-propertyeditor"), null, + "Inplace editor has been removed."); +} + +function* testClickingBelowContainer(view) { + info("Test that clicking below the box-model container blurs it"); + let span = view.doc.querySelector(".boxmodel-margin.boxmodel-top > span"); + is(span.textContent, 10, "Should have the right value in the box model."); + + info("Test that clicking below the boxmodel-container blurs the opened editor"); + EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView); + let editor = view.doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + + let onBlur = once(editor, "blur"); + let container = view.doc.querySelector("#boxmodel-container"); + // Using getBoxQuads here because getBoundingClientRect (and therefore synthesizeMouse) + // use an erroneous height of ~50px for the boxmodel-container. + let bounds = container.getBoxQuads({relativeTo: view.doc})[0].bounds; + EventUtils.synthesizeMouseAtPoint( + bounds.left + 10, + bounds.top + bounds.height + 10, + {}, view.doc.defaultView); + yield onBlur; + + is(view.doc.querySelector(".styleinspector-propertyeditor"), null, + "Inplace editor has been removed."); +} diff --git a/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_border.js b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_border.js new file mode 100644 index 000000000..6e9c04b14 --- /dev/null +++ b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_border.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 editing the border value in the box model applies the border style + +const TEST_URI = "<style>" + + "div { margin: 10px; padding: 3px }" + + "#div1 { margin-top: 5px }" + + "#div2 { border-bottom: 1em solid black; }" + + "#div3 { padding: 2em; }" + + "</style>" + + "<div id='div1'></div><div id='div2'></div><div id='div3'></div>"; + +add_task(function* () { + yield addTab("data:text/html," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = yield openBoxModelView(); + + is((yield getStyle(testActor, "#div1", "border-top-width")), "", + "Should have the right border"); + is((yield getStyle(testActor, "#div1", "border-top-style")), "", + "Should have the right border"); + yield selectNode("#div1", inspector); + + let span = view.doc.querySelector(".boxmodel-border.boxmodel-top > span"); + is(span.textContent, 0, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView); + let editor = view.doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "0", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("1", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is(editor.value, "1", "Should have the right value in the editor."); + is((yield getStyle(testActor, "#div1", "border-top-width")), "1px", + "Should have the right border"); + is((yield getStyle(testActor, "#div1", "border-top-style")), "solid", + "Should have the right border"); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is((yield getStyle(testActor, "#div1", "border-top-width")), "", + "Should be the right padding."); + is((yield getStyle(testActor, "#div1", "border-top-style")), "", + "Should have the right border"); + is(span.textContent, 0, "Should have the right value in the box model."); +}); diff --git a/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_stylerules.js b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_stylerules.js new file mode 100644 index 000000000..43346fa15 --- /dev/null +++ b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_stylerules.js @@ -0,0 +1,113 @@ +/* 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 units are displayed correctly when editing values in the box model +// and that values are retrieved and parsed correctly from the back-end + +const TEST_URI = "<style>" + + "div { margin: 10px; padding: 3px }" + + "#div1 { margin-top: 5px }" + + "#div2 { border-bottom: 1em solid black; }" + + "#div3 { padding: 2em; }" + + "</style>" + + "<div id='div1'></div><div id='div2'></div><div id='div3'></div>"; + +add_task(function* () { + yield addTab("data:text/html," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = yield openBoxModelView(); + + yield testUnits(inspector, view, testActor); + yield testValueComesFromStyleRule(inspector, view, testActor); + yield testShorthandsAreParsed(inspector, view, testActor); +}); + +function* testUnits(inspector, view, testActor) { + info("Test that entering units works"); + + is((yield getStyle(testActor, "#div1", "padding-top")), "", + "Should have the right padding"); + yield selectNode("#div1", inspector); + + let span = view.doc.querySelector(".boxmodel-padding.boxmodel-top > span"); + is(span.textContent, 3, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView); + let editor = view.doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "3px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("1", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + EventUtils.synthesizeKey("e", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is((yield getStyle(testActor, "#div1", "padding-top")), "", + "An invalid value is handled cleanly"); + + EventUtils.synthesizeKey("m", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is(editor.value, "1em", "Should have the right value in the editor."); + is((yield getStyle(testActor, "#div1", "padding-top")), + "1em", "Should have updated the padding."); + + EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView); + + is((yield getStyle(testActor, "#div1", "padding-top")), "1em", + "Should be the right padding."); + is(span.textContent, 16, "Should have the right value in the box model."); +} + +function* testValueComesFromStyleRule(inspector, view, testActor) { + info("Test that we pick up the value from a higher style rule"); + + is((yield getStyle(testActor, "#div2", "border-bottom-width")), "", + "Should have the right border-bottom-width"); + yield selectNode("#div2", inspector); + + let span = view.doc.querySelector(".boxmodel-border.boxmodel-bottom > span"); + is(span.textContent, 16, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView); + let editor = view.doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "1em", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("0", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + + is(editor.value, "0", "Should have the right value in the editor."); + is((yield getStyle(testActor, "#div2", "border-bottom-width")), "0px", + "Should have updated the border."); + + EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView); + + is((yield getStyle(testActor, "#div2", "border-bottom-width")), "0px", + "Should be the right border-bottom-width."); + is(span.textContent, 0, "Should have the right value in the box model."); +} + +function* testShorthandsAreParsed(inspector, view, testActor) { + info("Test that shorthand properties are parsed correctly"); + + is((yield getStyle(testActor, "#div3", "padding-right")), "", + "Should have the right padding"); + yield selectNode("#div3", inspector); + + let span = view.doc.querySelector(".boxmodel-padding.boxmodel-right > span"); + is(span.textContent, 32, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView); + let editor = view.doc.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "2em", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView); + + is((yield getStyle(testActor, "#div3", "padding-right")), "", + "Should be the right padding."); + is(span.textContent, 32, "Should have the right value in the box model."); +} diff --git a/devtools/client/inspector/components/test/browser_boxmodel_guides.js b/devtools/client/inspector/components/test/browser_boxmodel_guides.js new file mode 100644 index 000000000..612d9ace6 --- /dev/null +++ b/devtools/client/inspector/components/test/browser_boxmodel_guides.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that hovering over regions in the box-model shows the highlighter with +// the right options. +// Tests that actually check the highlighter is displayed and correct are in the +// devtools/inspector/test folder. This test only cares about checking that the +// box model view does call the highlighter, and it does so by mocking it. + +const STYLE = "div { position: absolute; top: 50px; left: 50px; " + + "height: 10px; width: 10px; border: 10px solid black; " + + "padding: 10px; margin: 10px;}"; +const HTML = "<style>" + STYLE + "</style><div></div>"; +const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML); + +var highlightedNodeFront, highlighterOptions; + +add_task(function* () { + yield addTab(TEST_URL); + let {toolbox, inspector, view} = yield openBoxModelView(); + yield selectNode("div", inspector); + + // Mock the highlighter by replacing the showBoxModel method. + toolbox.highlighter.showBoxModel = function (nodeFront, options) { + highlightedNodeFront = nodeFront; + highlighterOptions = options; + }; + + let elt = view.doc.getElementById("boxmodel-margins"); + yield testGuideOnLayoutHover(elt, "margin", inspector, view); + + elt = view.doc.getElementById("boxmodel-borders"); + yield testGuideOnLayoutHover(elt, "border", inspector, view); + + elt = view.doc.getElementById("boxmodel-padding"); + yield testGuideOnLayoutHover(elt, "padding", inspector, view); + + elt = view.doc.getElementById("boxmodel-content"); + yield testGuideOnLayoutHover(elt, "content", inspector, view); +}); + +function* testGuideOnLayoutHover(elt, expectedRegion, inspector) { + info("Synthesizing mouseover on the boxmodel-view"); + EventUtils.synthesizeMouse(elt, 2, 2, {type: "mouseover"}, + elt.ownerDocument.defaultView); + + info("Waiting for the node-highlight event from the toolbox"); + yield inspector.toolbox.once("node-highlight"); + + is(highlightedNodeFront, inspector.selection.nodeFront, + "The right nodeFront was highlighted"); + is(highlighterOptions.region, expectedRegion, + "Region " + expectedRegion + " was highlighted"); +} diff --git a/devtools/client/inspector/components/test/browser_boxmodel_rotate-labels-on-sides.js b/devtools/client/inspector/components/test/browser_boxmodel_rotate-labels-on-sides.js new file mode 100644 index 000000000..954cd298b --- /dev/null +++ b/devtools/client/inspector/components/test/browser_boxmodel_rotate-labels-on-sides.js @@ -0,0 +1,49 @@ +/* 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 longer values are rotated on the side + +const res1 = [ + {selector: ".boxmodel-margin.boxmodel-top > span", value: 30}, + {selector: ".boxmodel-margin.boxmodel-left > span", value: "auto"}, + {selector: ".boxmodel-margin.boxmodel-bottom > span", value: 30}, + {selector: ".boxmodel-margin.boxmodel-right > span", value: "auto"}, + {selector: ".boxmodel-padding.boxmodel-top > span", value: 20}, + {selector: ".boxmodel-padding.boxmodel-left > span", value: 2000000}, + {selector: ".boxmodel-padding.boxmodel-bottom > span", value: 20}, + {selector: ".boxmodel-padding.boxmodel-right > span", value: 20}, + {selector: ".boxmodel-border.boxmodel-top > span", value: 10}, + {selector: ".boxmodel-border.boxmodel-left > span", value: 10}, + {selector: ".boxmodel-border.boxmodel-bottom > span", value: 10}, + {selector: ".boxmodel-border.boxmodel-right > span", value: 10}, +]; + +const TEST_URI = encodeURIComponent([ + "<style>", + "div { border:10px solid black; padding: 20px 20px 20px 2000000px; " + + "margin: 30px auto; }", + "</style>", + "<div></div>" +].join("")); +const LONG_TEXT_ROTATE_LIMIT = 3; + +add_task(function* () { + yield addTab("data:text/html," + TEST_URI); + let {inspector, view} = yield openBoxModelView(); + yield selectNode("div", inspector); + + for (let i = 0; i < res1.length; i++) { + let elt = view.doc.querySelector(res1[i].selector); + let isLong = elt.textContent.length > LONG_TEXT_ROTATE_LIMIT; + let classList = elt.parentNode.classList; + let canBeRotated = classList.contains("boxmodel-left") || + classList.contains("boxmodel-right"); + let isRotated = classList.contains("boxmodel-rotate"); + + is(canBeRotated && isLong, + isRotated, res1[i].selector + " correctly rotated."); + } +}); diff --git a/devtools/client/inspector/components/test/browser_boxmodel_sync.js b/devtools/client/inspector/components/test/browser_boxmodel_sync.js new file mode 100644 index 000000000..a896bfe06 --- /dev/null +++ b/devtools/client/inspector/components/test/browser_boxmodel_sync.js @@ -0,0 +1,44 @@ +/* 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 editing box model syncs with the rule view. + +const TEST_URI = "<p>hello</p>"; + +add_task(function* () { + yield addTab("data:text/html," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openBoxModelView(); + + info("When a property is edited, it should sync in the rule view"); + + yield selectNode("p", inspector); + + info("Modify padding-bottom in box model view"); + let span = view.doc.querySelector(".boxmodel-padding.boxmodel-bottom > span"); + EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView); + let editor = view.doc.querySelector(".styleinspector-propertyeditor"); + + EventUtils.synthesizeKey("7", {}, view.doc.defaultView); + yield waitForUpdate(inspector); + is(editor.value, "7", "Should have the right value in the editor."); + EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView); + + let onRuleViewRefreshed = once(inspector, "rule-view-refreshed"); + let onRuleViewSelected = once(inspector.sidebar, "ruleview-selected"); + info("Select the rule view and check that the property was synced there"); + let ruleView = selectRuleView(inspector); + + info("Wait for the rule view to be selected"); + yield onRuleViewSelected; + + info("Wait for the rule view to be refreshed"); + yield onRuleViewRefreshed; + ok(true, "The rule view was refreshed"); + + let ruleEditor = getRuleViewRuleEditor(ruleView, 0); + let textProp = ruleEditor.rule.textProps[0]; + is(textProp.value, "7px", "The property has the right value"); +}); diff --git a/devtools/client/inspector/components/test/browser_boxmodel_tooltips.js b/devtools/client/inspector/components/test/browser_boxmodel_tooltips.js new file mode 100644 index 000000000..b65d2446a --- /dev/null +++ b/devtools/client/inspector/components/test/browser_boxmodel_tooltips.js @@ -0,0 +1,126 @@ +/* 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 the regions in the box model view have tooltips, and that individual +// values too. Also test that values that are set from a css rule have tooltips +// referencing the rule. + +const TEST_URI = "<style>" + + "#div1 { color: red; margin: 3em; }\n" + + "#div2 { border-bottom: 1px solid black; background: red; }\n" + + "html, body, #div3 { box-sizing: border-box; padding: 0 2em; }" + + "</style>" + + "<div id='div1'></div><div id='div2'></div><div id='div3'></div>"; + +// Test data for the tooltips over individual values. +// Each entry should contain: +// - selector: The selector for the node to be selected before starting to test +// - values: An array containing objects for each of the values that are defined +// by css rules. Each entry should contain: +// - name: the name of the property that is set by the css rule +// - ruleSelector: the selector of the rule +// - styleSheetLocation: the fileName:lineNumber +const VALUES_TEST_DATA = [{ + selector: "#div1", + values: [{ + name: "margin-top", + ruleSelector: "#div1", + styleSheetLocation: "inline:1" + }, { + name: "margin-right", + ruleSelector: "#div1", + styleSheetLocation: "inline:1" + }, { + name: "margin-bottom", + ruleSelector: "#div1", + styleSheetLocation: "inline:1" + }, { + name: "margin-left", + ruleSelector: "#div1", + styleSheetLocation: "inline:1" + }] +}, { + selector: "#div2", + values: [{ + name: "border-bottom-width", + ruleSelector: "#div2", + styleSheetLocation: "inline:2" + }] +}, { + selector: "#div3", + values: [{ + name: "padding-top", + ruleSelector: "html, body, #div3", + styleSheetLocation: "inline:3" + }, { + name: "padding-right", + ruleSelector: "html, body, #div3", + styleSheetLocation: "inline:3" + }, { + name: "padding-bottom", + ruleSelector: "html, body, #div3", + styleSheetLocation: "inline:3" + }, { + name: "padding-left", + ruleSelector: "html, body, #div3", + styleSheetLocation: "inline:3" + }] +}]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openBoxModelView(); + + info("Checking the regions tooltips"); + + ok(view.doc.querySelector("#boxmodel-margins").hasAttribute("title"), + "The margin region has a tooltip"); + is(view.doc.querySelector("#boxmodel-margins").getAttribute("title"), "margin", + "The margin region has the correct tooltip content"); + + ok(view.doc.querySelector("#boxmodel-borders").hasAttribute("title"), + "The border region has a tooltip"); + is(view.doc.querySelector("#boxmodel-borders").getAttribute("title"), "border", + "The border region has the correct tooltip content"); + + ok(view.doc.querySelector("#boxmodel-padding").hasAttribute("title"), + "The padding region has a tooltip"); + is(view.doc.querySelector("#boxmodel-padding").getAttribute("title"), "padding", + "The padding region has the correct tooltip content"); + + ok(view.doc.querySelector("#boxmodel-content").hasAttribute("title"), + "The content region has a tooltip"); + is(view.doc.querySelector("#boxmodel-content").getAttribute("title"), "content", + "The content region has the correct tooltip content"); + + for (let {selector, values} of VALUES_TEST_DATA) { + info("Selecting " + selector + " and checking the values tooltips"); + yield selectNode(selector, inspector); + + info("Iterate over all values"); + for (let key in view.map) { + if (key === "position") { + continue; + } + + let name = view.map[key].property; + let expectedTooltipData = values.find(o => o.name === name); + let el = view.doc.querySelector(view.map[key].selector); + + ok(el.hasAttribute("title"), "The " + name + " value has a tooltip"); + + if (expectedTooltipData) { + info("The " + name + " value comes from a css rule"); + let expectedTooltip = name + "\n" + expectedTooltipData.ruleSelector + + "\n" + expectedTooltipData.styleSheetLocation; + is(el.getAttribute("title"), expectedTooltip, "The tooltip is correct"); + } else { + info("The " + name + " isn't set by a css rule"); + is(el.getAttribute("title"), name, "The tooltip is correct"); + } + } + } +}); diff --git a/devtools/client/inspector/components/test/browser_boxmodel_update-after-navigation.js b/devtools/client/inspector/components/test/browser_boxmodel_update-after-navigation.js new file mode 100644 index 000000000..cb5960229 --- /dev/null +++ b/devtools/client/inspector/components/test/browser_boxmodel_update-after-navigation.js @@ -0,0 +1,91 @@ +/* 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 the box model view continues to work after a page navigation and that +// it also works after going back + +const IFRAME1 = URL_ROOT + "doc_boxmodel_iframe1.html"; +const IFRAME2 = URL_ROOT + "doc_boxmodel_iframe2.html"; + +add_task(function* () { + yield addTab(IFRAME1); + let {inspector, view, testActor} = yield openBoxModelView(); + + yield testFirstPage(inspector, view, testActor); + + info("Navigate to the second page"); + yield testActor.eval(`content.location.href="${IFRAME2}"`); + yield inspector.once("markuploaded"); + + yield testSecondPage(inspector, view, testActor); + + info("Go back to the first page"); + yield testActor.eval("content.history.back();"); + yield inspector.once("markuploaded"); + + yield testBackToFirstPage(inspector, view, testActor); +}); + +function* testFirstPage(inspector, view, testActor) { + info("Test that the box model view works on the first page"); + + info("Selecting the test node"); + yield selectNode("p", inspector); + + info("Checking that the box model view shows the right value"); + let paddingElt = view.doc.querySelector(".boxmodel-padding.boxmodel-top > span"); + is(paddingElt.textContent, "50"); + + info("Listening for box model view changes and modifying the padding"); + let onUpdated = waitForUpdate(inspector); + yield setStyle(testActor, "p", "padding", "20px"); + yield onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(paddingElt.textContent, "20"); +} + +function* testSecondPage(inspector, view, testActor) { + info("Test that the box model view works on the second page"); + + info("Selecting the test node"); + yield selectNode("p", inspector); + + info("Checking that the box model view shows the right value"); + let sizeElt = view.doc.querySelector(".boxmodel-size > span"); + is(sizeElt.textContent, "100" + "\u00D7" + "100"); + + info("Listening for box model view changes and modifying the size"); + let onUpdated = waitForUpdate(inspector); + yield setStyle(testActor, "p", "width", "200px"); + yield onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(sizeElt.textContent, "200" + "\u00D7" + "100"); +} + +function* testBackToFirstPage(inspector, view, testActor) { + info("Test that the box model view works on the first page after going back"); + + info("Selecting the test node"); + yield selectNode("p", inspector); + + info("Checking that the box model view shows the right value, which is the" + + "modified value from step one because of the bfcache"); + let paddingElt = view.doc.querySelector(".boxmodel-padding.boxmodel-top > span"); + is(paddingElt.textContent, "20"); + + info("Listening for box model view changes and modifying the padding"); + let onUpdated = waitForUpdate(inspector); + yield setStyle(testActor, "p", "padding", "100px"); + yield onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(paddingElt.textContent, "100"); +} diff --git a/devtools/client/inspector/components/test/browser_boxmodel_update-after-reload.js b/devtools/client/inspector/components/test/browser_boxmodel_update-after-reload.js new file mode 100644 index 000000000..7fc09bfa3 --- /dev/null +++ b/devtools/client/inspector/components/test/browser_boxmodel_update-after-reload.js @@ -0,0 +1,40 @@ +/* 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 the box model view continues to work after the page is reloaded + +add_task(function* () { + yield addTab(URL_ROOT + "doc_boxmodel_iframe1.html"); + let {inspector, view, testActor} = yield openBoxModelView(); + + info("Test that the box model view works on the first page"); + yield assertBoxModelView(inspector, view, testActor); + + info("Reload the page"); + yield testActor.reload(); + yield inspector.once("markuploaded"); + + info("Test that the box model view works on the reloaded page"); + yield assertBoxModelView(inspector, view, testActor); +}); + +function* assertBoxModelView(inspector, view, testActor) { + info("Selecting the test node"); + yield selectNode("p", inspector); + + info("Checking that the box model view shows the right value"); + let paddingElt = view.doc.querySelector(".boxmodel-padding.boxmodel-top > span"); + is(paddingElt.textContent, "50"); + + info("Listening for box model view changes and modifying the padding"); + let onUpdated = waitForUpdate(inspector); + yield setStyle(testActor, "p", "padding", "20px"); + yield onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(paddingElt.textContent, "20"); +} diff --git a/devtools/client/inspector/components/test/browser_boxmodel_update-in-iframes.js b/devtools/client/inspector/components/test/browser_boxmodel_update-in-iframes.js new file mode 100644 index 000000000..50014ad1c --- /dev/null +++ b/devtools/client/inspector/components/test/browser_boxmodel_update-in-iframes.js @@ -0,0 +1,101 @@ +/* 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 the box model view for elements within iframes also updates when they +// change + +add_task(function* () { + yield addTab(URL_ROOT + "doc_boxmodel_iframe1.html"); + let {inspector, view, testActor} = yield openBoxModelView(); + + yield testResizingInIframe(inspector, view, testActor); + yield testReflowsAfterIframeDeletion(inspector, view, testActor); +}); + +function* testResizingInIframe(inspector, view, testActor) { + info("Test that resizing an element in an iframe updates its box model"); + + info("Selecting the nested test node"); + yield selectNodeInIframe2("div", inspector); + + info("Checking that the box model view shows the right value"); + let sizeElt = view.doc.querySelector(".boxmodel-size > span"); + is(sizeElt.textContent, "400\u00D7200"); + + info("Listening for box model view changes and modifying its size"); + let onUpdated = waitForUpdate(inspector); + yield setStyleInIframe2(testActor, "div", "width", "200px"); + yield onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(sizeElt.textContent, "200\u00D7200"); +} + +function* testReflowsAfterIframeDeletion(inspector, view, testActor) { + info("Test reflows are still sent to the box model view after deleting an " + + "iframe"); + + info("Deleting the iframe2"); + yield removeIframe2(testActor); + yield inspector.once("inspector-updated"); + + info("Selecting the test node in iframe1"); + yield selectNodeInIframe1("p", inspector); + + info("Checking that the box model view shows the right value"); + let sizeElt = view.doc.querySelector(".boxmodel-size > span"); + is(sizeElt.textContent, "100\u00D7100"); + + info("Listening for box model view changes and modifying its size"); + let onUpdated = waitForUpdate(inspector); + yield setStyleInIframe1(testActor, "p", "width", "200px"); + yield onUpdated; + ok(true, "Box model view got updated"); + + info("Checking that the box model view shows the right value after update"); + is(sizeElt.textContent, "200\u00D7100"); +} + +function* selectNodeInIframe1(selector, inspector) { + let iframe1 = yield getNodeFront("iframe", inspector); + let node = yield getNodeFrontInFrame(selector, iframe1, inspector); + yield selectNode(node, inspector); +} + +function* selectNodeInIframe2(selector, inspector) { + let iframe1 = yield getNodeFront("iframe", inspector); + let iframe2 = yield getNodeFrontInFrame("iframe", iframe1, inspector); + let node = yield getNodeFrontInFrame(selector, iframe2, inspector); + yield selectNode(node, inspector); +} + +function* setStyleInIframe1(testActor, selector, propertyName, value) { + yield testActor.eval(` + content.document.querySelector("iframe") + .contentDocument.querySelector("${selector}") + .style.${propertyName} = "${value}"; + `); +} + +function* setStyleInIframe2(testActor, selector, propertyName, value) { + yield testActor.eval(` + content.document.querySelector("iframe") + .contentDocument + .querySelector("iframe") + .contentDocument.querySelector("${selector}") + .style.${propertyName} = "${value}"; + `); +} + +function* removeIframe2(testActor) { + yield testActor.eval(` + content.document.querySelector("iframe") + .contentDocument + .querySelector("iframe") + .remove(); + `); +} diff --git a/devtools/client/inspector/components/test/doc_boxmodel_iframe1.html b/devtools/client/inspector/components/test/doc_boxmodel_iframe1.html new file mode 100644 index 000000000..eef48ce07 --- /dev/null +++ b/devtools/client/inspector/components/test/doc_boxmodel_iframe1.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<p style="padding:50px;color:#f06;">Root page</p> +<iframe src="doc_boxmodel_iframe2.html"></iframe> diff --git a/devtools/client/inspector/components/test/doc_boxmodel_iframe2.html b/devtools/client/inspector/components/test/doc_boxmodel_iframe2.html new file mode 100644 index 000000000..1f1b0463c --- /dev/null +++ b/devtools/client/inspector/components/test/doc_boxmodel_iframe2.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<p style="width:100px;height:100px;background:red;">iframe 1</p> +<iframe src="data:text/html,<div style='width:400px;height:200px;background:yellow;'>iframe 2</div>"></iframe> diff --git a/devtools/client/inspector/components/test/head.js b/devtools/client/inspector/components/test/head.js new file mode 100644 index 000000000..fa86b5e9e --- /dev/null +++ b/devtools/client/inspector/components/test/head.js @@ -0,0 +1,87 @@ +/* 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/ */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from ../../../framework/test/shared-head.js */ +/* import-globals-from ../../test/head.js */ +"use strict"; + +// Import the inspector's head.js first (which itself imports shared-head.js). +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js", + this); + +Services.prefs.setIntPref("devtools.toolbox.footer.height", 350); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.toolbox.footer.height"); +}); + +/** + * Highlight a node and set the inspector's current selection to the node or + * the first match of the given css selector. + * @param {String|NodeFront} selectorOrNodeFront + * The selector for the node to be set, or the nodeFront + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox + * @return a promise that resolves when the inspector is updated with the new + * node + */ +function* selectAndHighlightNode(selectorOrNodeFront, inspector) { + info("Highlighting and selecting the node " + selectorOrNodeFront); + + let nodeFront = yield getNodeFront(selectorOrNodeFront, inspector); + let updated = inspector.toolbox.once("highlighter-ready"); + inspector.selection.setNodeFront(nodeFront, "test-highlight"); + yield updated; +} + +/** + * Open the toolbox, with the inspector tool visible, and the computed view + * sidebar tab selected to display the box model view. + * @return a promise that resolves when the inspector is ready and the box model + * view is visible and ready + */ +function openBoxModelView() { + return openInspectorSidebarTab("computedview").then(data => { + // The actual highligher show/hide methods are mocked in box model tests. + // The highlighter is tested in devtools/inspector/test. + function mockHighlighter({highlighter}) { + highlighter.showBoxModel = function () { + return promise.resolve(); + }; + highlighter.hideBoxModel = function () { + return promise.resolve(); + }; + } + mockHighlighter(data.toolbox); + + return { + toolbox: data.toolbox, + inspector: data.inspector, + view: data.inspector.computedview.boxModelView, + testActor: data.testActor + }; + }); +} + +/** + * Wait for the boxmodel-view-updated event. + * @return a promise + */ +function waitForUpdate(inspector) { + return inspector.once("boxmodel-view-updated"); +} + +function getStyle(testActor, selector, propertyName) { + return testActor.eval(` + content.document.querySelector("${selector}") + .style.getPropertyValue("${propertyName}"); + `); +} + +function setStyle(testActor, selector, propertyName, value) { + return testActor.eval(` + content.document.querySelector("${selector}") + .style.${propertyName} = "${value}"; + `); +} diff --git a/devtools/client/inspector/computed/computed.js b/devtools/client/inspector/computed/computed.js new file mode 100644 index 000000000..71d602a4e --- /dev/null +++ b/devtools/client/inspector/computed/computed.js @@ -0,0 +1,1522 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const ToolDefinitions = require("devtools/client/definitions").Tools; +const CssLogic = require("devtools/shared/inspector/css-logic"); +const {ELEMENT_STYLE} = require("devtools/shared/specs/styles"); +const promise = require("promise"); +const defer = require("devtools/shared/defer"); +const Services = require("Services"); +const {OutputParser} = require("devtools/client/shared/output-parser"); +const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils"); +const {createChild} = require("devtools/client/inspector/shared/utils"); +const {gDevTools} = require("devtools/client/framework/devtools"); +const {getCssProperties} = require("devtools/shared/fronts/css-properties"); +const HighlightersOverlay = require("devtools/client/inspector/shared/highlighters-overlay"); +const { + VIEW_NODE_SELECTOR_TYPE, + VIEW_NODE_PROPERTY_TYPE, + VIEW_NODE_VALUE_TYPE, + VIEW_NODE_IMAGE_URL_TYPE, +} = require("devtools/client/inspector/shared/node-types"); +const StyleInspectorMenu = require("devtools/client/inspector/shared/style-inspector-menu"); +const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay"); +const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts"); +const {BoxModelView} = require("devtools/client/inspector/components/box-model"); +const clipboardHelper = require("devtools/shared/platform/clipboard"); + +const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties"; +const {LocalizationHelper} = require("devtools/shared/l10n"); +const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES); + +const FILTER_CHANGED_TIMEOUT = 150; +const HTML_NS = "http://www.w3.org/1999/xhtml"; + +/** + * Helper for long-running processes that should yield occasionally to + * the mainloop. + * + * @param {Window} win + * Timeouts will be set on this window when appropriate. + * @param {Array} array + * The array of items to process. + * @param {Object} options + * Options for the update process: + * onItem {function} Will be called with the value of each iteration. + * onBatch {function} Will be called after each batch of iterations, + * before yielding to the main loop. + * onDone {function} Will be called when iteration is complete. + * onCancel {function} Will be called if the process is canceled. + * threshold {int} How long to process before yielding, in ms. + */ +function UpdateProcess(win, array, options) { + this.win = win; + this.index = 0; + this.array = array; + + this.onItem = options.onItem || function () {}; + this.onBatch = options.onBatch || function () {}; + this.onDone = options.onDone || function () {}; + this.onCancel = options.onCancel || function () {}; + this.threshold = options.threshold || 45; + + this.canceled = false; +} + +UpdateProcess.prototype = { + /** + * Error thrown when the array of items to process is empty. + */ + ERROR_ITERATION_DONE: new Error("UpdateProcess iteration done"), + + /** + * Schedule a new batch on the main loop. + */ + schedule: function () { + if (this.canceled) { + return; + } + this._timeout = setTimeout(this._timeoutHandler.bind(this), 0); + }, + + /** + * Cancel the running process. onItem will not be called again, + * and onCancel will be called. + */ + cancel: function () { + if (this._timeout) { + clearTimeout(this._timeout); + this._timeout = 0; + } + this.canceled = true; + this.onCancel(); + }, + + _timeoutHandler: function () { + this._timeout = null; + try { + this._runBatch(); + this.schedule(); + } catch (e) { + if (e === this.ERROR_ITERATION_DONE) { + this.onBatch(); + this.onDone(); + return; + } + console.error(e); + throw e; + } + }, + + _runBatch: function () { + let time = Date.now(); + while (!this.canceled) { + let next = this._next(); + this.onItem(next); + if ((Date.now() - time) > this.threshold) { + this.onBatch(); + return; + } + } + }, + + /** + * Returns the item at the current index and increases the index. + * If all items have already been processed, will throw ERROR_ITERATION_DONE. + */ + _next: function () { + if (this.index < this.array.length) { + return this.array[this.index++]; + } + throw this.ERROR_ITERATION_DONE; + }, +}; + +/** + * CssComputedView is a panel that manages the display of a table + * sorted by style. There should be one instance of CssComputedView + * per style display (of which there will generally only be one). + * + * @param {Inspector} inspector + * Inspector toolbox panel + * @param {Document} document + * The document that will contain the computed view. + * @param {PageStyleFront} pageStyle + * Front for the page style actor that will be providing + * the style information. + */ +function CssComputedView(inspector, document, pageStyle) { + this.inspector = inspector; + this.styleDocument = document; + this.styleWindow = this.styleDocument.defaultView; + this.pageStyle = pageStyle; + + this.propertyViews = []; + + let cssProperties = getCssProperties(inspector.toolbox); + this._outputParser = new OutputParser(document, cssProperties); + + // Create bound methods. + this.focusWindow = this.focusWindow.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + this._onClick = this._onClick.bind(this); + this._onCopy = this._onCopy.bind(this); + this._onFilterStyles = this._onFilterStyles.bind(this); + this._onClearSearch = this._onClearSearch.bind(this); + this._onIncludeBrowserStyles = this._onIncludeBrowserStyles.bind(this); + + let doc = this.styleDocument; + this.element = doc.getElementById("propertyContainer"); + this.searchField = doc.getElementById("computedview-searchbox"); + this.searchClearButton = doc.getElementById("computedview-searchinput-clear"); + this.includeBrowserStylesCheckbox = + doc.getElementById("browser-style-checkbox"); + + this.shortcuts = new KeyShortcuts({ window: this.styleWindow }); + this._onShortcut = this._onShortcut.bind(this); + this.shortcuts.on("CmdOrCtrl+F", this._onShortcut); + this.shortcuts.on("Escape", this._onShortcut); + this.styleDocument.addEventListener("mousedown", this.focusWindow); + this.element.addEventListener("click", this._onClick); + this.element.addEventListener("copy", this._onCopy); + this.element.addEventListener("contextmenu", this._onContextMenu); + this.searchField.addEventListener("input", this._onFilterStyles); + this.searchField.addEventListener("contextmenu", this.inspector.onTextBoxContextMenu); + this.searchClearButton.addEventListener("click", this._onClearSearch); + this.includeBrowserStylesCheckbox.addEventListener("input", + this._onIncludeBrowserStyles); + + this.searchClearButton.hidden = true; + + // No results text. + this.noResults = this.styleDocument.getElementById("computedview-no-results"); + + // Refresh panel when color unit changed. + this._handlePrefChange = this._handlePrefChange.bind(this); + gDevTools.on("pref-changed", this._handlePrefChange); + + // Refresh panel when pref for showing original sources changes + this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this); + this._prefObserver = new PrefObserver("devtools."); + this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged); + + // The element that we're inspecting, and the document that it comes from. + this._viewedElement = null; + + this.createStyleViews(); + + this._contextmenu = new StyleInspectorMenu(this, { isRuleView: false }); + + // Add the tooltips and highlightersoverlay + this.tooltips = new TooltipsOverlay(this); + this.tooltips.addToView(); + + this.highlighters = new HighlightersOverlay(this); + this.highlighters.addToView(); +} + +/** + * Lookup a l10n string in the shared styleinspector string bundle. + * + * @param {String} name + * The key to lookup. + * @returns {String} localized version of the given key. + */ +CssComputedView.l10n = function (name) { + try { + return STYLE_INSPECTOR_L10N.getStr(name); + } catch (ex) { + console.log("Error reading '" + name + "'"); + throw new Error("l10n error with " + name); + } +}; + +CssComputedView.prototype = { + // Cache the list of properties that match the selected element. + _matchedProperties: null, + + // Used for cancelling timeouts in the style filter. + _filterChangedTimeout: null, + + // Holds the ID of the panelRefresh timeout. + _panelRefreshTimeout: null, + + // Toggle for zebra striping + _darkStripe: true, + + // Number of visible properties + numVisibleProperties: 0, + + setPageStyle: function (pageStyle) { + this.pageStyle = pageStyle; + }, + + get includeBrowserStyles() { + return this.includeBrowserStylesCheckbox.checked; + }, + + _handlePrefChange: function (event, data) { + if (this._computed && (data.pref === "devtools.defaultColorUnit" || + data.pref === PREF_ORIG_SOURCES)) { + this.refreshPanel(); + } + }, + + /** + * Update the view with a new selected element. The CssComputedView panel + * will show the style information for the given element. + * + * @param {NodeFront} element + * The highlighted node to get styles for. + * @returns a promise that will be resolved when highlighting is complete. + */ + selectElement: function (element) { + if (!element) { + this._viewedElement = null; + this.noResults.hidden = false; + + if (this._refreshProcess) { + this._refreshProcess.cancel(); + } + // Hiding all properties + for (let propView of this.propertyViews) { + propView.refresh(); + } + return promise.resolve(undefined); + } + + if (element === this._viewedElement) { + return promise.resolve(undefined); + } + + this._viewedElement = element; + this.refreshSourceFilter(); + + return this.refreshPanel(); + }, + + /** + * Get the type of a given node in the computed-view + * + * @param {DOMNode} node + * The node which we want information about + * @return {Object} The type information object contains the following props: + * - type {String} One of the VIEW_NODE_XXX_TYPE const in + * client/inspector/shared/node-types + * - value {Object} Depends on the type of the node + * returns null if the node isn't anything we care about + */ + getNodeInfo: function (node) { + if (!node) { + return null; + } + + let classes = node.classList; + + // Check if the node isn't a selector first since this doesn't require + // walking the DOM + if (classes.contains("matched") || + classes.contains("bestmatch") || + classes.contains("parentmatch")) { + let selectorText = ""; + for (let child of node.childNodes) { + if (child.nodeType === node.TEXT_NODE) { + selectorText += child.textContent; + } + } + return { + type: VIEW_NODE_SELECTOR_TYPE, + value: selectorText.trim() + }; + } + + // Walk up the nodes to find out where node is + let propertyView; + let propertyContent; + let parent = node; + while (parent.parentNode) { + if (parent.classList.contains("property-view")) { + propertyView = parent; + break; + } + if (parent.classList.contains("property-content")) { + propertyContent = parent; + break; + } + parent = parent.parentNode; + } + if (!propertyView && !propertyContent) { + return null; + } + + let value, type; + + // Get the property and value for a node that's a property name or value + let isHref = classes.contains("theme-link") && !classes.contains("link"); + if (propertyView && (classes.contains("property-name") || + classes.contains("property-value") || + isHref)) { + value = { + property: parent.querySelector(".property-name").textContent, + value: parent.querySelector(".property-value").textContent + }; + } + if (propertyContent && (classes.contains("other-property-value") || + isHref)) { + let view = propertyContent.previousSibling; + value = { + property: view.querySelector(".property-name").textContent, + value: node.textContent + }; + } + + // Get the type + if (classes.contains("property-name")) { + type = VIEW_NODE_PROPERTY_TYPE; + } else if (classes.contains("property-value") || + classes.contains("other-property-value")) { + type = VIEW_NODE_VALUE_TYPE; + } else if (isHref) { + type = VIEW_NODE_IMAGE_URL_TYPE; + value.url = node.href; + } else { + return null; + } + + return {type, value}; + }, + + _createPropertyViews: function () { + if (this._createViewsPromise) { + return this._createViewsPromise; + } + + let deferred = defer(); + this._createViewsPromise = deferred.promise; + + this.refreshSourceFilter(); + this.numVisibleProperties = 0; + let fragment = this.styleDocument.createDocumentFragment(); + + this._createViewsProcess = new UpdateProcess( + this.styleWindow, CssComputedView.propertyNames, { + onItem: (propertyName) => { + // Per-item callback. + let propView = new PropertyView(this, propertyName); + fragment.appendChild(propView.buildMain()); + fragment.appendChild(propView.buildSelectorContainer()); + + if (propView.visible) { + this.numVisibleProperties++; + } + this.propertyViews.push(propView); + }, + onCancel: () => { + deferred.reject("_createPropertyViews cancelled"); + }, + onDone: () => { + // Completed callback. + this.element.appendChild(fragment); + this.noResults.hidden = this.numVisibleProperties > 0; + deferred.resolve(undefined); + } + } + ); + + this._createViewsProcess.schedule(); + return deferred.promise; + }, + + /** + * Refresh the panel content. + */ + refreshPanel: function () { + if (!this._viewedElement) { + return promise.resolve(); + } + + // Capture the current viewed element to return from the promise handler + // early if it changed + let viewedElement = this._viewedElement; + + return promise.all([ + this._createPropertyViews(), + this.pageStyle.getComputed(this._viewedElement, { + filter: this._sourceFilter, + onlyMatched: !this.includeBrowserStyles, + markMatched: true + }) + ]).then(([, computed]) => { + if (viewedElement !== this._viewedElement) { + return promise.resolve(); + } + + this._matchedProperties = new Set(); + for (let name in computed) { + if (computed[name].matched) { + this._matchedProperties.add(name); + } + } + this._computed = computed; + + if (this._refreshProcess) { + this._refreshProcess.cancel(); + } + + this.noResults.hidden = true; + + // Reset visible property count + this.numVisibleProperties = 0; + + // Reset zebra striping. + this._darkStripe = true; + + let deferred = defer(); + this._refreshProcess = new UpdateProcess( + this.styleWindow, this.propertyViews, { + onItem: (propView) => { + propView.refresh(); + }, + onCancel: () => { + deferred.reject("_refreshProcess of computed view cancelled"); + }, + onDone: () => { + this._refreshProcess = null; + this.noResults.hidden = this.numVisibleProperties > 0; + + if (this.searchField.value.length > 0 && + !this.numVisibleProperties) { + this.searchField.classList + .add("devtools-style-searchbox-no-match"); + } else { + this.searchField.classList + .remove("devtools-style-searchbox-no-match"); + } + + this.inspector.emit("computed-view-refreshed"); + deferred.resolve(undefined); + } + } + ); + this._refreshProcess.schedule(); + return deferred.promise; + }).then(null, (err) => console.error(err)); + }, + + /** + * Handle the shortcut events in the computed view. + */ + _onShortcut: function (name, event) { + if (!event.target.closest("#sidebar-panel-computedview")) { + return; + } + // Handle the search box's keypress event. If the escape key is pressed, + // clear the search box field. + if (name === "Escape" && event.target === this.searchField && + this._onClearSearch()) { + event.preventDefault(); + event.stopPropagation(); + } else if (name === "CmdOrCtrl+F") { + this.searchField.focus(); + event.preventDefault(); + } + }, + + /** + * Set the filter style search value. + * @param {String} value + * The search value. + */ + setFilterStyles: function (value = "") { + this.searchField.value = value; + this.searchField.focus(); + this._onFilterStyles(); + }, + + /** + * Called when the user enters a search term in the filter style search box. + */ + _onFilterStyles: function () { + if (this._filterChangedTimeout) { + clearTimeout(this._filterChangedTimeout); + } + + let filterTimeout = (this.searchField.value.length > 0) + ? FILTER_CHANGED_TIMEOUT : 0; + this.searchClearButton.hidden = this.searchField.value.length === 0; + + this._filterChangedTimeout = setTimeout(() => { + if (this.searchField.value.length > 0) { + this.searchField.setAttribute("filled", true); + this.inspector.emit("computed-view-filtered", true); + } else { + this.searchField.removeAttribute("filled"); + this.inspector.emit("computed-view-filtered", false); + } + + this.refreshPanel(); + this._filterChangeTimeout = null; + }, filterTimeout); + }, + + /** + * Called when the user clicks on the clear button in the filter style search + * box. Returns true if the search box is cleared and false otherwise. + */ + _onClearSearch: function () { + if (this.searchField.value) { + this.setFilterStyles(""); + return true; + } + + return false; + }, + + /** + * The change event handler for the includeBrowserStyles checkbox. + */ + _onIncludeBrowserStyles: function () { + this.refreshSourceFilter(); + this.refreshPanel(); + }, + + /** + * When includeBrowserStylesCheckbox.checked is false we only display + * properties that have matched selectors and have been included by the + * document or one of thedocument's stylesheets. If .checked is false we + * display all properties including those that come from UA stylesheets. + */ + refreshSourceFilter: function () { + this._matchedProperties = null; + this._sourceFilter = this.includeBrowserStyles ? + CssLogic.FILTER.UA : + CssLogic.FILTER.USER; + }, + + _onSourcePrefChanged: function () { + for (let propView of this.propertyViews) { + propView.updateSourceLinks(); + } + this.inspector.emit("computed-view-sourcelinks-updated"); + }, + + /** + * The CSS as displayed by the UI. + */ + createStyleViews: function () { + if (CssComputedView.propertyNames) { + return; + } + + CssComputedView.propertyNames = []; + + // Here we build and cache a list of css properties supported by the browser + // We could use any element but let's use the main document's root element + let styles = this.styleWindow + .getComputedStyle(this.styleDocument.documentElement); + let mozProps = []; + for (let i = 0, numStyles = styles.length; i < numStyles; i++) { + let prop = styles.item(i); + if (prop.startsWith("--")) { + // Skip any CSS variables used inside of browser CSS files + continue; + } else if (prop.startsWith("-")) { + mozProps.push(prop); + } else { + CssComputedView.propertyNames.push(prop); + } + } + + CssComputedView.propertyNames.sort(); + CssComputedView.propertyNames.push.apply(CssComputedView.propertyNames, + mozProps.sort()); + + this._createPropertyViews().then(null, e => { + if (!this._isDestroyed) { + console.warn("The creation of property views was cancelled because " + + "the computed-view was destroyed before it was done creating views"); + } else { + console.error(e); + } + }); + }, + + /** + * Get a set of properties that have matched selectors. + * + * @return {Set} If a property name is in the set, it has matching selectors. + */ + get matchedProperties() { + return this._matchedProperties || new Set(); + }, + + /** + * Focus the window on mousedown. + */ + focusWindow: function () { + this.styleWindow.focus(); + }, + + /** + * Context menu handler. + */ + _onContextMenu: function (event) { + this._contextmenu.show(event); + }, + + _onClick: function (event) { + let target = event.target; + + if (target.nodeName === "a") { + event.stopPropagation(); + event.preventDefault(); + let browserWin = this.inspector.target.tab.ownerDocument.defaultView; + browserWin.openUILinkIn(target.href, "tab"); + } + }, + + /** + * Callback for copy event. Copy selected text. + * + * @param {Event} event + * copy event object. + */ + _onCopy: function (event) { + this.copySelection(); + event.preventDefault(); + }, + + /** + * Copy the current selection to the clipboard + */ + copySelection: function () { + try { + let win = this.styleWindow; + let text = win.getSelection().toString().trim(); + + // Tidy up block headings by moving CSS property names and their + // values onto the same line and inserting a colon between them. + let textArray = text.split(/[\r\n]+/); + let result = ""; + + // Parse text array to output string. + if (textArray.length > 1) { + for (let prop of textArray) { + if (CssComputedView.propertyNames.indexOf(prop) !== -1) { + // Property name + result += prop; + } else { + // Property value + result += ": " + prop + ";\n"; + } + } + } else { + // Short text fragment. + result = textArray[0]; + } + + clipboardHelper.copyString(result); + } catch (e) { + console.error(e); + } + }, + + /** + * Destructor for CssComputedView. + */ + destroy: function () { + this._viewedElement = null; + this._outputParser = null; + + gDevTools.off("pref-changed", this._handlePrefChange); + + this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged); + this._prefObserver.destroy(); + + // Cancel tree construction + if (this._createViewsProcess) { + this._createViewsProcess.cancel(); + } + if (this._refreshProcess) { + this._refreshProcess.cancel(); + } + + // Remove context menu + if (this._contextmenu) { + this._contextmenu.destroy(); + this._contextmenu = null; + } + + this.tooltips.destroy(); + this.highlighters.destroy(); + + // Remove bound listeners + this.styleDocument.removeEventListener("mousedown", this.focusWindow); + this.element.removeEventListener("click", this._onClick); + this.element.removeEventListener("copy", this._onCopy); + this.element.removeEventListener("contextmenu", this._onContextMenu); + this.searchField.removeEventListener("input", this._onFilterStyles); + this.searchField.removeEventListener("contextmenu", + this.inspector.onTextBoxContextMenu); + this.searchClearButton.removeEventListener("click", this._onClearSearch); + this.includeBrowserStylesCheckbox.removeEventListener("input", + this._onIncludeBrowserStyles); + + // Nodes used in templating + this.element = null; + this.panel = null; + this.searchField = null; + this.searchClearButton = null; + this.includeBrowserStylesCheckbox = null; + + // Property views + for (let propView of this.propertyViews) { + propView.destroy(); + } + this.propertyViews = null; + + this.inspector = null; + this.styleDocument = null; + this.styleWindow = null; + + this._isDestroyed = true; + } +}; + +function PropertyInfo(tree, name) { + this.tree = tree; + this.name = name; +} + +PropertyInfo.prototype = { + get value() { + if (this.tree._computed) { + let value = this.tree._computed[this.name].value; + return value; + } + return null; + } +}; + +/** + * A container to give easy access to property data from the template engine. + * + * @param {CssComputedView} tree + * The CssComputedView instance we are working with. + * @param {String} name + * The CSS property name for which this PropertyView + * instance will render the rules. + */ +function PropertyView(tree, name) { + this.tree = tree; + this.name = name; + + this.link = "https://developer.mozilla.org/CSS/" + name; + + this._propertyInfo = new PropertyInfo(tree, name); +} + +PropertyView.prototype = { + // The parent element which contains the open attribute + element: null, + + // Property header node + propertyHeader: null, + + // Destination for property names + nameNode: null, + + // Destination for property values + valueNode: null, + + // Are matched rules expanded? + matchedExpanded: false, + + // Matched selector container + matchedSelectorsContainer: null, + + // Matched selector expando + matchedExpander: null, + + // Cache for matched selector views + _matchedSelectorViews: null, + + // The previously selected element used for the selector view caches + _prevViewedElement: null, + + /** + * Get the computed style for the current property. + * + * @return {String} the computed style for the current property of the + * currently highlighted element. + */ + get value() { + return this.propertyInfo.value; + }, + + /** + * An easy way to access the CssPropertyInfo behind this PropertyView. + */ + get propertyInfo() { + return this._propertyInfo; + }, + + /** + * Does the property have any matched selectors? + */ + get hasMatchedSelectors() { + return this.tree.matchedProperties.has(this.name); + }, + + /** + * Should this property be visible? + */ + get visible() { + if (!this.tree._viewedElement) { + return false; + } + + if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) { + return false; + } + + let searchTerm = this.tree.searchField.value.toLowerCase(); + let isValidSearchTerm = searchTerm.trim().length > 0; + if (isValidSearchTerm && + this.name.toLowerCase().indexOf(searchTerm) === -1 && + this.value.toLowerCase().indexOf(searchTerm) === -1) { + return false; + } + + return true; + }, + + /** + * Returns the className that should be assigned to the propertyView. + * + * @return {String} + */ + get propertyHeaderClassName() { + if (this.visible) { + let isDark = this.tree._darkStripe = !this.tree._darkStripe; + return isDark ? "property-view row-striped" : "property-view"; + } + return "property-view-hidden"; + }, + + /** + * Returns the className that should be assigned to the propertyView content + * container. + * + * @return {String} + */ + get propertyContentClassName() { + if (this.visible) { + let isDark = this.tree._darkStripe; + return isDark ? "property-content row-striped" : "property-content"; + } + return "property-content-hidden"; + }, + + /** + * Build the markup for on computed style + * + * @return {Element} + */ + buildMain: function () { + let doc = this.tree.styleDocument; + + // Build the container element + this.onMatchedToggle = this.onMatchedToggle.bind(this); + this.element = doc.createElementNS(HTML_NS, "div"); + this.element.setAttribute("class", this.propertyHeaderClassName); + this.element.addEventListener("dblclick", this.onMatchedToggle, false); + + // Make it keyboard navigable + this.element.setAttribute("tabindex", "0"); + this.shortcuts = new KeyShortcuts({ + window: this.tree.styleWindow, + target: this.element + }); + this.shortcuts.on("F1", (name, event) => { + this.mdnLinkClick(event); + // Prevent opening the options panel + event.preventDefault(); + event.stopPropagation(); + }); + this.shortcuts.on("Return", (name, event) => this.onMatchedToggle(event)); + this.shortcuts.on("Space", (name, event) => this.onMatchedToggle(event)); + + let nameContainer = doc.createElementNS(HTML_NS, "div"); + nameContainer.className = "property-name-container"; + this.element.appendChild(nameContainer); + + // Build the twisty expand/collapse + this.matchedExpander = doc.createElementNS(HTML_NS, "div"); + this.matchedExpander.className = "expander theme-twisty"; + this.matchedExpander.addEventListener("click", this.onMatchedToggle, false); + nameContainer.appendChild(this.matchedExpander); + + // Build the style name element + this.nameNode = doc.createElementNS(HTML_NS, "div"); + this.nameNode.setAttribute("class", "property-name theme-fg-color5"); + // Reset its tabindex attribute otherwise, if an ellipsis is applied + // it will be reachable via TABing + this.nameNode.setAttribute("tabindex", ""); + // Avoid english text (css properties) from being altered + // by RTL mode + this.nameNode.setAttribute("dir", "ltr"); + this.nameNode.textContent = this.nameNode.title = this.name; + // Make it hand over the focus to the container + this.onFocus = () => this.element.focus(); + this.nameNode.addEventListener("click", this.onFocus, false); + nameContainer.appendChild(this.nameNode); + + let valueContainer = doc.createElementNS(HTML_NS, "div"); + valueContainer.className = "property-value-container"; + this.element.appendChild(valueContainer); + + // Build the style value element + this.valueNode = doc.createElementNS(HTML_NS, "div"); + this.valueNode.setAttribute("class", "property-value theme-fg-color1"); + // Reset its tabindex attribute otherwise, if an ellipsis is applied + // it will be reachable via TABing + this.valueNode.setAttribute("tabindex", ""); + this.valueNode.setAttribute("dir", "ltr"); + // Make it hand over the focus to the container + this.valueNode.addEventListener("click", this.onFocus, false); + valueContainer.appendChild(this.valueNode); + + return this.element; + }, + + buildSelectorContainer: function () { + let doc = this.tree.styleDocument; + let element = doc.createElementNS(HTML_NS, "div"); + element.setAttribute("class", this.propertyContentClassName); + this.matchedSelectorsContainer = doc.createElementNS(HTML_NS, "div"); + this.matchedSelectorsContainer.setAttribute("class", "matchedselectors"); + element.appendChild(this.matchedSelectorsContainer); + + return element; + }, + + /** + * Refresh the panel's CSS property value. + */ + refresh: function () { + this.element.className = this.propertyHeaderClassName; + this.element.nextElementSibling.className = this.propertyContentClassName; + + if (this._prevViewedElement !== this.tree._viewedElement) { + this._matchedSelectorViews = null; + this._prevViewedElement = this.tree._viewedElement; + } + + if (!this.tree._viewedElement || !this.visible) { + this.valueNode.textContent = this.valueNode.title = ""; + this.matchedSelectorsContainer.parentNode.hidden = true; + this.matchedSelectorsContainer.textContent = ""; + this.matchedExpander.removeAttribute("open"); + return; + } + + this.tree.numVisibleProperties++; + + let outputParser = this.tree._outputParser; + let frag = outputParser.parseCssProperty(this.propertyInfo.name, + this.propertyInfo.value, + { + colorSwatchClass: "computedview-colorswatch", + colorClass: "computedview-color", + urlClass: "theme-link" + // No need to use baseURI here as computed URIs are never relative. + }); + this.valueNode.innerHTML = ""; + this.valueNode.appendChild(frag); + + this.refreshMatchedSelectors(); + }, + + /** + * Refresh the panel matched rules. + */ + refreshMatchedSelectors: function () { + let hasMatchedSelectors = this.hasMatchedSelectors; + this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors; + + if (hasMatchedSelectors) { + this.matchedExpander.classList.add("expandable"); + } else { + this.matchedExpander.classList.remove("expandable"); + } + + if (this.matchedExpanded && hasMatchedSelectors) { + return this.tree.pageStyle + .getMatchedSelectors(this.tree._viewedElement, this.name) + .then(matched => { + if (!this.matchedExpanded) { + return promise.resolve(undefined); + } + + this._matchedSelectorResponse = matched; + + return this._buildMatchedSelectors().then(() => { + this.matchedExpander.setAttribute("open", ""); + this.tree.inspector.emit("computed-view-property-expanded"); + }); + }).then(null, console.error); + } + + this.matchedSelectorsContainer.innerHTML = ""; + this.matchedExpander.removeAttribute("open"); + this.tree.inspector.emit("computed-view-property-collapsed"); + return promise.resolve(undefined); + }, + + get matchedSelectors() { + return this._matchedSelectorResponse; + }, + + _buildMatchedSelectors: function () { + let promises = []; + let frag = this.element.ownerDocument.createDocumentFragment(); + + for (let selector of this.matchedSelectorViews) { + let p = createChild(frag, "p"); + let span = createChild(p, "span", { + class: "rule-link" + }); + let link = createChild(span, "a", { + target: "_blank", + class: "link theme-link", + title: selector.href, + sourcelocation: selector.source, + tabindex: "0", + textContent: selector.source + }); + link.addEventListener("click", selector.openStyleEditor, false); + let shortcuts = new KeyShortcuts({ + window: this.tree.styleWindow, + target: link + }); + shortcuts.on("Return", () => selector.openStyleEditor()); + + let status = createChild(p, "span", { + dir: "ltr", + class: "rule-text theme-fg-color3 " + selector.statusClass, + title: selector.statusText, + textContent: selector.sourceText + }); + let valueSpan = createChild(status, "span", { + class: "other-property-value theme-fg-color1" + }); + valueSpan.appendChild(selector.outputFragment); + promises.push(selector.ready); + } + + this.matchedSelectorsContainer.innerHTML = ""; + this.matchedSelectorsContainer.appendChild(frag); + return promise.all(promises); + }, + + /** + * Provide access to the matched SelectorViews that we are currently + * displaying. + */ + get matchedSelectorViews() { + if (!this._matchedSelectorViews) { + this._matchedSelectorViews = []; + this._matchedSelectorResponse.forEach(selectorInfo => { + let selectorView = new SelectorView(this.tree, selectorInfo); + this._matchedSelectorViews.push(selectorView); + }, this); + } + return this._matchedSelectorViews; + }, + + /** + * Update all the selector source links to reflect whether we're linking to + * original sources (e.g. Sass files). + */ + updateSourceLinks: function () { + if (!this._matchedSelectorViews) { + return; + } + for (let view of this._matchedSelectorViews) { + view.updateSourceLink(); + } + }, + + /** + * The action when a user expands matched selectors. + * + * @param {Event} event + * Used to determine the class name of the targets click + * event. + */ + onMatchedToggle: function (event) { + if (event.shiftKey) { + return; + } + this.matchedExpanded = !this.matchedExpanded; + this.refreshMatchedSelectors(); + event.preventDefault(); + }, + + /** + * The action when a user clicks on the MDN help link for a property. + */ + mdnLinkClick: function (event) { + let inspector = this.tree.inspector; + + if (inspector.target.tab) { + let browserWin = inspector.target.tab.ownerDocument.defaultView; + browserWin.openUILinkIn(this.link, "tab"); + } + }, + + /** + * Destroy this property view, removing event listeners + */ + destroy: function () { + this.element.removeEventListener("dblclick", this.onMatchedToggle, false); + this.shortcuts.destroy(); + this.element = null; + + this.matchedExpander.removeEventListener("click", this.onMatchedToggle, + false); + this.matchedExpander = null; + + this.nameNode.removeEventListener("click", this.onFocus, false); + this.nameNode = null; + + this.valueNode.removeEventListener("click", this.onFocus, false); + this.valueNode = null; + } +}; + +/** + * A container to give us easy access to display data from a CssRule + * + * @param CssComputedView tree + * the owning CssComputedView + * @param selectorInfo + */ +function SelectorView(tree, selectorInfo) { + this.tree = tree; + this.selectorInfo = selectorInfo; + this._cacheStatusNames(); + + this.openStyleEditor = this.openStyleEditor.bind(this); + + this.ready = this.updateSourceLink(); +} + +/** + * Decode for cssInfo.rule.status + * @see SelectorView.prototype._cacheStatusNames + * @see CssLogic.STATUS + */ +SelectorView.STATUS_NAMES = [ + // "Parent Match", "Matched", "Best Match" +]; + +SelectorView.CLASS_NAMES = [ + "parentmatch", "matched", "bestmatch" +]; + +SelectorView.prototype = { + /** + * Cache localized status names. + * + * These statuses are localized inside the styleinspector.properties string + * bundle. + * @see css-logic.js - the CssLogic.STATUS array. + */ + _cacheStatusNames: function () { + if (SelectorView.STATUS_NAMES.length) { + return; + } + + for (let status in CssLogic.STATUS) { + let i = CssLogic.STATUS[status]; + if (i > CssLogic.STATUS.UNMATCHED) { + let value = CssComputedView.l10n("rule.status." + status); + // Replace normal spaces with non-breaking spaces + SelectorView.STATUS_NAMES[i] = value.replace(/ /g, "\u00A0"); + } + } + }, + + /** + * A localized version of cssRule.status + */ + get statusText() { + return SelectorView.STATUS_NAMES[this.selectorInfo.status]; + }, + + /** + * Get class name for selector depending on status + */ + get statusClass() { + return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1]; + }, + + get href() { + if (this._href) { + return this._href; + } + let sheet = this.selectorInfo.rule.parentStyleSheet; + this._href = sheet ? sheet.href : "#"; + return this._href; + }, + + get sourceText() { + return this.selectorInfo.sourceText; + }, + + get value() { + return this.selectorInfo.value; + }, + + get outputFragment() { + // Sadly, because this fragment is added to the template by DOM Templater + // we lose any events that are attached. This means that URLs will open in a + // new window. At some point we should fix this by stopping using the + // templater. + let outputParser = this.tree._outputParser; + let frag = outputParser.parseCssProperty( + this.selectorInfo.name, + this.selectorInfo.value, { + colorSwatchClass: "computedview-colorswatch", + colorClass: "computedview-color", + urlClass: "theme-link", + baseURI: this.selectorInfo.rule.href + } + ); + return frag; + }, + + /** + * Update the text of the source link to reflect whether we're showing + * original sources or not. + */ + updateSourceLink: function () { + return this.updateSource().then((oldSource) => { + if (oldSource !== this.source && this.tree.element) { + let selector = '[sourcelocation="' + oldSource + '"]'; + let link = this.tree.element.querySelector(selector); + if (link) { + link.textContent = this.source; + link.setAttribute("sourcelocation", this.source); + } + } + }); + }, + + /** + * Update the 'source' store based on our original sources preference. + */ + updateSource: function () { + let rule = this.selectorInfo.rule; + this.sheet = rule.parentStyleSheet; + + if (!rule || !this.sheet) { + let oldSource = this.source; + this.source = CssLogic.l10n("rule.sourceElement"); + return promise.resolve(oldSource); + } + + let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); + + if (showOrig && rule.type !== ELEMENT_STYLE) { + let deferred = defer(); + + // set as this first so we show something while we're fetching + this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line; + + rule.getOriginalLocation().then(({href, line}) => { + let oldSource = this.source; + this.source = CssLogic.shortSource({href: href}) + ":" + line; + deferred.resolve(oldSource); + }); + + return deferred.promise; + } + + let oldSource = this.source; + this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line; + return promise.resolve(oldSource); + }, + + /** + * When a css link is clicked this method is called in order to either: + * 1. Open the link in view source (for chrome stylesheets). + * 2. Open the link in the style editor. + * + * We can only view stylesheets contained in document.styleSheets inside the + * style editor. + */ + openStyleEditor: function () { + let inspector = this.tree.inspector; + let rule = this.selectorInfo.rule; + + // The style editor can only display stylesheets coming from content because + // chrome stylesheets are not listed in the editor's stylesheet selector. + // + // If the stylesheet is a content stylesheet we send it to the style + // editor else we display it in the view source window. + let parentStyleSheet = rule.parentStyleSheet; + if (!parentStyleSheet || parentStyleSheet.isSystem) { + let toolbox = gDevTools.getToolbox(inspector.target); + toolbox.viewSource(rule.href, rule.line); + return; + } + + let location = promise.resolve(rule.location); + if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { + location = rule.getOriginalLocation(); + } + + location.then(({source, href, line, column}) => { + let target = inspector.target; + if (ToolDefinitions.styleEditor.isTargetSupported(target)) { + gDevTools.showToolbox(target, "styleeditor").then(function (toolbox) { + let sheet = source || href; + toolbox.getCurrentPanel().selectStyleSheet(sheet, line, column); + }); + } + }); + } +}; + +function ComputedViewTool(inspector, window) { + this.inspector = inspector; + this.document = window.document; + + this.computedView = new CssComputedView(this.inspector, this.document, + this.inspector.pageStyle); + this.boxModelView = new BoxModelView(this.inspector, this.document); + + this.onSelected = this.onSelected.bind(this); + this.refresh = this.refresh.bind(this); + this.onPanelSelected = this.onPanelSelected.bind(this); + this.onMutations = this.onMutations.bind(this); + this.onResized = this.onResized.bind(this); + + this.inspector.selection.on("detached-front", this.onSelected); + this.inspector.selection.on("new-node-front", this.onSelected); + this.inspector.selection.on("pseudoclass", this.refresh); + this.inspector.sidebar.on("computedview-selected", this.onPanelSelected); + this.inspector.pageStyle.on("stylesheet-updated", this.refresh); + this.inspector.walker.on("mutations", this.onMutations); + this.inspector.walker.on("resize", this.onResized); + + this.computedView.selectElement(null); + + this.onSelected(); +} + +ComputedViewTool.prototype = { + isSidebarActive: function () { + if (!this.computedView) { + return false; + } + return this.inspector.sidebar.getCurrentTabID() == "computedview"; + }, + + onSelected: function (event) { + // Ignore the event if the view has been destroyed, or if it's inactive. + // But only if the current selection isn't null. If it's been set to null, + // let the update go through as this is needed to empty the view on + // navigation. + if (!this.computedView) { + return; + } + + let isInactive = !this.isSidebarActive() && + this.inspector.selection.nodeFront; + if (isInactive) { + return; + } + + this.computedView.setPageStyle(this.inspector.pageStyle); + + if (!this.inspector.selection.isConnected() || + !this.inspector.selection.isElementNode()) { + this.computedView.selectElement(null); + return; + } + + if (!event || event == "new-node-front") { + let done = this.inspector.updating("computed-view"); + this.computedView.selectElement(this.inspector.selection.nodeFront).then(() => { + done(); + }); + } + }, + + refresh: function () { + if (this.isSidebarActive()) { + this.computedView.refreshPanel(); + } + }, + + onPanelSelected: function () { + if (this.inspector.selection.nodeFront === this.computedView._viewedElement) { + this.refresh(); + } else { + this.onSelected(); + } + }, + + /** + * When markup mutations occur, if an attribute of the selected node changes, + * we need to refresh the view as that might change the node's styles. + */ + onMutations: function (mutations) { + for (let {type, target} of mutations) { + if (target === this.inspector.selection.nodeFront && + type === "attributes") { + this.refresh(); + break; + } + } + }, + + /** + * When the window gets resized, this may cause media-queries to match, and + * therefore, different styles may apply. + */ + onResized: function () { + this.refresh(); + }, + + destroy: function () { + this.inspector.walker.off("mutations", this.onMutations); + this.inspector.walker.off("resize", this.onResized); + 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.selection.off("detached-front", this.onSelected); + this.inspector.sidebar.off("computedview-selected", this.onPanelSelected); + if (this.inspector.pageStyle) { + this.inspector.pageStyle.off("stylesheet-updated", this.refresh); + } + + this.computedView.destroy(); + this.boxModelView.destroy(); + + this.computedView = this.boxModelView = this.document = this.inspector = null; + } +}; + +exports.CssComputedView = CssComputedView; +exports.ComputedViewTool = ComputedViewTool; +exports.PropertyView = PropertyView; diff --git a/devtools/client/inspector/computed/moz.build b/devtools/client/inspector/computed/moz.build new file mode 100644 index 000000000..5ce950325 --- /dev/null +++ b/devtools/client/inspector/computed/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DevToolsModules( + 'computed.js', +) + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/devtools/client/inspector/computed/test/.eslintrc.js b/devtools/client/inspector/computed/test/.eslintrc.js new file mode 100644 index 000000000..698ae9181 --- /dev/null +++ b/devtools/client/inspector/computed/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../../.eslintrc.mochitests.js" +}; diff --git a/devtools/client/inspector/computed/test/browser.ini b/devtools/client/inspector/computed/test/browser.ini new file mode 100644 index 000000000..33293e1eb --- /dev/null +++ b/devtools/client/inspector/computed/test/browser.ini @@ -0,0 +1,41 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + doc_matched_selectors.html + doc_media_queries.html + doc_pseudoelement.html + doc_sourcemaps.css + doc_sourcemaps.css.map + doc_sourcemaps.html + doc_sourcemaps.scss + head.js + !/devtools/client/commandline/test/helpers.js + !/devtools/client/framework/test/shared-head.js + !/devtools/client/inspector/test/head.js + !/devtools/client/inspector/test/shared-head.js + !/devtools/client/shared/test/test-actor.js + !/devtools/client/shared/test/test-actor-registry.js + +[browser_computed_browser-styles.js] +[browser_computed_cycle_color.js] +[browser_computed_getNodeInfo.js] +[browser_computed_keybindings_01.js] +[browser_computed_keybindings_02.js] +[browser_computed_matched-selectors-toggle.js] +[browser_computed_matched-selectors_01.js] +[browser_computed_matched-selectors_02.js] +[browser_computed_media-queries.js] +[browser_computed_no-results-placeholder.js] +[browser_computed_original-source-link.js] +[browser_computed_pseudo-element_01.js] +[browser_computed_refresh-on-style-change_01.js] +[browser_computed_search-filter.js] +[browser_computed_search-filter_clear.js] +[browser_computed_search-filter_context-menu.js] +subsuite = clipboard +[browser_computed_search-filter_escape-keypress.js] +[browser_computed_search-filter_noproperties.js] +[browser_computed_select-and-copy-styles.js] +subsuite = clipboard +[browser_computed_style-editor-link.js] diff --git a/devtools/client/inspector/computed/test/browser_computed_browser-styles.js b/devtools/client/inspector/computed/test/browser_computed_browser-styles.js new file mode 100644 index 000000000..32de63650 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_browser-styles.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"; + +// Tests that the checkbox to include browser styles works properly. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + yield selectNode("#matches", inspector); + + info("Checking the default styles"); + is(isPropertyVisible("color", view), true, + "span #matches color property is visible"); + is(isPropertyVisible("background-color", view), false, + "span #matches background-color property is hidden"); + + info("Toggling the browser styles"); + let doc = view.styleDocument; + let checkbox = doc.querySelector(".includebrowserstyles"); + let onRefreshed = inspector.once("computed-view-refreshed"); + checkbox.click(); + yield onRefreshed; + + info("Checking the browser styles"); + is(isPropertyVisible("color", view), true, + "span color property is visible"); + is(isPropertyVisible("background-color", view), true, + "span background-color property is visible"); +}); + +function isPropertyVisible(name, view) { + info("Checking property visibility for " + name); + let propertyViews = view.propertyViews; + for (let propView of propertyViews) { + if (propView.name == name) { + return propView.visible; + } + } + return false; +} diff --git a/devtools/client/inspector/computed/test/browser_computed_cycle_color.js b/devtools/client/inspector/computed/test/browser_computed_cycle_color.js new file mode 100644 index 000000000..c9892fafe --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_cycle_color.js @@ -0,0 +1,71 @@ +/* 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"; + +// Computed view color cycling test. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #f00; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + yield selectNode("#matches", inspector); + + info("Checking the property itself"); + let container = getComputedViewPropertyView(view, "color").valueNode; + checkColorCycling(container, view); + + info("Checking matched selectors"); + container = yield getComputedViewMatchedRules(view, "color"); + yield checkColorCycling(container, view); +}); + +function* checkColorCycling(container, view) { + let valueNode = container.querySelector(".computedview-color"); + let win = view.styleWindow; + + // "Authored" (default; currently the computed value) + is(valueNode.textContent, "rgb(255, 0, 0)", + "Color displayed as an RGB value."); + + let tests = [{ + value: "red", + comment: "Color displayed as a color name." + }, { + value: "#f00", + comment: "Color displayed as an authored value." + }, { + value: "hsl(0, 100%, 50%)", + comment: "Color displayed as an HSL value again." + }, { + value: "rgb(255, 0, 0)", + comment: "Color displayed as an RGB value again." + }]; + + for (let test of tests) { + yield checkSwatchShiftClick(container, win, test.value, test.comment); + } +} + +function* checkSwatchShiftClick(container, win, expectedValue, comment) { + let swatch = container.querySelector(".computedview-colorswatch"); + let valueNode = container.querySelector(".computedview-color"); + swatch.scrollIntoView(); + + let onUnitChange = swatch.once("unit-change"); + EventUtils.synthesizeMouseAtCenter(swatch, { + type: "mousedown", + shiftKey: true + }, win); + yield onUnitChange; + is(valueNode.textContent, expectedValue, comment); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_getNodeInfo.js b/devtools/client/inspector/computed/test/browser_computed_getNodeInfo.js new file mode 100644 index 000000000..30113e7ec --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_getNodeInfo.js @@ -0,0 +1,178 @@ +/* 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 various output of the computed-view's getNodeInfo method. +// This method is used by the HighlightersOverlay and TooltipsOverlay on mouseover to +// decide which highlighter or tooltip to show when hovering over a value/name/selector +// if any. +// +// For instance, browser_ruleview_selector-highlighter_01.js and +// browser_ruleview_selector-highlighter_02.js test that the selector +// highlighter appear when hovering over a selector in the rule-view. +// Since the code to make this work for the computed-view is 90% the same, +// there is no need for testing it again here. +// This test however serves as a unit test for getNodeInfo. + +const { + VIEW_NODE_SELECTOR_TYPE, + VIEW_NODE_PROPERTY_TYPE, + VIEW_NODE_VALUE_TYPE, + VIEW_NODE_IMAGE_URL_TYPE +} = require("devtools/client/inspector/shared/node-types"); + +const TEST_URI = ` + <style type="text/css"> + body { + background: red; + color: white; + } + div { + background: green; + } + div div { + background-color: yellow; + background-image: url(chrome://global/skin/icons/warning-64.png); + color: red; + } + </style> + <div><div id="testElement">Test element</div></div> +`; + +// Each item in this array must have the following properties: +// - desc {String} will be logged for information +// - getHoveredNode {Generator Function} received the computed-view instance as +// argument and must return the node to be tested +// - assertNodeInfo {Function} should check the validity of the nodeInfo +// argument it receives +const TEST_DATA = [ + { + desc: "Testing a null node", + getHoveredNode: function* () { + return null; + }, + assertNodeInfo: function (nodeInfo) { + is(nodeInfo, null); + } + }, + { + desc: "Testing a useless node", + getHoveredNode: function* (view) { + return view.element; + }, + assertNodeInfo: function (nodeInfo) { + is(nodeInfo, null); + } + }, + { + desc: "Testing a property name", + 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, "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, "rgb(255, 0, 0)"); + } + }, + { + desc: "Testing an image url", + getHoveredNode: function* (view) { + let {valueSpan} = getComputedViewProperty(view, "background-image"); + return valueSpan.querySelector(".theme-link"); + }, + assertNodeInfo: function (nodeInfo) { + is(nodeInfo.type, VIEW_NODE_IMAGE_URL_TYPE); + ok("property" in nodeInfo.value); + ok("value" in nodeInfo.value); + is(nodeInfo.value.property, "background-image"); + is(nodeInfo.value.value, + "url(\"chrome://global/skin/icons/warning-64.png\")"); + is(nodeInfo.value.url, "chrome://global/skin/icons/warning-64.png"); + } + }, + { + desc: "Testing a matched rule selector (bestmatch)", + getHoveredNode: function* (view) { + let el = yield getComputedViewMatchedRules(view, "background-color"); + return el.querySelector(".bestmatch"); + }, + assertNodeInfo: function (nodeInfo) { + is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE); + is(nodeInfo.value, "div div"); + } + }, + { + desc: "Testing a matched rule selector (matched)", + getHoveredNode: function* (view) { + let el = yield getComputedViewMatchedRules(view, "background-color"); + return el.querySelector(".matched"); + }, + assertNodeInfo: function (nodeInfo) { + is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE); + is(nodeInfo.value, "div"); + } + }, + { + desc: "Testing a matched rule selector (parentmatch)", + getHoveredNode: function* (view) { + let el = yield getComputedViewMatchedRules(view, "color"); + return el.querySelector(".parentmatch"); + }, + assertNodeInfo: function (nodeInfo) { + is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE); + is(nodeInfo.value, "body"); + } + }, + { + desc: "Testing a matched rule value", + getHoveredNode: function* (view) { + let el = yield getComputedViewMatchedRules(view, "color"); + return el.querySelector(".other-property-value"); + }, + assertNodeInfo: function (nodeInfo) { + is(nodeInfo.type, VIEW_NODE_VALUE_TYPE); + is(nodeInfo.value.property, "color"); + is(nodeInfo.value.value, "red"); + } + }, + { + desc: "Testing a matched rule stylesheet link", + getHoveredNode: function* (view) { + let el = yield getComputedViewMatchedRules(view, "color"); + return el.querySelector(".rule-link .theme-link"); + }, + assertNodeInfo: function (nodeInfo) { + is(nodeInfo, null); + } + } +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + yield selectNode("#testElement", inspector); + + for (let {desc, getHoveredNode, assertNodeInfo} of TEST_DATA) { + info(desc); + let nodeInfo = view.getNodeInfo(yield getHoveredNode(view)); + assertNodeInfo(nodeInfo); + } +}); diff --git a/devtools/client/inspector/computed/test/browser_computed_keybindings_01.js b/devtools/client/inspector/computed/test/browser_computed_keybindings_01.js new file mode 100644 index 000000000..199e125af --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_keybindings_01.js @@ -0,0 +1,83 @@ +/* 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 computed view key bindings. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + } + </style> + <span class="matches">Some styled text</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + yield selectNode(".matches", inspector); + + let propView = getFirstVisiblePropertyView(view); + let rulesTable = propView.matchedSelectorsContainer; + let matchedExpander = propView.element; + + info("Focusing the property"); + matchedExpander.scrollIntoView(); + let onMatchedExpanderFocus = once(matchedExpander, "focus", true); + EventUtils.synthesizeMouseAtCenter(matchedExpander, {}, view.styleWindow); + yield onMatchedExpanderFocus; + + yield checkToggleKeyBinding(view.styleWindow, "VK_SPACE", rulesTable, + inspector); + yield checkToggleKeyBinding(view.styleWindow, "VK_RETURN", rulesTable, + inspector); + yield checkHelpLinkKeybinding(view); +}); + +function getFirstVisiblePropertyView(view) { + let propView = null; + view.propertyViews.some(p => { + if (p.visible) { + propView = p; + return true; + } + return false; + }); + + return propView; +} + +function* checkToggleKeyBinding(win, key, rulesTable, inspector) { + info("Pressing " + key + " key a couple of times to check that the " + + "property gets expanded/collapsed"); + + let onExpand = inspector.once("computed-view-property-expanded"); + let onCollapse = inspector.once("computed-view-property-collapsed"); + + info("Expanding the property"); + EventUtils.synthesizeKey(key, {}, win); + yield onExpand; + isnot(rulesTable.innerHTML, "", "The property has been expanded"); + + info("Collapsing the property"); + EventUtils.synthesizeKey(key, {}, win); + yield onCollapse; + is(rulesTable.innerHTML, "", "The property has been collapsed"); +} + +function checkHelpLinkKeybinding(view) { + info("Check that MDN link is opened on \"F1\""); + let def = defer(); + + let propView = getFirstVisiblePropertyView(view); + propView.mdnLinkClick = function (event) { + ok(true, "Pressing F1 opened the MDN link"); + def.resolve(); + }; + + EventUtils.synthesizeKey("VK_F1", {}, view.styleWindow); + return def.promise; +} diff --git a/devtools/client/inspector/computed/test/browser_computed_keybindings_02.js b/devtools/client/inspector/computed/test/browser_computed_keybindings_02.js new file mode 100644 index 000000000..2a9220ec8 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_keybindings_02.js @@ -0,0 +1,66 @@ +/* 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 the computed-view keyboard navigation. + +const TEST_URI = ` + <style type="text/css"> + span { + font-variant: small-caps; + color: #000000; + } + .nomatches { + color: #ff0000; + } + </style> + <div id="first" style="margin: 10em; + font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA"> + <h1>Some header text</h1> + <p id="salutation" style="font-size: 12pt">hi.</p> + <p id="body" style="font-size: 12pt">I am a test-case. This text exists + solely to provide some things to <span style="color: yellow"> + highlight</span> and <span style="font-weight: bold">count</span> + style list-items in the box at right. If you are reading this, + you should go do something else instead. Maybe read a book. Or better + yet, write some test-cases for another bit of code. + <span style="font-style: italic">some text</span></p> + <p id="closing">more text</p> + <p>even more text</p> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + yield selectNode("span", inspector); + + info("Selecting the first computed style in the list"); + let firstStyle = view.styleDocument.querySelector(".property-view"); + ok(firstStyle, "First computed style found in panel"); + firstStyle.focus(); + + info("Tab to select the 2nd style and press return"); + let onExpanded = inspector.once("computed-view-property-expanded"); + EventUtils.synthesizeKey("VK_TAB", {}); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onExpanded; + + info("Verify the 2nd style has been expanded"); + let secondStyleSelectors = view.styleDocument.querySelectorAll( + ".property-content .matchedselectors")[1]; + ok(secondStyleSelectors.childNodes.length > 0, "Matched selectors expanded"); + + info("Tab back up and test the same thing, with space"); + onExpanded = inspector.once("computed-view-property-expanded"); + EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}); + EventUtils.synthesizeKey("VK_SPACE", {}); + yield onExpanded; + + info("Verify the 1st style has been expanded too"); + let firstStyleSelectors = view.styleDocument.querySelectorAll( + ".property-content .matchedselectors")[0]; + ok(firstStyleSelectors.childNodes.length > 0, "Matched selectors expanded"); +}); diff --git a/devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js b/devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js new file mode 100644 index 000000000..abbbb77be --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js @@ -0,0 +1,104 @@ +/* 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 computed view properties can be expanded and collapsed with +// either the twisty or by dbl-clicking on the container. + +const TEST_URI = ` + <style type="text/css"> , + html { color: #000000; font-size: 15pt; } + h1 { color: red; } + </style> + <h1>Some header text</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + yield selectNode("h1", inspector); + + yield testExpandOnTwistyClick(view, inspector); + yield testCollapseOnTwistyClick(view, inspector); + yield testExpandOnDblClick(view, inspector); + yield testCollapseOnDblClick(view, inspector); +}); + +function* testExpandOnTwistyClick({styleDocument, styleWindow}, inspector) { + info("Testing that a property expands on twisty click"); + + info("Getting twisty element"); + let twisty = styleDocument.querySelector("#propertyContainer .expandable"); + ok(twisty, "Twisty found"); + + let onExpand = inspector.once("computed-view-property-expanded"); + info("Clicking on the twisty element"); + twisty.click(); + + yield onExpand; + + // Expanded means the matchedselectors div is not empty + let div = styleDocument.querySelector(".property-content .matchedselectors"); + ok(div.childNodes.length > 0, + "Matched selectors are expanded on twisty click"); +} + +function* testCollapseOnTwistyClick({styleDocument, styleWindow}, inspector) { + info("Testing that a property collapses on twisty click"); + + info("Getting twisty element"); + let twisty = styleDocument.querySelector("#propertyContainer .expandable"); + ok(twisty, "Twisty found"); + + let onCollapse = inspector.once("computed-view-property-collapsed"); + info("Clicking on the twisty element"); + twisty.click(); + + yield onCollapse; + + // Collapsed means the matchedselectors div is empty + let div = styleDocument.querySelector(".property-content .matchedselectors"); + ok(div.childNodes.length === 0, + "Matched selectors are collapsed on twisty click"); +} + +function* testExpandOnDblClick({styleDocument, styleWindow}, inspector) { + info("Testing that a property expands on container dbl-click"); + + info("Getting computed property container"); + let container = styleDocument.querySelector(".property-view"); + ok(container, "Container found"); + + container.scrollIntoView(); + + let onExpand = inspector.once("computed-view-property-expanded"); + info("Dbl-clicking on the container"); + EventUtils.synthesizeMouseAtCenter(container, {clickCount: 2}, styleWindow); + + yield onExpand; + + // Expanded means the matchedselectors div is not empty + let div = styleDocument.querySelector(".property-content .matchedselectors"); + ok(div.childNodes.length > 0, "Matched selectors are expanded on dblclick"); +} + +function* testCollapseOnDblClick({styleDocument, styleWindow}, inspector) { + info("Testing that a property collapses on container dbl-click"); + + info("Getting computed property container"); + let container = styleDocument.querySelector(".property-view"); + ok(container, "Container found"); + + let onCollapse = inspector.once("computed-view-property-collapsed"); + info("Dbl-clicking on the container"); + EventUtils.synthesizeMouseAtCenter(container, {clickCount: 2}, styleWindow); + + yield onCollapse; + + // Collapsed means the matchedselectors div is empty + let div = styleDocument.querySelector(".property-content .matchedselectors"); + ok(div.childNodes.length === 0, + "Matched selectors are collapsed on dblclick"); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js new file mode 100644 index 000000000..66cabe7a9 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js @@ -0,0 +1,40 @@ +/* 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"; + +// Checking selector counts, matched rules and titles in the computed-view. + +const {PropertyView} = + require("devtools/client/inspector/computed/computed"); +const TEST_URI = URL_ROOT + "doc_matched_selectors.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openComputedView(); + + yield selectNode("#test", inspector); + yield testMatchedSelectors(view, inspector); +}); + +function* testMatchedSelectors(view, inspector) { + info("checking selector counts, matched rules and titles"); + + let nodeFront = yield getNodeFront("#test", inspector); + is(nodeFront, view._viewedElement, + "style inspector node matches the selected node"); + + let propertyView = new PropertyView(view, "color"); + propertyView.buildMain(); + propertyView.buildSelectorContainer(); + propertyView.matchedExpanded = true; + + yield propertyView.refreshMatchedSelectors(); + + let numMatchedSelectors = propertyView.matchedSelectors.length; + is(numMatchedSelectors, 6, + "CssLogic returns the correct number of matched selectors for div"); + is(propertyView.hasMatchedSelectors, true, + "hasMatchedSelectors returns true"); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_matched-selectors_02.js b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_02.js new file mode 100644 index 000000000..43172d55f --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_02.js @@ -0,0 +1,41 @@ +/* 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 for matched selector texts in the computed view. + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8,<div style='color:blue;'></div>"); + let {inspector, view} = yield openComputedView(); + yield selectNode("div", inspector); + + info("Checking the color property view"); + let propertyView = getPropertyView(view, "color"); + ok(propertyView, "found PropertyView for color"); + is(propertyView.hasMatchedSelectors, true, "hasMatchedSelectors is true"); + + info("Expanding the matched selectors"); + propertyView.matchedExpanded = true; + yield propertyView.refreshMatchedSelectors(); + + let span = propertyView.matchedSelectorsContainer + .querySelector("span.rule-text"); + ok(span, "Found the first table row"); + + let selector = propertyView.matchedSelectorViews[0]; + ok(selector, "Found the first matched selector view"); +}); + +function getPropertyView(computedView, name) { + let propertyView = null; + computedView.propertyViews.some(function (view) { + if (view.name == name) { + propertyView = view; + return true; + } + return false; + }); + return propertyView; +} diff --git a/devtools/client/inspector/computed/test/browser_computed_media-queries.js b/devtools/client/inspector/computed/test/browser_computed_media-queries.js new file mode 100644 index 000000000..79cccb49b --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_media-queries.js @@ -0,0 +1,36 @@ +/* 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 correctly display appropriate media query titles in the +// property view. + +const TEST_URI = URL_ROOT + "doc_media_queries.html"; + +var {PropertyView} = require("devtools/client/inspector/computed/computed"); + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openComputedView(); + yield selectNode("div", inspector); + yield checkPropertyView(view); +}); + +function checkPropertyView(view) { + let propertyView = new PropertyView(view, "width"); + propertyView.buildMain(); + propertyView.buildSelectorContainer(); + propertyView.matchedExpanded = true; + + return propertyView.refreshMatchedSelectors().then(() => { + let numMatchedSelectors = propertyView.matchedSelectors.length; + + is(numMatchedSelectors, 2, + "Property view has the correct number of matched selectors for div"); + + is(propertyView.hasMatchedSelectors, true, + "hasMatchedSelectors returns true"); + }); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_no-results-placeholder.js b/devtools/client/inspector/computed/test/browser_computed_no-results-placeholder.js new file mode 100644 index 000000000..b1371abd7 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_no-results-placeholder.js @@ -0,0 +1,70 @@ +/* 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 no results placeholder works properly. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + yield selectNode("#matches", inspector); + + yield enterInvalidFilter(inspector, view); + checkNoResultsPlaceholderShown(view); + + yield clearFilterText(inspector, view); + checkNoResultsPlaceholderHidden(view); +}); + +function* enterInvalidFilter(inspector, computedView) { + let searchbar = computedView.searchField; + let searchTerm = "xxxxx"; + + info("setting filter text to \"" + searchTerm + "\""); + + let onRefreshed = inspector.once("computed-view-refreshed"); + searchbar.focus(); + synthesizeKeys(searchTerm, computedView.styleWindow); + yield onRefreshed; +} + +function checkNoResultsPlaceholderShown(computedView) { + info("Checking that the no results placeholder is shown"); + + let placeholder = computedView.noResults; + let win = computedView.styleWindow; + let display = win.getComputedStyle(placeholder).display; + is(display, "block", "placeholder is visible"); +} + +function* clearFilterText(inspector, computedView) { + info("Clearing the filter text"); + + let searchbar = computedView.searchField; + + let onRefreshed = inspector.once("computed-view-refreshed"); + searchbar.focus(); + searchbar.value = ""; + EventUtils.synthesizeKey("c", {}, computedView.styleWindow); + yield onRefreshed; +} + +function checkNoResultsPlaceholderHidden(computedView) { + info("Checking that the no results placeholder is hidden"); + + let placeholder = computedView.noResults; + let win = computedView.styleWindow; + let display = win.getComputedStyle(placeholder).display; + is(display, "none", "placeholder is hidden"); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_original-source-link.js b/devtools/client/inspector/computed/test/browser_computed_original-source-link.js new file mode 100644 index 000000000..1bceed4e3 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_original-source-link.js @@ -0,0 +1,73 @@ +/* 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 computed view shows the original source link when source maps +// are enabled. + +const TESTCASE_URI = URL_ROOT_SSL + "doc_sourcemaps.html"; +const PREF = "devtools.styleeditor.source-maps-enabled"; +const SCSS_LOC = "doc_sourcemaps.scss:4"; +const CSS_LOC = "doc_sourcemaps.css:1"; + +add_task(function* () { + info("Turning the pref " + PREF + " on"); + Services.prefs.setBoolPref(PREF, true); + + yield addTab(TESTCASE_URI); + let {toolbox, inspector, view} = yield openComputedView(); + yield selectNode("div", inspector); + + info("Expanding the first property"); + yield expandComputedViewPropertyByIndex(view, 0); + + info("Verifying the link text"); + // Forcing a call to updateSourceLink on the SelectorView here. The + // computed-view already does it, but we have no way of waiting for it to be + // done here, so just call it again and wait for the returned promise to + // resolve. + let propertyView = getComputedViewPropertyView(view, "color"); + yield propertyView.matchedSelectorViews[0].updateSourceLink(); + verifyLinkText(view, SCSS_LOC); + + info("Toggling the pref"); + let onLinksUpdated = inspector.once("computed-view-sourcelinks-updated"); + Services.prefs.setBoolPref(PREF, false); + yield onLinksUpdated; + + info("Verifying that the link text has changed after the pref change"); + yield verifyLinkText(view, CSS_LOC); + + info("Toggling the pref again"); + onLinksUpdated = inspector.once("computed-view-sourcelinks-updated"); + Services.prefs.setBoolPref(PREF, true); + yield onLinksUpdated; + + info("Testing that clicking on the link works"); + yield testClickingLink(toolbox, view); + + info("Turning the pref " + PREF + " off"); + Services.prefs.clearUserPref(PREF); +}); + +function* testClickingLink(toolbox, view) { + let onEditor = waitForStyleEditor(toolbox, "doc_sourcemaps.scss"); + + info("Clicking the computedview stylesheet link"); + let link = getComputedViewLinkByIndex(view, 0); + link.scrollIntoView(); + link.click(); + + let editor = yield onEditor; + + let {line} = editor.sourceEditor.getCursor(); + is(line, 3, "cursor is at correct line number in original source"); +} + +function verifyLinkText(view, text) { + let link = getComputedViewLinkByIndex(view, 0); + is(link.textContent, text, + "Linked text changed to display the correct location"); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js b/devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js new file mode 100644 index 000000000..9ca5451a5 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js @@ -0,0 +1,39 @@ +/* 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 pseudoelements are displayed correctly in the rule view. + +const TEST_URI = URL_ROOT + "doc_pseudoelement.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openComputedView(); + yield testTopLeft(inspector, view); +}); + +function* testTopLeft(inspector, view) { + let node = yield getNodeFront("#topleft", inspector.markup); + yield selectNode(node, inspector); + let float = getComputedViewPropertyValue(view, "float"); + is(float, "left", "The computed view shows the correct float"); + + let children = yield inspector.markup.walker.children(node); + is(children.nodes.length, 3, "Element has correct number of children"); + + let beforeElement = children.nodes[0]; + yield selectNode(beforeElement, inspector); + let top = getComputedViewPropertyValue(view, "top"); + is(top, "0px", "The computed view shows the correct top"); + let left = getComputedViewPropertyValue(view, "left"); + is(left, "0px", "The computed view shows the correct left"); + + let afterElement = children.nodes[children.nodes.length - 1]; + yield selectNode(afterElement, inspector); + top = getComputedViewPropertyValue(view, "top"); + is(top, "50%", "The computed view shows the correct top"); + left = getComputedViewPropertyValue(view, "left"); + is(left, "50%", "The computed view shows the correct left"); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_refresh-on-style-change_01.js b/devtools/client/inspector/computed/test/browser_computed_refresh-on-style-change_01.js new file mode 100644 index 000000000..43f210307 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_refresh-on-style-change_01.js @@ -0,0 +1,30 @@ +/* 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 computed view refreshes when the current node has its style +// changed. + +const TEST_URI = "<div id='testdiv' style='font-size:10px;'>Test div!</div>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = yield openComputedView(); + yield selectNode("#testdiv", inspector); + + let fontSize = getComputedViewPropertyValue(view, "font-size"); + is(fontSize, "10px", "The computed view shows the right font-size"); + + info("Changing the node's style and waiting for the update"); + let onUpdated = inspector.once("computed-view-refreshed"); + yield testActor.setAttribute("#testdiv", "style", + "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, "rgb(255, 0, 0)", "The computed view also shows the color now"); +}); diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter.js b/devtools/client/inspector/computed/test/browser_computed_search-filter.js new file mode 100644 index 000000000..10ba82293 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter.js @@ -0,0 +1,66 @@ +/* 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 search filter works properly. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + yield selectNode("#matches", inspector); + yield testToggleDefaultStyles(inspector, view); + yield testAddTextInFilter(inspector, view); +}); + +function* testToggleDefaultStyles(inspector, computedView) { + info("checking \"Browser styles\" checkbox"); + let checkbox = computedView.includeBrowserStylesCheckbox; + let onRefreshed = inspector.once("computed-view-refreshed"); + checkbox.click(); + yield onRefreshed; +} + +function* testAddTextInFilter(inspector, computedView) { + info("setting filter text to \"color\""); + let doc = computedView.styleDocument; + let boxModelWrapper = doc.querySelector("#boxmodel-wrapper"); + let searchField = computedView.searchField; + let onRefreshed = inspector.once("computed-view-refreshed"); + let win = computedView.styleWindow; + + // First check to make sure that accel + F doesn't focus search if the + // container isn't focused + inspector.panelWin.focus(); + EventUtils.synthesizeKey("f", { accelKey: true }); + isnot(inspector.panelDoc.activeElement, searchField, + "Search field isn't focused"); + + computedView.element.focus(); + EventUtils.synthesizeKey("f", { accelKey: true }); + is(inspector.panelDoc.activeElement, searchField, "Search field is focused"); + + synthesizeKeys("color", win); + yield onRefreshed; + + ok(boxModelWrapper.hidden, "Box model is hidden"); + + info("check that the correct properties are visible"); + + let propertyViews = computedView.propertyViews; + propertyViews.forEach(propView => { + let name = propView.name; + is(propView.visible, name.indexOf("color") > -1, + "span " + name + " property visibility check"); + }); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js new file mode 100644 index 000000000..bd989854f --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js @@ -0,0 +1,71 @@ +/* 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 search filter clear button works properly. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + background-color: #00F; + border-color: #0F0; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + yield selectNode("#matches", inspector); + yield testAddTextInFilter(inspector, view); + yield testClearSearchFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, computedView) { + info("Setting filter text to \"background-color\""); + + let win = computedView.styleWindow; + let propertyViews = computedView.propertyViews; + let searchField = computedView.searchField; + + searchField.focus(); + synthesizeKeys("background-color", win); + yield inspector.once("computed-view-refreshed"); + + info("Check that the correct properties are visible"); + + propertyViews.forEach((propView) => { + let name = propView.name; + is(propView.visible, name.indexOf("background-color") > -1, + "span " + name + " property visibility check"); + }); +} + +function* testClearSearchFilter(inspector, computedView) { + info("Clearing the search filter"); + + let win = computedView.styleWindow; + let doc = computedView.styleDocument; + let boxModelWrapper = doc.querySelector("#boxmodel-wrapper"); + let propertyViews = computedView.propertyViews; + let searchField = computedView.searchField; + let searchClearButton = computedView.searchClearButton; + let onRefreshed = inspector.once("computed-view-refreshed"); + + EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win); + yield onRefreshed; + + ok(!boxModelWrapper.hidden, "Box model is displayed"); + + info("Check that the correct properties are visible"); + + ok(!searchField.value, "Search filter is cleared"); + propertyViews.forEach((propView) => { + is(propView.visible, propView.hasMatchedSelectors, + "span " + propView.name + " property visibility check"); + }); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js new file mode 100644 index 000000000..b5dbe4475 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js @@ -0,0 +1,84 @@ +/* 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 computed view search filter context menu works properly. + +const TEST_INPUT = "h1"; + +const TEST_URI = "<h1>test filter context menu</h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view} = yield openComputedView(); + yield selectNode("h1", inspector); + + let win = view.styleWindow; + let searchField = view.searchField; + let searchContextMenu = toolbox.textBoxContextMenuPopup; + ok(searchContextMenu, + "The search filter context menu is loaded in the computed view"); + + let cmdUndo = searchContextMenu.querySelector("[command=cmd_undo]"); + let cmdDelete = searchContextMenu.querySelector("[command=cmd_delete]"); + let cmdSelectAll = searchContextMenu.querySelector("[command=cmd_selectAll]"); + let cmdCut = searchContextMenu.querySelector("[command=cmd_cut]"); + let cmdCopy = searchContextMenu.querySelector("[command=cmd_copy]"); + let cmdPaste = searchContextMenu.querySelector("[command=cmd_paste]"); + + info("Opening context menu"); + + emptyClipboard(); + + let onFocus = once(searchField, "focus"); + searchField.focus(); + yield onFocus; + + let onContextMenuPopup = once(searchContextMenu, "popupshowing"); + EventUtils.synthesizeMouse(searchField, 2, 2, + {type: "contextmenu", button: 2}, win); + yield onContextMenuPopup; + + is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled"); + is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled"); + is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled"); + + // Cut/Copy items are enabled in context menu even if there + // is no selection. See also Bug 1303033 + is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled"); + is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled"); + + if (isWindows()) { + // emptyClipboard only works on Windows (666254), assert paste only for this OS. + is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled"); + } + + info("Closing context menu"); + let onContextMenuHidden = once(searchContextMenu, "popuphidden"); + searchContextMenu.hidePopup(); + yield onContextMenuHidden; + + info("Copy text in search field using the context menu"); + searchField.value = TEST_INPUT; + searchField.select(); + EventUtils.synthesizeMouse(searchField, 2, 2, + {type: "contextmenu", button: 2}, win); + yield onContextMenuPopup; + yield waitForClipboardPromise(() => cmdCopy.click(), TEST_INPUT); + searchContextMenu.hidePopup(); + yield onContextMenuHidden; + + info("Reopen context menu and check command properties"); + EventUtils.synthesizeMouse(searchField, 2, 2, + {type: "contextmenu", button: 2}, win); + yield onContextMenuPopup; + + is(cmdUndo.getAttribute("disabled"), "", "cmdUndo is enabled"); + is(cmdDelete.getAttribute("disabled"), "", "cmdDelete is enabled"); + is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled"); + is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled"); + is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled"); + is(cmdPaste.getAttribute("disabled"), "", "cmdPaste is enabled"); +}); diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_escape-keypress.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_escape-keypress.js new file mode 100644 index 000000000..e52e2cc89 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_escape-keypress.js @@ -0,0 +1,75 @@ +/* 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"; + +// Avoid test timeouts on Linux debug builds where the test takes just a bit too long to +// run (see bug 1258081). +requestLongerTimeout(2); + +// Tests that search filter escape keypress will clear the search field. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + yield selectNode("#matches", inspector); + yield testAddTextInFilter(inspector, view); + yield testEscapeKeypress(inspector, view); +}); + +function* testAddTextInFilter(inspector, computedView) { + info("Setting filter text to \"background-color\""); + + let win = computedView.styleWindow; + let propertyViews = computedView.propertyViews; + let searchField = computedView.searchField; + let checkbox = computedView.includeBrowserStylesCheckbox; + + info("Include browser styles"); + checkbox.click(); + yield inspector.once("computed-view-refreshed"); + + searchField.focus(); + synthesizeKeys("background-color", win); + yield inspector.once("computed-view-refreshed"); + + info("Check that the correct properties are visible"); + + propertyViews.forEach((propView) => { + let name = propView.name; + is(propView.visible, name.indexOf("background-color") > -1, + "span " + name + " property visibility check"); + }); +} + +function* testEscapeKeypress(inspector, computedView) { + info("Pressing the escape key on search filter"); + + let win = computedView.styleWindow; + let propertyViews = computedView.propertyViews; + let searchField = computedView.searchField; + let onRefreshed = inspector.once("computed-view-refreshed"); + + searchField.focus(); + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + yield onRefreshed; + + info("Check that the correct properties are visible"); + + ok(!searchField.value, "Search filter is cleared"); + propertyViews.forEach((propView) => { + let name = propView.name; + is(propView.visible, true, + "span " + name + " property is visible"); + }); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_noproperties.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_noproperties.js new file mode 100644 index 000000000..99ee6d58a --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_noproperties.js @@ -0,0 +1,61 @@ +/* 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 "no-results" message is displayed when selecting an invalid element or +// when all properties have been filtered out. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + background-color: #00F; + border-color: #0F0; + } + </style> + <div> + <!-- comment node --> + <span id="matches" class="matches">Some styled text</span> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + let propertyViews = view.propertyViews; + + info("Select the #matches node"); + let matchesNode = yield getNodeFront("#matches", inspector); + let onRefresh = inspector.once("computed-view-refreshed"); + yield selectNode(matchesNode, inspector); + yield onRefresh; + + ok(propertyViews.filter(p => p.visible).length > 0, "CSS properties are displayed"); + ok(view.noResults.hasAttribute("hidden"), "no-results message is hidden"); + + info("Select a comment node"); + let commentNode = yield inspector.walker.previousSibling(matchesNode); + yield selectNode(commentNode, inspector); + + is(propertyViews.filter(p => p.visible).length, 0, "No properties displayed"); + ok(!view.noResults.hasAttribute("hidden"), "no-results message is displayed"); + + info("Select the #matches node again"); + onRefresh = inspector.once("computed-view-refreshed"); + yield selectNode(matchesNode, inspector); + yield onRefresh; + + ok(propertyViews.filter(p => p.visible).length > 0, "CSS properties are displayed"); + ok(view.noResults.hasAttribute("hidden"), "no-results message is hidden"); + + info("Filter by 'will-not-match' and check the no-results message is displayed"); + let searchField = view.searchField; + searchField.focus(); + synthesizeKeys("will-not-match", view.styleWindow); + yield inspector.once("computed-view-refreshed"); + + is(propertyViews.filter(p => p.visible).length, 0, "No properties displayed"); + ok(!view.noResults.hasAttribute("hidden"), "no-results message is displayed"); +}); diff --git a/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles.js b/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles.js new file mode 100644 index 000000000..ce8be59ad --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles.js @@ -0,0 +1,118 @@ +/* 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 properties can be selected and copied from the computed view. + +const osString = Services.appinfo.OS; + +const TEST_URI = ` + <style type="text/css"> + span { + font-variant-caps: small-caps; + color: #000000; + } + .nomatches { + color: #ff0000; + } + </style> + <div id="first" style="margin: 10em; + font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA"> + <h1>Some header text</h1> + <p id="salutation" style="font-size: 12pt">hi.</p> + <p id="body" style="font-size: 12pt">I am a test-case. This text exists + solely to provide some things to <span style="color: yellow"> + highlight</span> and <span style="font-weight: bold">count</span> + style list-items in the box at right. If you are reading this, + you should go do something else instead. Maybe read a book. Or better + yet, write some test-cases for another bit of code. + <span style="font-style: italic">some text</span></p> + <p id="closing">more text</p> + <p>even more text</p> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + yield selectNode("span", inspector); + yield checkCopySelection(view); + yield checkSelectAll(view); +}); + +function* checkCopySelection(view) { + info("Testing selection copy"); + + let contentDocument = view.styleDocument; + let props = contentDocument.querySelectorAll(".property-view"); + ok(props, "captain, we have the property-view nodes"); + + let range = contentDocument.createRange(); + range.setStart(props[1], 0); + range.setEnd(props[3], 2); + contentDocument.defaultView.getSelection().addRange(range); + + info("Checking that cssHtmlTree.siBoundCopy() returns the correct " + + "clipboard value"); + + let expectedPattern = "font-family: helvetica,sans-serif;[\\r\\n]+" + + "font-size: 16px;[\\r\\n]+" + + "font-variant-caps: small-caps;[\\r\\n]*"; + + try { + yield waitForClipboardPromise(() => fireCopyEvent(props[0]), + () => checkClipboardData(expectedPattern)); + } catch (e) { + failedClipboard(expectedPattern); + } +} + +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: 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]*"; + + try { + yield waitForClipboardPromise(() => fireCopyEvent(prop), + () => checkClipboardData(expectedPattern)); + } catch (e) { + failedClipboard(expectedPattern); + } +} + +function checkClipboardData(expectedPattern) { + let actual = SpecialPowers.getClipboardData("text/unicode"); + let expectedRegExp = new RegExp(expectedPattern, "g"); + return expectedRegExp.test(actual); +} + +function failedClipboard(expectedPattern) { + // Format expected text for comparison + let terminator = osString == "WINNT" ? "\r\n" : "\n"; + expectedPattern = expectedPattern.replace(/\[\\r\\n\][+*]/g, terminator); + expectedPattern = expectedPattern.replace(/\\\(/g, "("); + expectedPattern = expectedPattern.replace(/\\\)/g, ")"); + + let actual = SpecialPowers.getClipboardData("text/unicode"); + + // Trim the right hand side of our strings. This is because expectedPattern + // accounts for windows sometimes adding a newline to our copied data. + expectedPattern = expectedPattern.trimRight(); + actual = actual.trimRight(); + + dump("TEST-UNEXPECTED-FAIL | Clipboard text does not match expected ... " + + "results (escaped for accurate comparison):\n"); + info("Actual: " + escape(actual)); + info("Expected: " + escape(expectedPattern)); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_style-editor-link.js b/devtools/client/inspector/computed/test/browser_computed_style-editor-link.js new file mode 100644 index 000000000..6a95fd83f --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_style-editor-link.js @@ -0,0 +1,142 @@ +/* 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"; + +// Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejection should be fixed. +thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source"); + +// Tests the links from the computed view to the style editor. + +const STYLESHEET_URL = "data:text/css," + encodeURIComponent( + ".highlight {color: blue}"); + +const DOCUMENT_URL = "data:text/html;charset=utf-8," + encodeURIComponent( + `<html> + <head> + <title>Computed view style editor link test</title> + <style type="text/css"> + html { color: #000000; } + span { font-variant: small-caps; color: #000000; } + .nomatches {color: #ff0000;}</style> <div id="first" style="margin: 10em; + font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA"> + </style> + <style> + div { color: #f06; } + </style> + <link rel="stylesheet" type="text/css" href="${STYLESHEET_URL}"> + </head> + <body> + <h1>Some header text</h1> + <p id="salutation" style="font-size: 12pt">hi.</p> + <p id="body" style="font-size: 12pt">I am a test-case. This text exists + solely to provide some things to + <span style="color: yellow" class="highlight"> + highlight</span> and <span style="font-weight: bold">count</span> + style list-items in the box at right. If you are reading this, + you should go do something else instead. Maybe read a book. Or better + yet, write some test-cases for another bit of code. + <span style="font-style: italic">some text</span></p> + <p id="closing">more text</p> + <p>even more text</p> + </div> + </body> + </html>`); + +add_task(function* () { + yield addTab(DOCUMENT_URL); + let {toolbox, inspector, view, testActor} = yield openComputedView(); + yield selectNode("span", inspector); + + yield testInlineStyle(view); + yield testFirstInlineStyleSheet(view, toolbox, testActor); + yield testSecondInlineStyleSheet(view, toolbox, testActor); + yield testExternalStyleSheet(view, toolbox, testActor); +}); + +function* testInlineStyle(view) { + info("Testing inline style"); + + yield expandComputedViewPropertyByIndex(view, 0); + + let onTab = waitForTab(); + info("Clicking on the first rule-link in the computed-view"); + clickLinkByIndex(view, 0); + + let tab = yield onTab; + + let tabURI = tab.linkedBrowser.documentURI.spec; + ok(tabURI.startsWith("view-source:"), "View source tab is open"); + info("Closing tab"); + gBrowser.removeTab(tab); +} + +function* testFirstInlineStyleSheet(view, toolbox, testActor) { + info("Testing inline stylesheet"); + + info("Listening for toolbox switch to the styleeditor"); + let onSwitch = waitForStyleEditor(toolbox); + + info("Clicking an inline stylesheet"); + clickLinkByIndex(view, 2); + let editor = yield onSwitch; + + ok(true, "Switched to the style-editor panel in the toolbox"); + + yield validateStyleEditorSheet(editor, 0, testActor); +} + +function* testSecondInlineStyleSheet(view, toolbox, testActor) { + info("Testing second inline stylesheet"); + + info("Waiting for the stylesheet editor to be selected"); + let panel = toolbox.getCurrentPanel(); + let onSelected = panel.UI.once("editor-selected"); + + info("Switching back to the inspector panel in the toolbox"); + yield toolbox.selectTool("inspector"); + + info("Clicking on second inline stylesheet link"); + clickLinkByIndex(view, 4); + let editor = yield onSelected; + + is(toolbox.currentToolId, "styleeditor", + "The style editor is selected again"); + yield validateStyleEditorSheet(editor, 1, testActor); +} + +function* testExternalStyleSheet(view, toolbox, testActor) { + info("Testing external stylesheet"); + + info("Waiting for the stylesheet editor to be selected"); + let panel = toolbox.getCurrentPanel(); + let onSelected = panel.UI.once("editor-selected"); + + info("Switching back to the inspector panel in the toolbox"); + yield toolbox.selectTool("inspector"); + + info("Clicking on an external stylesheet link"); + clickLinkByIndex(view, 1); + let editor = yield onSelected; + + is(toolbox.currentToolId, "styleeditor", + "The style editor is selected again"); + yield validateStyleEditorSheet(editor, 2, testActor); +} + +function* validateStyleEditorSheet(editor, expectedSheetIndex, testActor) { + info("Validating style editor stylesheet"); + let expectedHref = yield testActor.eval(` + document.styleSheets[${expectedSheetIndex}].href; + `); + is(editor.styleSheet.href, expectedHref, + "loaded stylesheet matches document stylesheet"); +} + +function clickLinkByIndex(view, index) { + let link = getComputedViewLinkByIndex(view, index); + link.scrollIntoView(); + link.click(); +} diff --git a/devtools/client/inspector/computed/test/doc_matched_selectors.html b/devtools/client/inspector/computed/test/doc_matched_selectors.html new file mode 100644 index 000000000..8fe007409 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_matched_selectors.html @@ -0,0 +1,28 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> + <head> + <style> + .matched1, .matched2, .matched3, .matched4, .matched5 { + color: #000; + } + + div { + position: absolute; + top: 40px; + left: 20px; + border: 1px solid #000; + color: #111; + width: 100px; + height: 50px; + } + </style> + </head> + <body> + inspectstyle($("test")); + <div id="test" class="matched1 matched2 matched3 matched4 matched5">Test div</div> + <div id="dummy"> + <div></div> + </div> + </body> +</html> diff --git a/devtools/client/inspector/computed/test/doc_media_queries.html b/devtools/client/inspector/computed/test/doc_media_queries.html new file mode 100644 index 000000000..819e1ea7a --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_media_queries.html @@ -0,0 +1,21 @@ +<html> +<head> + <title>test</title> + <style> + div { + width: 1000px; + height: 100px; + background-color: #f00; + } + + @media screen and (min-width: 1px) { + div { + width: 200px; + } + } + </style> +</head> +<body> +<div></div> +</body> +</html> diff --git a/devtools/client/inspector/computed/test/doc_pseudoelement.html b/devtools/client/inspector/computed/test/doc_pseudoelement.html new file mode 100644 index 000000000..6145d4bf1 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_pseudoelement.html @@ -0,0 +1,131 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> + <head> + <style> + +body { + color: #333; +} + +.box { + float:left; + width: 128px; + height: 128px; + background: #ddd; + padding: 32px; + margin: 32px; + position:relative; +} + +.box:first-line { + color: orange; + background: red; +} + +.box:first-letter { + color: green; +} + +* { + cursor: default; +} + +nothing { + cursor: pointer; +} + +p::-moz-selection { + color: white; + background: black; +} +p::selection { + color: white; + background: black; +} + +p:first-line { + background: blue; +} +p:first-letter { + color: red; + font-size: 130%; +} + +.box:before { + background: green; + content: " "; + position: absolute; + height:32px; + width:32px; +} + +.box:after { + background: red; + content: " "; + position: absolute; + border-radius: 50%; + height:32px; + width:32px; + top: 50%; + left: 50%; + margin-top: -16px; + margin-left: -16px; +} + +.topleft:before { + top:0; + left:0; +} + +.topleft:first-line { + color: orange; +} +.topleft::selection { + color: orange; +} + +.topright:before { + top:0; + right:0; +} + +.bottomright:before { + bottom:10px; + right:10px; + color: red; +} + +.bottomright:before { + bottom:0; + right:0; +} + +.bottomleft:before { + bottom:0; + left:0; +} + + </style> + </head> + <body> + <h1>ruleview pseudoelement($("test"));</h1> + + <div id="topleft" class="box topleft"> + <p>Top Left<br />Position</p> + </div> + + <div id="topright" class="box topright"> + <p>Top Right<br />Position</p> + </div> + + <div id="bottomright" class="box bottomright"> + <p>Bottom Right<br />Position</p> + </div> + + <div id="bottomleft" class="box bottomleft"> + <p>Bottom Left<br />Position</p> + </div> + + </body> +</html> diff --git a/devtools/client/inspector/computed/test/doc_sourcemaps.css b/devtools/client/inspector/computed/test/doc_sourcemaps.css new file mode 100644 index 000000000..a9b437a40 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_sourcemaps.css @@ -0,0 +1,7 @@ +div { + color: #ff0066; } + +span { + background-color: #EEE; } + +/*# sourceMappingURL=doc_sourcemaps.css.map */
\ No newline at end of file diff --git a/devtools/client/inspector/computed/test/doc_sourcemaps.css.map b/devtools/client/inspector/computed/test/doc_sourcemaps.css.map new file mode 100644 index 000000000..0f7486fd9 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_sourcemaps.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAGA,GAAI;EACF,KAAK,EAHU,OAAI;;AAMrB,IAAK;EACH,gBAAgB,EAAE,IAAI", +"sources": ["doc_sourcemaps.scss"], +"names": [], +"file": "doc_sourcemaps.css" +} diff --git a/devtools/client/inspector/computed/test/doc_sourcemaps.html b/devtools/client/inspector/computed/test/doc_sourcemaps.html new file mode 100644 index 000000000..0014e55fe --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_sourcemaps.html @@ -0,0 +1,11 @@ +<!doctype html> +<html> +<head> + <title>testcase for testing CSS source maps</title> + <link rel="stylesheet" type="text/css" href="simple.css"/> + <link rel="stylesheet" type="text/css" href="doc_sourcemaps.css"/> +</head> +<body> + <div>source maps <span>testcase</span></div> +</body> +</html> diff --git a/devtools/client/inspector/computed/test/doc_sourcemaps.scss b/devtools/client/inspector/computed/test/doc_sourcemaps.scss new file mode 100644 index 000000000..0ff6c471b --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_sourcemaps.scss @@ -0,0 +1,10 @@ + +$paulrougetpink: #f06; + +div { + color: $paulrougetpink; +} + +span { + background-color: #EEE; +}
\ No newline at end of file diff --git a/devtools/client/inspector/computed/test/head.js b/devtools/client/inspector/computed/test/head.js new file mode 100644 index 000000000..17c47be1a --- /dev/null +++ b/devtools/client/inspector/computed/test/head.js @@ -0,0 +1,157 @@ +/* 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/ */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from ../../test/head.js */ +"use strict"; + +// Import the inspector's head.js first (which itself imports shared-head.js). +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js", + this); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.defaultColorUnit"); +}); + +/** + * Dispatch the copy event on the given element + */ +function fireCopyEvent(element) { + let evt = element.ownerDocument.createEvent("Event"); + evt.initEvent("copy", true, true); + element.dispatchEvent(evt); +} + +/** + * Get references to the name and value span nodes corresponding to a given + * property name in the computed-view + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {String} name + * The name of the property to retrieve + * @return an object {nameSpan, valueSpan} + */ +function getComputedViewProperty(view, name) { + let prop; + for (let property of view.styleDocument.querySelectorAll(".property-view")) { + let nameSpan = property.querySelector(".property-name"); + let valueSpan = property.querySelector(".property-value"); + + if (nameSpan.textContent === name) { + prop = {nameSpan: nameSpan, valueSpan: valueSpan}; + break; + } + } + return prop; +} + +/** + * Get an instance of PropertyView from the computed-view. + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {String} name + * The name of the property to retrieve + * @return {PropertyView} + */ +function getComputedViewPropertyView(view, name) { + let propView; + for (let propertyView of view.propertyViews) { + if (propertyView._propertyInfo.name === name) { + propView = propertyView; + break; + } + } + return propView; +} + +/** + * Get a reference to the property-content element for a given property name in + * the computed-view. + * A property-content element always follows (nextSibling) the property itself + * and is only shown when the twisty icon is expanded on the property. + * A property-content element contains matched rules, with selectors, + * properties, values and stylesheet links + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {String} name + * The name of the property to retrieve + * @return {Promise} A promise that resolves to the property matched rules + * container + */ +var getComputedViewMatchedRules = Task.async(function* (view, name) { + let expander; + let propertyContent; + for (let property of view.styleDocument.querySelectorAll(".property-view")) { + let nameSpan = property.querySelector(".property-name"); + if (nameSpan.textContent === name) { + expander = property.querySelector(".expandable"); + propertyContent = property.nextSibling; + break; + } + } + + if (!expander.hasAttribute("open")) { + // Need to expand the property + let onExpand = view.inspector.once("computed-view-property-expanded"); + expander.click(); + yield onExpand; + } + + return propertyContent; +}); + +/** + * Get the text value of the property corresponding to a given name in the + * computed-view + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {String} name + * The name of the property to retrieve + * @return {String} The property value + */ +function getComputedViewPropertyValue(view, name, propertyName) { + return getComputedViewProperty(view, name, propertyName) + .valueSpan.textContent; +} + +/** + * Expand a given property, given its index in the current property list of + * the computed view + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {Number} index + * The index of the property to be expanded + * @return a promise that resolves when the property has been expanded, or + * rejects if the property was not found + */ +function expandComputedViewPropertyByIndex(view, index) { + info("Expanding property " + index + " in the computed view"); + let expandos = view.styleDocument.querySelectorAll("#propertyContainer .expandable"); + if (!expandos.length || !expandos[index]) { + return promise.reject(); + } + + let onExpand = view.inspector.once("computed-view-property-expanded"); + expandos[index].click(); + return onExpand; +} + +/** + * Get a rule-link from the computed-view given its index + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {Number} index + * The index of the link to be retrieved + * @return {DOMNode} The link at the given index, if one exists, null otherwise + */ +function getComputedViewLinkByIndex(view, index) { + let links = view.styleDocument.querySelectorAll(".rule-link .link"); + return links[index]; +} diff --git a/devtools/client/inspector/fonts/fonts.js b/devtools/client/inspector/fonts/fonts.js new file mode 100644 index 000000000..b0087e9f6 --- /dev/null +++ b/devtools/client/inspector/fonts/fonts.js @@ -0,0 +1,250 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {gDevTools} = require("devtools/client/framework/devtools"); + +const DEFAULT_PREVIEW_TEXT = "Abc"; +const PREVIEW_UPDATE_DELAY = 150; + +const {Task} = require("devtools/shared/task"); +const {getColor} = require("devtools/client/shared/theme"); + +function FontInspector(inspector, window) { + this.inspector = inspector; + this.pageStyle = this.inspector.pageStyle; + this.chromeDoc = window.document; + this.init(); +} + +FontInspector.prototype = { + init: function () { + this.update = this.update.bind(this); + this.onNewNode = this.onNewNode.bind(this); + this.onThemeChanged = this.onThemeChanged.bind(this); + this.inspector.selection.on("new-node-front", this.onNewNode); + this.inspector.sidebar.on("fontinspector-selected", this.onNewNode); + this.showAll = this.showAll.bind(this); + this.showAllLink = this.chromeDoc.getElementById("font-showall"); + this.showAllLink.addEventListener("click", this.showAll); + this.previewTextChanged = this.previewTextChanged.bind(this); + this.previewInput = this.chromeDoc.getElementById("font-preview-text-input"); + this.previewInput.addEventListener("input", this.previewTextChanged); + this.previewInput.addEventListener("contextmenu", + this.inspector.onTextBoxContextMenu); + + // Listen for theme changes as the color of the previews depend on the theme + gDevTools.on("theme-switched", this.onThemeChanged); + + this.update(); + }, + + /** + * Is the fontinspector visible in the sidebar? + */ + isActive: function () { + return this.inspector.sidebar && + this.inspector.sidebar.getCurrentTabID() == "fontinspector"; + }, + + /** + * Remove listeners. + */ + destroy: function () { + this.chromeDoc = null; + this.inspector.sidebar.off("fontinspector-selected", this.onNewNode); + this.inspector.selection.off("new-node-front", this.onNewNode); + this.showAllLink.removeEventListener("click", this.showAll); + this.previewInput.removeEventListener("input", this.previewTextChanged); + this.previewInput.removeEventListener("contextmenu", + this.inspector.onTextBoxContextMenu); + + gDevTools.off("theme-switched", this.onThemeChanged); + + if (this._previewUpdateTimeout) { + clearTimeout(this._previewUpdateTimeout); + } + }, + + /** + * Selection 'new-node' event handler. + */ + onNewNode: function () { + if (this.isActive() && + this.inspector.selection.isConnected() && + this.inspector.selection.isElementNode()) { + this.undim(); + this.update(); + } else { + this.dim(); + } + }, + + /** + * The text to use for previews. Returns either the value user has typed to + * the preview input or DEFAULT_PREVIEW_TEXT if the input is empty or contains + * only whitespace. + */ + getPreviewText: function () { + let inputText = this.previewInput.value.trim(); + if (inputText === "") { + return DEFAULT_PREVIEW_TEXT; + } + + return inputText; + }, + + /** + * Preview input 'input' event handler. + */ + previewTextChanged: function () { + if (this._previewUpdateTimeout) { + clearTimeout(this._previewUpdateTimeout); + } + + this._previewUpdateTimeout = setTimeout(() => { + this.update(this._lastUpdateShowedAllFonts); + }, PREVIEW_UPDATE_DELAY); + }, + + /** + * Callback for the theme-switched event. + */ + onThemeChanged: function (event, frame) { + if (frame === this.chromeDoc.defaultView) { + this.update(this._lastUpdateShowedAllFonts); + } + }, + + /** + * Hide the font list. No node are selected. + */ + dim: function () { + let panel = this.chromeDoc.getElementById("sidebar-panel-fontinspector"); + panel.classList.add("dim"); + this.clear(); + }, + + /** + * Show the font list. A node is selected. + */ + undim: function () { + let panel = this.chromeDoc.getElementById("sidebar-panel-fontinspector"); + panel.classList.remove("dim"); + }, + + /** + * Clears the font list. + */ + clear: function () { + this.chromeDoc.querySelector("#all-fonts").innerHTML = ""; + }, + + /** + * Retrieve all the font info for the selected node and display it. + */ + update: Task.async(function* (showAllFonts) { + let node = this.inspector.selection.nodeFront; + let panel = this.chromeDoc.getElementById("sidebar-panel-fontinspector"); + + if (!node || + !this.isActive() || + !this.inspector.selection.isConnected() || + !this.inspector.selection.isElementNode() || + panel.classList.contains("dim")) { + return; + } + + this._lastUpdateShowedAllFonts = showAllFonts; + + let options = { + includePreviews: true, + previewText: this.getPreviewText(), + previewFillStyle: getColor("body-color") + }; + + let fonts = []; + if (showAllFonts) { + fonts = yield this.pageStyle.getAllUsedFontFaces(options) + .then(null, console.error); + } else { + fonts = yield this.pageStyle.getUsedFontFaces(node, options) + .then(null, console.error); + } + + if (!fonts || !fonts.length) { + // No fonts to display. Clear the previously shown fonts. + this.clear(); + return; + } + + for (let font of fonts) { + font.previewUrl = yield font.preview.data.string(); + } + + // in case we've been destroyed in the meantime + if (!this.chromeDoc) { + return; + } + + // Make room for the new fonts. + this.clear(); + + for (let font of fonts) { + this.render(font); + } + + this.inspector.emit("fontinspector-updated"); + }), + + /** + * Display the information of one font. + */ + render: function (font) { + let s = this.chromeDoc.querySelector("#font-template > section"); + s = s.cloneNode(true); + + s.querySelector(".font-name").textContent = font.name; + s.querySelector(".font-css-name").textContent = font.CSSFamilyName; + + if (font.URI) { + s.classList.add("is-remote"); + } else { + s.classList.add("is-local"); + } + + let formatElem = s.querySelector(".font-format"); + if (font.format) { + formatElem.textContent = font.format; + } else { + formatElem.hidden = true; + } + + s.querySelector(".font-url").value = font.URI; + + if (font.rule) { + // This is the @font-face{…} code. + let cssText = font.ruleText; + + s.classList.add("has-code"); + s.querySelector(".font-css-code").textContent = cssText; + } + let preview = s.querySelector(".font-preview"); + preview.src = font.previewUrl; + + this.chromeDoc.querySelector("#all-fonts").appendChild(s); + }, + + /** + * Show all fonts for the document (including iframes) + */ + showAll: function () { + this.update(true); + }, +}; + +exports.FontInspector = FontInspector; diff --git a/devtools/client/inspector/fonts/moz.build b/devtools/client/inspector/fonts/moz.build new file mode 100644 index 000000000..a66982b71 --- /dev/null +++ b/devtools/client/inspector/fonts/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DevToolsModules( + 'fonts.js', +) + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/devtools/client/inspector/fonts/test/.eslintrc.js b/devtools/client/inspector/fonts/test/.eslintrc.js new file mode 100644 index 000000000..698ae9181 --- /dev/null +++ b/devtools/client/inspector/fonts/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../../.eslintrc.mochitests.js" +}; diff --git a/devtools/client/inspector/fonts/test/OstrichLicense.txt b/devtools/client/inspector/fonts/test/OstrichLicense.txt new file mode 100644 index 000000000..14c043d60 --- /dev/null +++ b/devtools/client/inspector/fonts/test/OstrichLicense.txt @@ -0,0 +1,41 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the copyright statement(s). + +"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
\ No newline at end of file diff --git a/devtools/client/inspector/fonts/test/browser.ini b/devtools/client/inspector/fonts/test/browser.ini new file mode 100644 index 000000000..99b00231d --- /dev/null +++ b/devtools/client/inspector/fonts/test/browser.ini @@ -0,0 +1,20 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + browser_fontinspector.html + test_iframe.html + ostrich-black.ttf + ostrich-regular.ttf + head.js + !/devtools/client/commandline/test/helpers.js + !/devtools/client/framework/test/shared-head.js + !/devtools/client/inspector/test/head.js + !/devtools/client/inspector/test/shared-head.js + !/devtools/client/shared/test/test-actor.js + !/devtools/client/shared/test/test-actor-registry.js + +[browser_fontinspector.js] +[browser_fontinspector_edit-previews.js] +[browser_fontinspector_edit-previews-show-all.js] +[browser_fontinspector_theme-change.js] diff --git a/devtools/client/inspector/fonts/test/browser_fontinspector.html b/devtools/client/inspector/fonts/test/browser_fontinspector.html new file mode 100644 index 000000000..009b2f087 --- /dev/null +++ b/devtools/client/inspector/fonts/test/browser_fontinspector.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> + +<style> + @font-face { + font-family: bar; + src: url(bad/font/name.ttf), url(ostrich-regular.ttf) format("truetype"); + } + @font-face { + font-family: barnormal; + font-weight: normal; + src: url(ostrich-regular.ttf); + } + @font-face { + font-family: bar; + font-weight: bold; + src: url(ostrich-black.ttf); + } + @font-face { + font-family: bar; + font-weight: 800; + src: url(ostrich-black.ttf); + } + body{ + font-family:Arial; + font-size: 36px; + } + div { + font-family:Arial; + font-family:bar; + } + .normal-text { + font-family: barnormal; + font-weight: normal; + } + .bold-text { + font-family: bar; + font-weight: bold; + } + .black-text { + font-family: bar; + font-weight: 800; + } +</style> + +<body> + BODY + <div>DIV</div> + <iframe src="test_iframe.html"></iframe> + <div class="normal-text">NORMAL DIV</div> + <div class="bold-text">BOLD DIV</div> + <div class="black-text">800 DIV</div> +</body> diff --git a/devtools/client/inspector/fonts/test/browser_fontinspector.js b/devtools/client/inspector/fonts/test/browser_fontinspector.js new file mode 100644 index 000000000..a36c57771 --- /dev/null +++ b/devtools/client/inspector/fonts/test/browser_fontinspector.js @@ -0,0 +1,108 @@ +/* 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"; + +requestLongerTimeout(2); + +const TEST_URI = URL_ROOT + "browser_fontinspector.html"; +const FONTS = [{ + name: "Ostrich Sans Medium", + remote: true, + url: URL_ROOT + "ostrich-regular.ttf", + format: "truetype", + cssName: "bar" +}, { + name: "Ostrich Sans Black", + remote: true, + url: URL_ROOT + "ostrich-black.ttf", + format: "", + cssName: "bar" +}, { + name: "Ostrich Sans Black", + remote: true, + url: URL_ROOT + "ostrich-black.ttf", + format: "", + cssName: "bar" +}, { + name: "Ostrich Sans Medium", + remote: true, + url: URL_ROOT + "ostrich-regular.ttf", + format: "", + cssName: "barnormal" +}]; + +add_task(function* () { + let { inspector, view } = yield openFontInspectorForURL(TEST_URI); + ok(!!view, "Font inspector document is alive."); + + let viewDoc = view.chromeDoc; + + yield testBodyFonts(inspector, viewDoc); + yield testDivFonts(inspector, viewDoc); + yield testShowAllFonts(inspector, viewDoc); +}); + +function* testBodyFonts(inspector, viewDoc) { + let s = viewDoc.querySelectorAll("#all-fonts > section"); + is(s.length, 5, "Found 5 fonts"); + + for (let i = 0; i < FONTS.length; i++) { + let section = s[i]; + let font = FONTS[i]; + is(section.querySelector(".font-name").textContent, font.name, + "font " + i + " right font name"); + is(section.classList.contains("is-remote"), font.remote, + "font " + i + " remote value correct"); + is(section.querySelector(".font-url").value, font.url, + "font " + i + " url correct"); + is(section.querySelector(".font-format").hidden, !font.format, + "font " + i + " format hidden value correct"); + is(section.querySelector(".font-format").textContent, + font.format, "font " + i + " format correct"); + is(section.querySelector(".font-css-name").textContent, + font.cssName, "font " + i + " css name correct"); + } + + // test that the bold and regular fonts have different previews + let regSrc = s[0].querySelector(".font-preview").src; + let boldSrc = s[1].querySelector(".font-preview").src; + isnot(regSrc, boldSrc, "preview for bold font is different from regular"); + + // test system font + let localFontName = s[4].querySelector(".font-name").textContent; + let localFontCSSName = s[4].querySelector(".font-css-name").textContent; + + // On Linux test machines, the Arial font doesn't exist. + // The fallback is "Liberation Sans" + ok((localFontName == "Arial") || (localFontName == "Liberation Sans"), + "local font right font name"); + ok(s[4].classList.contains("is-local"), "local font is local"); + ok((localFontCSSName == "Arial") || (localFontCSSName == "Liberation Sans"), + "Arial", "local font has right css name"); +} + +function* testDivFonts(inspector, viewDoc) { + let updated = inspector.once("fontinspector-updated"); + yield selectNode("div", inspector); + yield updated; + + let sections1 = viewDoc.querySelectorAll("#all-fonts > section"); + is(sections1.length, 1, "Found 1 font on DIV"); + is(sections1[0].querySelector(".font-name").textContent, + "Ostrich Sans Medium", + "The DIV font has the right name"); +} + +function* testShowAllFonts(inspector, viewDoc) { + info("testing showing all fonts"); + + let updated = inspector.once("fontinspector-updated"); + viewDoc.querySelector("#font-showall").click(); + yield updated; + + // shouldn't change the node selection + is(inspector.selection.nodeFront.nodeName, "DIV", "Show all fonts selected"); + let sections = viewDoc.querySelectorAll("#all-fonts > section"); + is(sections.length, 6, "Font inspector shows 6 fonts (1 from iframe)"); +} diff --git a/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews-show-all.js b/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews-show-all.js new file mode 100644 index 000000000..f1319b400 --- /dev/null +++ b/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews-show-all.js @@ -0,0 +1,44 @@ +/* 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 correct previews are shown if the text is edited after 'Show all' +// button is pressed. + +const TEST_URI = URL_ROOT + "browser_fontinspector.html"; + +add_task(function* () { + let { inspector, view } = yield openFontInspectorForURL(TEST_URI); + let viewDoc = view.chromeDoc; + + info("Selecting a node that doesn't contain all document fonts."); + yield selectNode(".normal-text", inspector); + + let normalTextNumPreviews = + viewDoc.querySelectorAll("#all-fonts .font-preview").length; + + let onUpdated = inspector.once("fontinspector-updated"); + + info("Clicking 'Select all' button."); + viewDoc.getElementById("font-showall").click(); + + info("Waiting for font-inspector to update."); + yield onUpdated; + + let allFontsNumPreviews = + viewDoc.querySelectorAll("#all-fonts .font-preview").length; + + // Sanity check. If this fails all fonts apply also to the .normal-text node + // meaning we won't detect if preview editing causes the panel not to show all + // fonts. + isnot(allFontsNumPreviews, normalTextNumPreviews, + "The .normal-text didn't show all fonts."); + + info("Editing the preview text."); + yield updatePreviewText(view, "The quick brown"); + + let numPreviews = viewDoc.querySelectorAll("#all-fonts .font-preview").length; + is(numPreviews, allFontsNumPreviews, + "All fonts are still shown after the preview text was edited."); +}); diff --git a/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews.js b/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews.js new file mode 100644 index 000000000..adc421b6b --- /dev/null +++ b/devtools/client/inspector/fonts/test/browser_fontinspector_edit-previews.js @@ -0,0 +1,60 @@ +/* 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 previews change when the preview text changes. It doesn't check the +// exact preview images because they are drawn on a canvas causing them to vary +// between systems, platforms and software versions. + +const TEST_URI = URL_ROOT + "browser_fontinspector.html"; + +add_task(function* () { + let {view} = yield openFontInspectorForURL(TEST_URI); + let viewDoc = view.chromeDoc; + + let previews = viewDoc.querySelectorAll("#all-fonts .font-preview"); + let initialPreviews = [...previews].map(p => p.src); + + info("Typing 'Abc' to check that the reference previews are correct."); + yield updatePreviewText(view, "Abc"); + checkPreviewImages(viewDoc, initialPreviews, true); + + info("Typing something else to the preview box."); + yield updatePreviewText(view, "The quick brown"); + checkPreviewImages(viewDoc, initialPreviews, false); + + info("Blanking the input to restore default previews."); + yield updatePreviewText(view, ""); + checkPreviewImages(viewDoc, initialPreviews, true); +}); + +/** + * Compares the previous preview image URIs to the current URIs. + * + * @param {Document} viewDoc + * The FontInspector document. + * @param {Array[String]} originalURIs + * An array of URIs to compare with the current URIs. + * @param {Boolean} assertIdentical + * If true, this method asserts that the previous and current URIs are + * identical. If false, this method asserts that the previous and current + * URI's are different. + */ +function checkPreviewImages(viewDoc, originalURIs, assertIdentical) { + let previews = viewDoc.querySelectorAll("#all-fonts .font-preview"); + let newURIs = [...previews].map(p => p.src); + + is(newURIs.length, originalURIs.length, + "The number of previews has not changed."); + + for (let i = 0; i < newURIs.length; ++i) { + if (assertIdentical) { + is(newURIs[i], originalURIs[i], + `The preview image at index ${i} has stayed the same.`); + } else { + isnot(newURIs[i], originalURIs[i], + `The preview image at index ${i} has changed.`); + } + } +} diff --git a/devtools/client/inspector/fonts/test/browser_fontinspector_theme-change.js b/devtools/client/inspector/fonts/test/browser_fontinspector_theme-change.js new file mode 100644 index 000000000..7fcfc9cc2 --- /dev/null +++ b/devtools/client/inspector/fonts/test/browser_fontinspector_theme-change.js @@ -0,0 +1,55 @@ +/* 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"; + +requestLongerTimeout(2); + +// Test that the preview images are updated when the theme changes. + +const { getTheme, setTheme } = require("devtools/client/shared/theme"); + +const TEST_URI = URL_ROOT + "browser_fontinspector.html"; +const originalTheme = getTheme(); + +registerCleanupFunction(() => { + info(`Restoring theme to '${originalTheme}.`); + setTheme(originalTheme); +}); + +add_task(function* () { + let { inspector, view } = yield openFontInspectorForURL(TEST_URI); + let { chromeDoc: doc } = view; + + yield selectNode(".normal-text", inspector); + + // Store the original preview URI for later comparison. + let originalURI = doc.querySelector("#all-fonts .font-preview").src; + let newTheme = originalTheme === "light" ? "dark" : "light"; + + info(`Original theme was '${originalTheme}'.`); + + yield setThemeAndWaitForUpdate(newTheme, inspector); + isnot(doc.querySelector("#all-fonts .font-preview").src, originalURI, + "The preview image changed with the theme."); + + yield setThemeAndWaitForUpdate(originalTheme, inspector); + is(doc.querySelector("#all-fonts .font-preview").src, originalURI, + "The preview image is correct after the original theme was restored."); +}); + +/** + * Sets the current theme and waits for fontinspector-updated event. + * + * @param {String} theme - the new theme + * @param {Object} inspector - the inspector panel + */ +function* setThemeAndWaitForUpdate(theme, inspector) { + let onUpdated = inspector.once("fontinspector-updated"); + + info(`Setting theme to '${theme}'.`); + setTheme(theme); + + info("Waiting for font-inspector to update."); + yield onUpdated; +} diff --git a/devtools/client/inspector/fonts/test/head.js b/devtools/client/inspector/fonts/test/head.js new file mode 100644 index 000000000..f510ed798 --- /dev/null +++ b/devtools/client/inspector/fonts/test/head.js @@ -0,0 +1,86 @@ + /* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from ../../test/head.js */ +"use strict"; + +// Import the inspector's head.js first (which itself imports shared-head.js). +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js", + this); + +Services.prefs.setBoolPref("devtools.fontinspector.enabled", true); +Services.prefs.setCharPref("devtools.inspector.activeSidebar", "fontinspector"); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.fontinspector.enabled"); +}); + +/** + * The font-inspector doesn't participate in the inspector's update mechanism + * (i.e. it doesn't call inspector.updating() when updating), so simply calling + * the default selectNode isn't enough to guaranty that the panel has finished + * updating. We also need to wait for the fontinspector-updated event. + */ +var _selectNode = selectNode; +selectNode = function* (node, inspector, reason) { + let onUpdated = inspector.once("fontinspector-updated"); + yield _selectNode(node, inspector, reason); + yield onUpdated; +}; + +/** + * Adds a new tab with the given URL, opens the inspector and selects the + * font-inspector tab. + * @return {Promise} resolves to a {toolbox, inspector, view} object + */ +var openFontInspectorForURL = Task.async(function* (url) { + yield addTab(url); + let {toolbox, inspector} = yield openInspector(); + + // Call selectNode again here to force a fontinspector update since we don't + // know if the fontinspector-updated event has been sent while the inspector + // was being opened or not. + yield selectNode("body", inspector); + + return { + toolbox, + inspector, + view: inspector.fontInspector + }; +}); + +/** + * Clears the preview input field, types new text into it and waits for the + * preview images to be updated. + * + * @param {FontInspector} view - The FontInspector instance. + * @param {String} text - The text to preview. + */ +function* updatePreviewText(view, text) { + info(`Changing the preview text to '${text}'`); + + let doc = view.chromeDoc; + let input = doc.getElementById("font-preview-text-input"); + let update = view.inspector.once("fontinspector-updated"); + + info("Focusing the input field."); + input.focus(); + + is(doc.activeElement, input, "The input was focused."); + + info("Blanking the input field."); + for (let i = input.value.length; i >= 0; i--) { + EventUtils.sendKey("BACK_SPACE", doc.defaultView); + } + + is(input.value, "", "The input is now blank."); + + info("Typing the specified text to the input field."); + EventUtils.sendString(text, doc.defaultView); + is(input.value, text, "The input now contains the correct text."); + + info("Waiting for the font-inspector to update."); + yield update; +} diff --git a/devtools/client/inspector/fonts/test/ostrich-black.ttf b/devtools/client/inspector/fonts/test/ostrich-black.ttf Binary files differnew file mode 100755 index 000000000..a0ef8fe1c --- /dev/null +++ b/devtools/client/inspector/fonts/test/ostrich-black.ttf diff --git a/devtools/client/inspector/fonts/test/ostrich-regular.ttf b/devtools/client/inspector/fonts/test/ostrich-regular.ttf Binary files differnew file mode 100755 index 000000000..9682c0735 --- /dev/null +++ b/devtools/client/inspector/fonts/test/ostrich-regular.ttf diff --git a/devtools/client/inspector/fonts/test/test_iframe.html b/devtools/client/inspector/fonts/test/test_iframe.html new file mode 100644 index 000000000..29393a9e9 --- /dev/null +++ b/devtools/client/inspector/fonts/test/test_iframe.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> + +<style> + div{ + font-family: "Times New Roman"; + } +</style> + +<body> + <div>Hello world</div> +</body> diff --git a/devtools/client/inspector/inspector-commands.js b/devtools/client/inspector/inspector-commands.js new file mode 100644 index 000000000..ff26e4b94 --- /dev/null +++ b/devtools/client/inspector/inspector-commands.js @@ -0,0 +1,114 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const l10n = require("gcli/l10n"); +const {gDevTools} = require("devtools/client/framework/devtools"); +/* eslint-disable mozilla/reject-some-requires */ +const {EyeDropper, HighlighterEnvironment} = require("devtools/server/actors/highlighters"); +/* eslint-enable mozilla/reject-some-requires */ +const Telemetry = require("devtools/client/shared/telemetry"); + +const windowEyeDroppers = new WeakMap(); + +exports.items = [{ + item: "command", + runAt: "client", + name: "inspect", + description: l10n.lookup("inspectDesc"), + manual: l10n.lookup("inspectManual"), + params: [ + { + name: "selector", + type: "string", + description: l10n.lookup("inspectNodeDesc"), + manual: l10n.lookup("inspectNodeManual") + } + ], + exec: function* (args, context) { + let target = context.environment.target; + let toolbox = yield gDevTools.showToolbox(target, "inspector"); + let walker = toolbox.getCurrentPanel().walker; + let rootNode = yield walker.getRootNode(); + let nodeFront = yield walker.querySelector(rootNode, args.selector); + toolbox.getCurrentPanel().selection.setNodeFront(nodeFront, "gcli"); + }, +}, { + item: "command", + runAt: "client", + name: "eyedropper", + description: l10n.lookup("eyedropperDesc"), + manual: l10n.lookup("eyedropperManual"), + params: [{ + // This hidden parameter is only set to true when the eyedropper browser menu item is + // used. It is useful to log a different telemetry event whether the tool was used + // from the menu, or from the gcli command line. + group: "hiddengroup", + params: [{ + name: "frommenu", + type: "boolean", + hidden: true + }, { + name: "hide", + type: "boolean", + hidden: true + }] + }], + exec: function* (args, context) { + if (args.hide) { + context.updateExec("eyedropper_server_hide").catch(e => console.error(e)); + return; + } + + // If the inspector is already picking a color from the page, cancel it. + let target = context.environment.target; + let toolbox = gDevTools.getToolbox(target); + if (toolbox) { + let inspector = toolbox.getPanel("inspector"); + if (inspector) { + yield inspector.hideEyeDropper(); + } + } + + let telemetry = new Telemetry(); + telemetry.toolOpened(args.frommenu ? "menueyedropper" : "eyedropper"); + context.updateExec("eyedropper_server").catch(e => console.error(e)); + } +}, { + item: "command", + runAt: "server", + name: "eyedropper_server", + hidden: true, + exec: function (args, {environment}) { + let eyeDropper = windowEyeDroppers.get(environment.window); + + if (!eyeDropper) { + let env = new HighlighterEnvironment(); + env.initFromWindow(environment.window); + + eyeDropper = new EyeDropper(env); + eyeDropper.once("hidden", () => { + eyeDropper.destroy(); + env.destroy(); + windowEyeDroppers.delete(environment.window); + }); + + windowEyeDroppers.set(environment.window, eyeDropper); + } + + eyeDropper.show(environment.document.documentElement, {copyOnSelect: true}); + } +}, { + item: "command", + runAt: "server", + name: "eyedropper_server_hide", + hidden: true, + exec: function (args, {environment}) { + let eyeDropper = windowEyeDroppers.get(environment.window); + if (eyeDropper) { + eyeDropper.hide(); + } + } +}]; diff --git a/devtools/client/inspector/inspector-search.js b/devtools/client/inspector/inspector-search.js new file mode 100644 index 000000000..50e0383bc --- /dev/null +++ b/devtools/client/inspector/inspector-search.js @@ -0,0 +1,549 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const promise = require("promise"); +const {Task} = require("devtools/shared/task"); +const {KeyCodes} = require("devtools/client/shared/keycodes"); + +const EventEmitter = require("devtools/shared/event-emitter"); +const {AutocompletePopup} = require("devtools/client/shared/autocomplete-popup"); +const Services = require("Services"); + +// Maximum number of selector suggestions shown in the panel. +const MAX_SUGGESTIONS = 15; + +/** + * Converts any input field into a document search box. + * + * @param {InspectorPanel} inspector + * The InspectorPanel whose `walker` attribute should be used for + * document traversal. + * @param {DOMNode} input + * The input element to which the panel will be attached and from where + * search input will be taken. + * @param {DOMNode} clearBtn + * The clear button in the input field that will clear the input value. + * + * Emits the following events: + * - search-cleared: when the search box is emptied + * - search-result: when a search is made and a result is selected + */ +function InspectorSearch(inspector, input, clearBtn) { + this.inspector = inspector; + this.searchBox = input; + this.searchClearButton = clearBtn; + this._lastSearched = null; + + this.searchClearButton.hidden = true; + + this._onKeyDown = this._onKeyDown.bind(this); + this._onInput = this._onInput.bind(this); + this._onClearSearch = this._onClearSearch.bind(this); + this.searchBox.addEventListener("keydown", this._onKeyDown, true); + this.searchBox.addEventListener("input", this._onInput, true); + this.searchBox.addEventListener("contextmenu", this.inspector.onTextBoxContextMenu); + this.searchClearButton.addEventListener("click", this._onClearSearch); + + // For testing, we need to be able to wait for the most recent node request + // to finish. Tests can watch this promise for that. + this._lastQuery = promise.resolve(null); + + this.autocompleter = new SelectorAutocompleter(inspector, input); + EventEmitter.decorate(this); +} + +exports.InspectorSearch = InspectorSearch; + +InspectorSearch.prototype = { + get walker() { + return this.inspector.walker; + }, + + destroy: function () { + this.searchBox.removeEventListener("keydown", this._onKeyDown, true); + this.searchBox.removeEventListener("input", this._onInput, true); + this.searchBox.removeEventListener("contextmenu", + this.inspector.onTextBoxContextMenu); + this.searchClearButton.removeEventListener("click", this._onClearSearch); + this.searchBox = null; + this.searchClearButton = null; + this.autocompleter.destroy(); + }, + + _onSearch: function (reverse = false) { + this.doFullTextSearch(this.searchBox.value, reverse) + .catch(e => console.error(e)); + }, + + doFullTextSearch: Task.async(function* (query, reverse) { + let lastSearched = this._lastSearched; + this._lastSearched = query; + + if (query.length === 0) { + this.searchBox.classList.remove("devtools-style-searchbox-no-match"); + if (!lastSearched || lastSearched.length > 0) { + this.emit("search-cleared"); + } + return; + } + + let res = yield this.walker.search(query, { reverse }); + + // Value has changed since we started this request, we're done. + if (query !== this.searchBox.value) { + return; + } + + if (res) { + this.inspector.selection.setNodeFront(res.node, "inspectorsearch"); + this.searchBox.classList.remove("devtools-style-searchbox-no-match"); + + res.query = query; + this.emit("search-result", res); + } else { + this.searchBox.classList.add("devtools-style-searchbox-no-match"); + this.emit("search-result"); + } + }), + + _onInput: function () { + if (this.searchBox.value.length === 0) { + this.searchClearButton.hidden = true; + this._onSearch(); + } else { + this.searchClearButton.hidden = false; + } + }, + + _onKeyDown: function (event) { + if (event.keyCode === KeyCodes.DOM_VK_RETURN) { + this._onSearch(event.shiftKey); + } + + const modifierKey = Services.appinfo.OS === "Darwin" + ? event.metaKey : event.ctrlKey; + if (event.keyCode === KeyCodes.DOM_VK_G && modifierKey) { + this._onSearch(event.shiftKey); + event.preventDefault(); + } + }, + + _onClearSearch: function () { + this.searchBox.classList.remove("devtools-style-searchbox-no-match"); + this.searchBox.value = ""; + this.searchClearButton.hidden = true; + this.emit("search-cleared"); + } +}; + +/** + * Converts any input box on a page to a CSS selector search and suggestion box. + * + * Emits 'processing-done' event when it is done processing the current + * keypress, search request or selection from the list, whether that led to a + * search or not. + * + * @constructor + * @param InspectorPanel inspector + * The InspectorPanel whose `walker` attribute should be used for + * document traversal. + * @param nsiInputElement inputNode + * The input element to which the panel will be attached and from where + * search input will be taken. + */ +function SelectorAutocompleter(inspector, inputNode) { + this.inspector = inspector; + this.searchBox = inputNode; + this.panelDoc = this.searchBox.ownerDocument; + + this.showSuggestions = this.showSuggestions.bind(this); + this._onSearchKeypress = this._onSearchKeypress.bind(this); + this._onSearchPopupClick = this._onSearchPopupClick.bind(this); + this._onMarkupMutation = this._onMarkupMutation.bind(this); + + // Options for the AutocompletePopup. + let options = { + listId: "searchbox-panel-listbox", + autoSelect: true, + position: "top", + theme: "auto", + onClick: this._onSearchPopupClick, + }; + + // The popup will be attached to the toolbox document. + this.searchPopup = new AutocompletePopup(inspector._toolbox.doc, options); + + this.searchBox.addEventListener("input", this.showSuggestions, true); + this.searchBox.addEventListener("keypress", this._onSearchKeypress, true); + this.inspector.on("markupmutation", this._onMarkupMutation); + + // For testing, we need to be able to wait for the most recent node request + // to finish. Tests can watch this promise for that. + this._lastQuery = promise.resolve(null); + EventEmitter.decorate(this); +} + +exports.SelectorAutocompleter = SelectorAutocompleter; + +SelectorAutocompleter.prototype = { + get walker() { + return this.inspector.walker; + }, + + // The possible states of the query. + States: { + CLASS: "class", + ID: "id", + TAG: "tag", + ATTRIBUTE: "attribute", + }, + + // The current state of the query. + _state: null, + + // The query corresponding to last state computation. + _lastStateCheckAt: null, + + /** + * Computes the state of the query. State refers to whether the query + * currently requires a class suggestion, or a tag, or an Id suggestion. + * This getter will effectively compute the state by traversing the query + * character by character each time the query changes. + * + * @example + * '#f' requires an Id suggestion, so the state is States.ID + * 'div > .foo' requires class suggestion, so state is States.CLASS + */ + get state() { + if (!this.searchBox || !this.searchBox.value) { + return null; + } + + let query = this.searchBox.value; + if (this._lastStateCheckAt == query) { + // If query is the same, return early. + return this._state; + } + this._lastStateCheckAt = query; + + this._state = null; + let subQuery = ""; + // Now we iterate over the query and decide the state character by + // character. + // The logic here is that while iterating, the state can go from one to + // another with some restrictions. Like, if the state is Class, then it can + // never go to Tag state without a space or '>' character; Or like, a Class + // state with only '.' cannot go to an Id state without any [a-zA-Z] after + // the '.' which means that '.#' is a selector matching a class name '#'. + // Similarily for '#.' which means a selctor matching an id '.'. + for (let i = 1; i <= query.length; i++) { + // Calculate the state. + subQuery = query.slice(0, i); + let [secondLastChar, lastChar] = subQuery.slice(-2); + switch (this._state) { + case null: + // This will happen only in the first iteration of the for loop. + lastChar = secondLastChar; + + case this.States.TAG: // eslint-disable-line + if (lastChar === ".") { + this._state = this.States.CLASS; + } else if (lastChar === "#") { + this._state = this.States.ID; + } else if (lastChar === "[") { + this._state = this.States.ATTRIBUTE; + } else { + this._state = this.States.TAG; + } + break; + + case this.States.CLASS: + if (subQuery.match(/[\.]+[^\.]*$/)[0].length > 2) { + // Checks whether the subQuery has atleast one [a-zA-Z] after the + // '.'. + if (lastChar === " " || lastChar === ">") { + this._state = this.States.TAG; + } else if (lastChar === "#") { + this._state = this.States.ID; + } else if (lastChar === "[") { + this._state = this.States.ATTRIBUTE; + } else { + this._state = this.States.CLASS; + } + } + break; + + case this.States.ID: + if (subQuery.match(/[#]+[^#]*$/)[0].length > 2) { + // Checks whether the subQuery has atleast one [a-zA-Z] after the + // '#'. + if (lastChar === " " || lastChar === ">") { + this._state = this.States.TAG; + } else if (lastChar === ".") { + this._state = this.States.CLASS; + } else if (lastChar === "[") { + this._state = this.States.ATTRIBUTE; + } else { + this._state = this.States.ID; + } + } + break; + + case this.States.ATTRIBUTE: + if (subQuery.match(/[\[][^\]]+[\]]/) !== null) { + // Checks whether the subQuery has at least one ']' after the '['. + if (lastChar === " " || lastChar === ">") { + this._state = this.States.TAG; + } else if (lastChar === ".") { + this._state = this.States.CLASS; + } else if (lastChar === "#") { + this._state = this.States.ID; + } else { + this._state = this.States.ATTRIBUTE; + } + } + break; + } + } + return this._state; + }, + + /** + * Removes event listeners and cleans up references. + */ + destroy: function () { + this.searchBox.removeEventListener("input", this.showSuggestions, true); + this.searchBox.removeEventListener("keypress", + this._onSearchKeypress, true); + this.inspector.off("markupmutation", this._onMarkupMutation); + this.searchPopup.destroy(); + this.searchPopup = null; + this.searchBox = null; + this.panelDoc = null; + }, + + /** + * Handles keypresses inside the input box. + */ + _onSearchKeypress: function (event) { + let popup = this.searchPopup; + + switch (event.keyCode) { + case KeyCodes.DOM_VK_RETURN: + case KeyCodes.DOM_VK_TAB: + if (popup.isOpen) { + if (popup.selectedItem) { + this.searchBox.value = popup.selectedItem.label; + } + this.hidePopup(); + } else if (!popup.isOpen) { + // When tab is pressed with focus on searchbox and closed popup, + // do not prevent the default to avoid a keyboard trap and move focus + // to next/previous element. + this.emit("processing-done"); + return; + } + break; + + case KeyCodes.DOM_VK_UP: + if (popup.isOpen && popup.itemCount > 0) { + if (popup.selectedIndex === 0) { + popup.selectedIndex = popup.itemCount - 1; + } else { + popup.selectedIndex--; + } + this.searchBox.value = popup.selectedItem.label; + } + break; + + case KeyCodes.DOM_VK_DOWN: + if (popup.isOpen && popup.itemCount > 0) { + if (popup.selectedIndex === popup.itemCount - 1) { + popup.selectedIndex = 0; + } else { + popup.selectedIndex++; + } + this.searchBox.value = popup.selectedItem.label; + } + break; + + case KeyCodes.DOM_VK_ESCAPE: + if (popup.isOpen) { + this.hidePopup(); + } + break; + + default: + return; + } + + event.preventDefault(); + event.stopPropagation(); + this.emit("processing-done"); + }, + + /** + * Handles click events from the autocomplete popup. + */ + _onSearchPopupClick: function (event) { + let selectedItem = this.searchPopup.selectedItem; + if (selectedItem) { + this.searchBox.value = selectedItem.label; + } + this.hidePopup(); + + event.preventDefault(); + event.stopPropagation(); + }, + + /** + * Reset previous search results on markup-mutations to make sure we search + * again after nodes have been added/removed/changed. + */ + _onMarkupMutation: function () { + this._searchResults = null; + this._lastSearched = null; + }, + + /** + * Populates the suggestions list and show the suggestion popup. + * + * @return {Promise} promise that will resolve when the autocomplete popup is fully + * displayed or hidden. + */ + _showPopup: function (list, firstPart, popupState) { + let total = 0; + let query = this.searchBox.value; + let items = []; + + for (let [value, , state] of list) { + if (query.match(/[\s>+]$/)) { + // for cases like 'div ' or 'div >' or 'div+' + value = query + value; + } else if (query.match(/[\s>+][\.#a-zA-Z][^\s>+\.#\[]*$/)) { + // for cases like 'div #a' or 'div .a' or 'div > d' and likewise + let lastPart = query.match(/[\s>+][\.#a-zA-Z][^\s>+\.#\[]*$/)[0]; + value = query.slice(0, -1 * lastPart.length + 1) + value; + } else if (query.match(/[a-zA-Z][#\.][^#\.\s+>]*$/)) { + // for cases like 'div.class' or '#foo.bar' and likewise + let lastPart = query.match(/[a-zA-Z][#\.][^#\.\s+>]*$/)[0]; + value = query.slice(0, -1 * lastPart.length + 1) + value; + } else if (query.match(/[a-zA-Z]*\[[^\]]*\][^\]]*/)) { + // for cases like '[foo].bar' and likewise + let attrPart = query.substring(0, query.lastIndexOf("]") + 1); + value = attrPart + value; + } + + let item = { + preLabel: query, + label: value + }; + + // In case the query's state is tag and the item's state is id or class + // adjust the preLabel + if (popupState === this.States.TAG && state === this.States.CLASS) { + item.preLabel = "." + item.preLabel; + } + if (popupState === this.States.TAG && state === this.States.ID) { + item.preLabel = "#" + item.preLabel; + } + + items.unshift(item); + if (++total > MAX_SUGGESTIONS - 1) { + break; + } + } + + if (total > 0) { + let onPopupOpened = this.searchPopup.once("popup-opened"); + this.searchPopup.once("popup-closed", () => { + this.searchPopup.setItems(items); + this.searchPopup.openPopup(this.searchBox); + }); + this.searchPopup.hidePopup(); + return onPopupOpened; + } + + return this.hidePopup(); + }, + + /** + * Hide the suggestion popup if necessary. + */ + hidePopup: function () { + let onPopupClosed = this.searchPopup.once("popup-closed"); + this.searchPopup.hidePopup(); + return onPopupClosed; + }, + + /** + * Suggests classes,ids and tags based on the user input as user types in the + * searchbox. + */ + showSuggestions: function () { + let query = this.searchBox.value; + let state = this.state; + let firstPart = ""; + + if (query.endsWith("*") || state === this.States.ATTRIBUTE) { + // Hide the popup if the query ends with * (because we don't want to + // suggest all nodes) or if it is an attribute selector (because + // it would give a lot of useless results). + this.hidePopup(); + return; + } + + if (state === this.States.TAG) { + // gets the tag that is being completed. For ex. 'div.foo > s' returns + // 's', 'di' returns 'di' and likewise. + firstPart = (query.match(/[\s>+]?([a-zA-Z]*)$/) || ["", query])[1]; + query = query.slice(0, query.length - firstPart.length); + } else if (state === this.States.CLASS) { + // gets the class that is being completed. For ex. '.foo.b' returns 'b' + firstPart = query.match(/\.([^\.]*)$/)[1]; + query = query.slice(0, query.length - firstPart.length - 1); + } else if (state === this.States.ID) { + // gets the id that is being completed. For ex. '.foo#b' returns 'b' + firstPart = query.match(/#([^#]*)$/)[1]; + query = query.slice(0, query.length - firstPart.length - 1); + } + // TODO: implement some caching so that over the wire request is not made + // everytime. + if (/[\s+>~]$/.test(query)) { + query += "*"; + } + + let suggestionsPromise = this.walker.getSuggestionsForQuery( + query, firstPart, state); + this._lastQuery = suggestionsPromise.then(result => { + this.emit("processing-done"); + if (result.query !== query) { + // This means that this response is for a previous request and the user + // as since typed something extra leading to a new request. + return promise.resolve(null); + } + + if (state === this.States.CLASS) { + firstPart = "." + firstPart; + } else if (state === this.States.ID) { + firstPart = "#" + firstPart; + } + + // If there is a single tag match and it's what the user typed, then + // don't need to show a popup. + if (result.suggestions.length === 1 && + result.suggestions[0][0] === firstPart) { + result.suggestions = []; + } + + // Wait for the autocomplete-popup to fire its popup-opened event, to make sure + // the autoSelect item has been selected. + return this._showPopup(result.suggestions, firstPart, state); + }); + + return; + } +}; diff --git a/devtools/client/inspector/inspector.js b/devtools/client/inspector/inspector.js new file mode 100644 index 000000000..c056c213f --- /dev/null +++ b/devtools/client/inspector/inspector.js @@ -0,0 +1,1936 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* global window */ + +"use strict"; + +var Cu = Components.utils; +var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +var Services = require("Services"); +var promise = require("promise"); +var defer = require("devtools/shared/defer"); +var EventEmitter = require("devtools/shared/event-emitter"); +const {executeSoon} = require("devtools/shared/DevToolsUtils"); +var {KeyShortcuts} = require("devtools/client/shared/key-shortcuts"); +var {Task} = require("devtools/shared/task"); +const {initCssProperties} = require("devtools/shared/fronts/css-properties"); +const nodeConstants = require("devtools/shared/dom-node-constants"); +const Telemetry = require("devtools/client/shared/telemetry"); + +const Menu = require("devtools/client/framework/menu"); +const MenuItem = require("devtools/client/framework/menu-item"); + +const {CommandUtils} = require("devtools/client/shared/developer-toolbar"); +const {ComputedViewTool} = require("devtools/client/inspector/computed/computed"); +const {FontInspector} = require("devtools/client/inspector/fonts/fonts"); +const {HTMLBreadcrumbs} = require("devtools/client/inspector/breadcrumbs"); +const {InspectorSearch} = require("devtools/client/inspector/inspector-search"); +const MarkupView = require("devtools/client/inspector/markup/markup"); +const {RuleViewTool} = require("devtools/client/inspector/rules/rules"); +const {ToolSidebar} = require("devtools/client/inspector/toolsidebar"); +const {ViewHelpers} = require("devtools/client/shared/widgets/view-helpers"); +const clipboardHelper = require("devtools/shared/platform/clipboard"); + +const {LocalizationHelper, localizeMarkup} = require("devtools/shared/l10n"); +const INSPECTOR_L10N = + new LocalizationHelper("devtools/client/locales/inspector.properties"); +const TOOLBOX_L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties"); + +// Sidebar dimensions +const INITIAL_SIDEBAR_SIZE = 350; + +// If the toolbox width is smaller than given amount of pixels, +// the sidebar automatically switches from 'landscape' to 'portrait' mode. +const PORTRAIT_MODE_WIDTH = 700; + +/** + * Represents an open instance of the Inspector for a tab. + * The inspector controls the breadcrumbs, the markup view, and the sidebar + * (computed view, rule view, font view and animation inspector). + * + * Events: + * - ready + * Fired when the inspector panel is opened for the first time and ready to + * use + * - new-root + * Fired after a new root (navigation to a new page) event was fired by + * the walker, and taken into account by the inspector (after the markup + * view has been reloaded) + * - markuploaded + * Fired when the markup-view frame has loaded + * - breadcrumbs-updated + * Fired when the breadcrumb widget updates to a new node + * - boxmodel-view-updated + * Fired when the box model updates to a new node + * - markupmutation + * Fired after markup mutations have been processed by the markup-view + * - computed-view-refreshed + * Fired when the computed rules view updates to a new node + * - computed-view-property-expanded + * Fired when a property is expanded in the computed rules view + * - computed-view-property-collapsed + * Fired when a property is collapsed in the computed rules view + * - computed-view-sourcelinks-updated + * Fired when the stylesheet source links have been updated (when switching + * to source-mapped files) + * - computed-view-filtered + * Fired when the computed rules view is filtered + * - rule-view-refreshed + * Fired when the rule view updates to a new node + * - rule-view-sourcelinks-updated + * Fired when the stylesheet source links have been updated (when switching + * to source-mapped files) + */ +function Inspector(toolbox) { + this._toolbox = toolbox; + this._target = toolbox.target; + this.panelDoc = window.document; + this.panelWin = window; + this.panelWin.inspector = this; + + this.telemetry = new Telemetry(); + + this.nodeMenuTriggerInfo = null; + + this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this); + this._onBeforeNavigate = this._onBeforeNavigate.bind(this); + this.onNewRoot = this.onNewRoot.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + this.onTextBoxContextMenu = this.onTextBoxContextMenu.bind(this); + this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this); + this.onNewSelection = this.onNewSelection.bind(this); + this.onDetached = this.onDetached.bind(this); + this.onPaneToggleButtonClicked = this.onPaneToggleButtonClicked.bind(this); + this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this); + this.onPanelWindowResize = this.onPanelWindowResize.bind(this); + this.onSidebarShown = this.onSidebarShown.bind(this); + this.onSidebarHidden = this.onSidebarHidden.bind(this); + + this._target.on("will-navigate", this._onBeforeNavigate); + this._detectingActorFeatures = this._detectActorFeatures(); + + EventEmitter.decorate(this); +} + +Inspector.prototype = { + /** + * open is effectively an asynchronous constructor + */ + init: Task.async(function* () { + // Localize all the nodes containing a data-localization attribute. + localizeMarkup(this.panelDoc); + + this._cssPropertiesLoaded = initCssProperties(this.toolbox); + yield this._cssPropertiesLoaded; + yield this.target.makeRemote(); + yield this._getPageStyle(); + + // This may throw if the document is still loading and we are + // refering to a dead about:blank document + let defaultSelection = yield this._getDefaultNodeForSelection() + .catch(this._handleRejectionIfNotDestroyed); + + return yield this._deferredOpen(defaultSelection); + }), + + get toolbox() { + return this._toolbox; + }, + + get inspector() { + return this._toolbox.inspector; + }, + + get walker() { + return this._toolbox.walker; + }, + + get selection() { + return this._toolbox.selection; + }, + + get highlighter() { + return this._toolbox.highlighter; + }, + + get isOuterHTMLEditable() { + return this._target.client.traits.editOuterHTML; + }, + + get hasUrlToImageDataResolver() { + return this._target.client.traits.urlToImageDataResolver; + }, + + get canGetUniqueSelector() { + return this._target.client.traits.getUniqueSelector; + }, + + get canGetUsedFontFaces() { + return this._target.client.traits.getUsedFontFaces; + }, + + get canPasteInnerOrAdjacentHTML() { + return this._target.client.traits.pasteHTML; + }, + + /** + * Handle promise rejections for various asynchronous actions, and only log errors if + * the inspector panel still exists. + * This is useful to silence useless errors that happen when the inspector is closed + * while still initializing (and making protocol requests). + */ + _handleRejectionIfNotDestroyed: function (e) { + if (!this._panelDestroyer) { + console.error(e); + } + }, + + /** + * Figure out what features the backend supports + */ + _detectActorFeatures: function () { + this._supportsDuplicateNode = false; + this._supportsScrollIntoView = false; + this._supportsResolveRelativeURL = false; + + // Use getActorDescription first so that all actorHasMethod calls use + // a cached response from the server. + return this._target.getActorDescription("domwalker").then(desc => { + return promise.all([ + this._target.actorHasMethod("domwalker", "duplicateNode").then(value => { + this._supportsDuplicateNode = value; + }).catch(e => console.error(e)), + this._target.actorHasMethod("domnode", "scrollIntoView").then(value => { + this._supportsScrollIntoView = value; + }).catch(e => console.error(e)), + this._target.actorHasMethod("inspector", "resolveRelativeURL").then(value => { + this._supportsResolveRelativeURL = value; + }).catch(e => console.error(e)), + ]); + }); + }, + + _deferredOpen: function (defaultSelection) { + let deferred = defer(); + + this.breadcrumbs = new HTMLBreadcrumbs(this); + + this.walker.on("new-root", this.onNewRoot); + + this.selection.on("new-node-front", this.onNewSelection); + this.selection.on("detached-front", this.onDetached); + + if (this.target.isLocalTab) { + // Show a warning when the debugger is paused. + // We show the warning only when the inspector + // is selected. + this.updateDebuggerPausedWarning = () => { + let notificationBox = this._toolbox.getNotificationBox(); + let notification = + notificationBox.getNotificationWithValue("inspector-script-paused"); + if (!notification && this._toolbox.currentToolId == "inspector" && + this._toolbox.threadClient.paused) { + let message = INSPECTOR_L10N.getStr("debuggerPausedWarning.message"); + notificationBox.appendNotification(message, + "inspector-script-paused", "", notificationBox.PRIORITY_WARNING_HIGH); + } + + if (notification && this._toolbox.currentToolId != "inspector") { + notificationBox.removeNotification(notification); + } + + if (notification && !this._toolbox.threadClient.paused) { + notificationBox.removeNotification(notification); + } + }; + this.target.on("thread-paused", this.updateDebuggerPausedWarning); + this.target.on("thread-resumed", this.updateDebuggerPausedWarning); + this._toolbox.on("select", this.updateDebuggerPausedWarning); + this.updateDebuggerPausedWarning(); + } + + this._initMarkup(); + this.isReady = false; + + this.once("markuploaded", () => { + this.isReady = true; + + // All the components are initialized. Let's select a node. + if (defaultSelection) { + this.selection.setNodeFront(defaultSelection, "inspector-open"); + this.markup.expandNode(this.selection.nodeFront); + } + + // And setup the toolbar only now because it may depend on the document. + this.setupToolbar(); + + this.emit("ready"); + deferred.resolve(this); + }); + + this.setupSearchBox(); + this.setupSidebar(); + + return deferred.promise; + }, + + _onBeforeNavigate: function () { + this._defaultNode = null; + this.selection.setNodeFront(null); + this._destroyMarkup(); + this.isDirty = false; + this._pendingSelection = null; + }, + + _getPageStyle: function () { + return this.inspector.getPageStyle().then(pageStyle => { + this.pageStyle = pageStyle; + }, this._handleRejectionIfNotDestroyed); + }, + + /** + * Return a promise that will resolve to the default node for selection. + */ + _getDefaultNodeForSelection: function () { + if (this._defaultNode) { + return this._defaultNode; + } + let walker = this.walker; + let rootNode = null; + let pendingSelection = this._pendingSelection; + + // A helper to tell if the target has or is about to navigate. + // this._pendingSelection changes on "will-navigate" and "new-root" events. + let hasNavigated = () => pendingSelection !== this._pendingSelection; + + // If available, set either the previously selected node or the body + // as default selected, else set documentElement + return walker.getRootNode().then(node => { + if (hasNavigated()) { + return promise.reject("navigated; resolution of _defaultNode aborted"); + } + + rootNode = node; + if (this.selectionCssSelector) { + return walker.querySelector(rootNode, this.selectionCssSelector); + } + return null; + }).then(front => { + if (hasNavigated()) { + return promise.reject("navigated; resolution of _defaultNode aborted"); + } + + if (front) { + return front; + } + return walker.querySelector(rootNode, "body"); + }).then(front => { + if (hasNavigated()) { + return promise.reject("navigated; resolution of _defaultNode aborted"); + } + + if (front) { + return front; + } + return this.walker.documentElement(); + }).then(node => { + if (hasNavigated()) { + return promise.reject("navigated; resolution of _defaultNode aborted"); + } + this._defaultNode = node; + return node; + }); + }, + + /** + * Target getter. + */ + get target() { + return this._target; + }, + + /** + * Target setter. + */ + set target(value) { + this._target = value; + }, + + /** + * Indicate that a tool has modified the state of the page. Used to + * decide whether to show the "are you sure you want to navigate" + * notification. + */ + markDirty: function () { + this.isDirty = true; + }, + + /** + * Hooks the searchbar to show result and auto completion suggestions. + */ + setupSearchBox: function () { + this.searchBox = this.panelDoc.getElementById("inspector-searchbox"); + this.searchClearButton = this.panelDoc.getElementById("inspector-searchinput-clear"); + this.searchResultsLabel = this.panelDoc.getElementById("inspector-searchlabel"); + + this.search = new InspectorSearch(this, this.searchBox, this.searchClearButton); + this.search.on("search-cleared", this._updateSearchResultsLabel); + this.search.on("search-result", this._updateSearchResultsLabel); + + let shortcuts = new KeyShortcuts({ + window: this.panelDoc.defaultView, + }); + let key = INSPECTOR_L10N.getStr("inspector.searchHTML.key"); + shortcuts.on(key, (name, event) => { + // Prevent overriding same shortcut from the computed/rule views + if (event.target.closest("#sidebar-panel-ruleview") || + event.target.closest("#sidebar-panel-computedview")) { + return; + } + event.preventDefault(); + this.searchBox.focus(); + }); + }, + + get searchSuggestions() { + return this.search.autocompleter; + }, + + _updateSearchResultsLabel: function (event, result) { + let str = ""; + if (event !== "search-cleared") { + if (result) { + str = INSPECTOR_L10N.getFormatStr( + "inspector.searchResultsCount2", result.resultsIndex + 1, result.resultsLength); + } else { + str = INSPECTOR_L10N.getStr("inspector.searchResultsNone"); + } + } + + this.searchResultsLabel.textContent = str; + }, + + get React() { + return this._toolbox.React; + }, + + get ReactDOM() { + return this._toolbox.ReactDOM; + }, + + get ReactRedux() { + return this._toolbox.ReactRedux; + }, + + get browserRequire() { + return this._toolbox.browserRequire; + }, + + get InspectorTabPanel() { + if (!this._InspectorTabPanel) { + this._InspectorTabPanel = + this.React.createFactory(this.browserRequire( + "devtools/client/inspector/components/inspector-tab-panel")); + } + return this._InspectorTabPanel; + }, + + /** + * Check if the inspector should use the landscape mode. + * + * @return {Boolean} true if the inspector should be in landscape mode. + */ + useLandscapeMode: function () { + let { clientWidth } = this.panelDoc.getElementById("inspector-splitter-box"); + return clientWidth > PORTRAIT_MODE_WIDTH; + }, + + /** + * Build Splitter located between the main and side area of + * the Inspector panel. + */ + setupSplitter: function () { + let SplitBox = this.React.createFactory(this.browserRequire( + "devtools/client/shared/components/splitter/split-box")); + + let splitter = SplitBox({ + className: "inspector-sidebar-splitter", + initialWidth: INITIAL_SIDEBAR_SIZE, + initialHeight: INITIAL_SIDEBAR_SIZE, + splitterSize: 1, + endPanelControl: true, + startPanel: this.InspectorTabPanel({ + id: "inspector-main-content" + }), + endPanel: this.InspectorTabPanel({ + id: "inspector-sidebar-container" + }), + vert: this.useLandscapeMode(), + }); + + this._splitter = this.ReactDOM.render(splitter, + this.panelDoc.getElementById("inspector-splitter-box")); + + this.panelWin.addEventListener("resize", this.onPanelWindowResize, true); + + // Persist splitter state in preferences. + this.sidebar.on("show", this.onSidebarShown); + this.sidebar.on("hide", this.onSidebarHidden); + this.sidebar.on("destroy", this.onSidebarHidden); + }, + + /** + * Splitter clean up. + */ + teardownSplitter: function () { + this.panelWin.removeEventListener("resize", this.onPanelWindowResize, true); + + this.sidebar.off("show", this.onSidebarShown); + this.sidebar.off("hide", this.onSidebarHidden); + this.sidebar.off("destroy", this.onSidebarHidden); + }, + + /** + * If Toolbox width is less than 600 px, the splitter changes its mode + * to `horizontal` to support portrait view. + */ + onPanelWindowResize: function () { + this._splitter.setState({ + vert: this.useLandscapeMode(), + }); + }, + + onSidebarShown: function () { + let width; + let height; + + // Initialize splitter size from preferences. + try { + width = Services.prefs.getIntPref("devtools.toolsidebar-width.inspector"); + height = Services.prefs.getIntPref("devtools.toolsidebar-height.inspector"); + } catch (e) { + // Set width and height of the splitter. Only one + // value is really useful at a time depending on the current + // orientation (vertical/horizontal). + // Having both is supported by the splitter component. + width = INITIAL_SIDEBAR_SIZE; + height = INITIAL_SIDEBAR_SIZE; + } + + this._splitter.setState({width, height}); + }, + + onSidebarHidden: function () { + // Store the current splitter size to preferences. + let state = this._splitter.state; + Services.prefs.setIntPref("devtools.toolsidebar-width.inspector", state.width); + Services.prefs.setIntPref("devtools.toolsidebar-height.inspector", state.height); + }, + + /** + * Build the sidebar. + */ + setupSidebar: function () { + let tabbox = this.panelDoc.querySelector("#inspector-sidebar"); + this.sidebar = new ToolSidebar(tabbox, this, "inspector", { + showAllTabsMenu: true + }); + + let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar"); + + this._setDefaultSidebar = (event, toolId) => { + Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId); + }; + + this.sidebar.on("select", this._setDefaultSidebar); + + if (!Services.prefs.getBoolPref("devtools.fontinspector.enabled") && + defaultTab == "fontinspector") { + defaultTab = "ruleview"; + } + + // Append all side panels + this.sidebar.addExistingTab( + "ruleview", + INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"), + defaultTab == "ruleview"); + + this.sidebar.addExistingTab( + "computedview", + INSPECTOR_L10N.getStr("inspector.sidebar.computedViewTitle"), + defaultTab == "computedview"); + + this.ruleview = new RuleViewTool(this, this.panelWin); + this.computedview = new ComputedViewTool(this, this.panelWin); + + if (Services.prefs.getBoolPref("devtools.layoutview.enabled")) { + const {LayoutView} = this.browserRequire("devtools/client/inspector/layout/layout"); + this.layoutview = new LayoutView(this, this.panelWin); + } + + if (this.target.form.animationsActor) { + this.sidebar.addFrameTab( + "animationinspector", + INSPECTOR_L10N.getStr("inspector.sidebar.animationInspectorTitle"), + "chrome://devtools/content/animationinspector/animation-inspector.xhtml", + defaultTab == "animationinspector"); + } + + if (Services.prefs.getBoolPref("devtools.fontinspector.enabled") && + this.canGetUsedFontFaces) { + this.sidebar.addExistingTab( + "fontinspector", + INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"), + defaultTab == "fontinspector"); + + this.fontInspector = new FontInspector(this, this.panelWin); + this.sidebar.toggleTab(true, "fontinspector"); + } + + // Setup the splitter before the sidebar is displayed so, + // we don't miss any events. + this.setupSplitter(); + + this.sidebar.show(defaultTab); + }, + + /** + * Register a side-panel tab. This API can be used outside of + * DevTools (e.g. from an extension) as well as by DevTools + * code base. + * + * @param {string} tab uniq id + * @param {string} title tab title + * @param {React.Component} panel component. See `InspectorPanelTab` as an example. + * @param {boolean} selected true if the panel should be selected + */ + addSidebarTab: function (id, title, panel, selected) { + this.sidebar.addTab(id, title, panel, selected); + }, + + setupToolbar: function () { + this.teardownToolbar(); + + // Setup the sidebar toggle button. + let SidebarToggle = this.React.createFactory(this.browserRequire( + "devtools/client/shared/components/sidebar-toggle")); + + let sidebarToggle = SidebarToggle({ + onClick: this.onPaneToggleButtonClicked, + collapsed: false, + expandPaneTitle: INSPECTOR_L10N.getStr("inspector.expandPane"), + collapsePaneTitle: INSPECTOR_L10N.getStr("inspector.collapsePane"), + }); + + let parentBox = this.panelDoc.getElementById("inspector-sidebar-toggle-box"); + this._sidebarToggle = this.ReactDOM.render(sidebarToggle, parentBox); + + // Setup the add-node button. + this.addNode = this.addNode.bind(this); + this.addNodeButton = this.panelDoc.getElementById("inspector-element-add-button"); + this.addNodeButton.addEventListener("click", this.addNode); + + // Setup the eye-dropper icon if we're in an HTML document and we have actor support. + if (this.selection.nodeFront && this.selection.nodeFront.isInHTMLDocument) { + this.target.actorHasMethod("inspector", "pickColorFromPage").then(value => { + if (!value) { + return; + } + + this.onEyeDropperDone = this.onEyeDropperDone.bind(this); + this.onEyeDropperButtonClicked = this.onEyeDropperButtonClicked.bind(this); + this.eyeDropperButton = this.panelDoc + .getElementById("inspector-eyedropper-toggle"); + this.eyeDropperButton.disabled = false; + this.eyeDropperButton.title = INSPECTOR_L10N.getStr("inspector.eyedropper.label"); + this.eyeDropperButton.addEventListener("click", this.onEyeDropperButtonClicked); + }, e => console.error(e)); + } else { + let eyeDropperButton = this.panelDoc.getElementById("inspector-eyedropper-toggle"); + eyeDropperButton.disabled = true; + eyeDropperButton.title = INSPECTOR_L10N.getStr("eyedropper.disabled.title"); + } + }, + + teardownToolbar: function () { + this._sidebarToggle = null; + + if (this.addNodeButton) { + this.addNodeButton.removeEventListener("click", this.addNode); + this.addNodeButton = null; + } + + if (this.eyeDropperButton) { + this.eyeDropperButton.removeEventListener("click", this.onEyeDropperButtonClicked); + this.eyeDropperButton = null; + } + }, + + /** + * Reset the inspector on new root mutation. + */ + onNewRoot: function () { + this._defaultNode = null; + this.selection.setNodeFront(null); + this._destroyMarkup(); + this.isDirty = false; + + let onNodeSelected = defaultNode => { + // Cancel this promise resolution as a new one had + // been queued up. + if (this._pendingSelection != onNodeSelected) { + return; + } + this._pendingSelection = null; + this.selection.setNodeFront(defaultNode, "navigateaway"); + + this._initMarkup(); + this.once("markuploaded", () => { + if (!this.markup) { + return; + } + this.markup.expandNode(this.selection.nodeFront); + this.emit("new-root"); + }); + + // Setup the toolbar again, since its content may depend on the current document. + this.setupToolbar(); + }; + this._pendingSelection = onNodeSelected; + this._getDefaultNodeForSelection() + .then(onNodeSelected, this._handleRejectionIfNotDestroyed); + }, + + _selectionCssSelector: null, + + /** + * Set the currently selected node unique css selector. + * Will store the current target url along with it to allow pre-selection at + * reload + */ + set selectionCssSelector(cssSelector = null) { + if (this._panelDestroyer) { + return; + } + + this._selectionCssSelector = { + selector: cssSelector, + url: this._target.url + }; + }, + + /** + * Get the current selection unique css selector if any, that is, if a node + * is actually selected and that node has been selected while on the same url + */ + get selectionCssSelector() { + if (this._selectionCssSelector && + this._selectionCssSelector.url === this._target.url) { + return this._selectionCssSelector.selector; + } + return null; + }, + + /** + * Can a new HTML element be inserted into the currently selected element? + * @return {Boolean} + */ + canAddHTMLChild: function () { + let selection = this.selection; + + // Don't allow to insert an element into these elements. This should only + // contain elements where walker.insertAdjacentHTML has no effect. + let invalidTagNames = ["html", "iframe"]; + + return selection.isHTMLNode() && + selection.isElementNode() && + !selection.isPseudoElementNode() && + !selection.isAnonymousNode() && + invalidTagNames.indexOf( + selection.nodeFront.nodeName.toLowerCase()) === -1; + }, + + /** + * When a new node is selected. + */ + onNewSelection: function (event, value, reason) { + if (reason === "selection-destroy") { + return; + } + + // Wait for all the known tools to finish updating and then let the + // client know. + let selection = this.selection.nodeFront; + + // Update the state of the add button in the toolbar depending on the + // current selection. + let btn = this.panelDoc.querySelector("#inspector-element-add-button"); + if (this.canAddHTMLChild()) { + btn.removeAttribute("disabled"); + } else { + btn.setAttribute("disabled", "true"); + } + + // On any new selection made by the user, store the unique css selector + // of the selected node so it can be restored after reload of the same page + if (this.canGetUniqueSelector && + this.selection.isElementNode()) { + selection.getUniqueSelector().then(selector => { + this.selectionCssSelector = selector; + }, this._handleRejectionIfNotDestroyed); + } + + let selfUpdate = this.updating("inspector-panel"); + executeSoon(() => { + try { + selfUpdate(selection); + } catch (ex) { + console.error(ex); + } + }); + }, + + /** + * Delay the "inspector-updated" notification while a tool + * is updating itself. Returns a function that must be + * invoked when the tool is done updating with the node + * that the tool is viewing. + */ + updating: function (name) { + if (this._updateProgress && this._updateProgress.node != this.selection.nodeFront) { + this.cancelUpdate(); + } + + if (!this._updateProgress) { + // Start an update in progress. + let self = this; + this._updateProgress = { + node: this.selection.nodeFront, + outstanding: new Set(), + checkDone: function () { + if (this !== self._updateProgress) { + return; + } + // Cancel update if there is no `selection` anymore. + // It can happen if the inspector panel is already destroyed. + if (!self.selection || (this.node !== self.selection.nodeFront)) { + self.cancelUpdate(); + return; + } + if (this.outstanding.size !== 0) { + return; + } + + self._updateProgress = null; + self.emit("inspector-updated", name); + }, + }; + } + + let progress = this._updateProgress; + let done = function () { + progress.outstanding.delete(done); + progress.checkDone(); + }; + progress.outstanding.add(done); + return done; + }, + + /** + * Cancel notification of inspector updates. + */ + cancelUpdate: function () { + this._updateProgress = null; + }, + + /** + * When a node is deleted, select its parent node or the defaultNode if no + * parent is found (may happen when deleting an iframe inside which the + * node was selected). + */ + onDetached: function (event, parentNode) { + this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode)); + this.selection.setNodeFront(parentNode ? parentNode : this._defaultNode, "detached"); + }, + + /** + * Destroy the inspector. + */ + destroy: function () { + if (this._panelDestroyer) { + return this._panelDestroyer; + } + + if (this.walker) { + this.walker.off("new-root", this.onNewRoot); + this.pageStyle = null; + } + + this.cancelUpdate(); + + this.target.off("will-navigate", this._onBeforeNavigate); + + this.target.off("thread-paused", this.updateDebuggerPausedWarning); + this.target.off("thread-resumed", this.updateDebuggerPausedWarning); + this._toolbox.off("select", this.updateDebuggerPausedWarning); + + if (this.ruleview) { + this.ruleview.destroy(); + } + + if (this.computedview) { + this.computedview.destroy(); + } + + if (this.layoutview) { + this.layoutview.destroy(); + } + + if (this.fontInspector) { + this.fontInspector.destroy(); + } + + let cssPropertiesDestroyer = this._cssPropertiesLoaded.then(({front}) => { + if (front) { + front.destroy(); + } + }); + + this.sidebar.off("select", this._setDefaultSidebar); + let sidebarDestroyer = this.sidebar.destroy(); + + this.teardownSplitter(); + + this.sidebar = null; + + this.teardownToolbar(); + this.breadcrumbs.destroy(); + this.selection.off("new-node-front", this.onNewSelection); + this.selection.off("detached-front", this.onDetached); + let markupDestroyer = this._destroyMarkup(); + this.panelWin.inspector = null; + this.target = null; + this.panelDoc = null; + this.panelWin = null; + this.breadcrumbs = null; + this._toolbox = null; + this.search.destroy(); + this.search = null; + this.searchBox = null; + + this._panelDestroyer = promise.all([ + sidebarDestroyer, + markupDestroyer, + cssPropertiesDestroyer + ]); + + return this._panelDestroyer; + }, + + /** + * Returns the clipboard content if it is appropriate for pasting + * into the current node's outer HTML, otherwise returns null. + */ + _getClipboardContentForPaste: function () { + let flavors = clipboardHelper.getCurrentFlavors(); + if (flavors.indexOf("text") != -1 || + (flavors.indexOf("html") != -1 && flavors.indexOf("image") == -1)) { + let content = clipboardHelper.getData(); + if (content && content.trim().length > 0) { + return content; + } + } + return null; + }, + + _onContextMenu: function (e) { + e.preventDefault(); + this._openMenu({ + screenX: e.screenX, + screenY: e.screenY, + target: e.target, + }); + }, + + /** + * This is meant to be called by all the search, filter, inplace text boxes in the + * inspector, and just calls through to the toolbox openTextBoxContextMenu helper. + * @param {DOMEvent} e + */ + onTextBoxContextMenu: function (e) { + e.stopPropagation(); + e.preventDefault(); + this.toolbox.openTextBoxContextMenu(e.screenX, e.screenY); + }, + + _openMenu: function ({ target, screenX = 0, screenY = 0 } = { }) { + let markupContainer = this.markup.getContainer(this.selection.nodeFront); + + this.contextMenuTarget = target; + this.nodeMenuTriggerInfo = markupContainer && + markupContainer.editor.getInfoAtNode(target); + + let isSelectionElement = this.selection.isElementNode() && + !this.selection.isPseudoElementNode(); + let isEditableElement = isSelectionElement && + !this.selection.isAnonymousNode(); + let isDuplicatableElement = isSelectionElement && + !this.selection.isAnonymousNode() && + !this.selection.isRoot(); + let isScreenshotable = isSelectionElement && + this.canGetUniqueSelector && + this.selection.nodeFront.isTreeDisplayed; + + let menu = new Menu(); + menu.append(new MenuItem({ + id: "node-menu-edithtml", + label: INSPECTOR_L10N.getStr("inspectorHTMLEdit.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorHTMLEdit.accesskey"), + disabled: !isEditableElement || !this.isOuterHTMLEditable, + click: () => this.editHTML(), + })); + menu.append(new MenuItem({ + id: "node-menu-add", + label: INSPECTOR_L10N.getStr("inspectorAddNode.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorAddNode.accesskey"), + disabled: !this.canAddHTMLChild(), + click: () => this.addNode(), + })); + menu.append(new MenuItem({ + id: "node-menu-duplicatenode", + label: INSPECTOR_L10N.getStr("inspectorDuplicateNode.label"), + hidden: !this._supportsDuplicateNode, + disabled: !isDuplicatableElement, + click: () => this.duplicateNode(), + })); + menu.append(new MenuItem({ + id: "node-menu-delete", + label: INSPECTOR_L10N.getStr("inspectorHTMLDelete.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorHTMLDelete.accesskey"), + disabled: !isEditableElement, + click: () => this.deleteNode(), + })); + + menu.append(new MenuItem({ + label: INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.label"), + accesskey: + INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.accesskey"), + submenu: this._getAttributesSubmenu(isEditableElement), + })); + + menu.append(new MenuItem({ + type: "separator", + })); + + // Set the pseudo classes + for (let name of ["hover", "active", "focus"]) { + let menuitem = new MenuItem({ + id: "node-menu-pseudo-" + name, + label: name, + type: "checkbox", + click: this.togglePseudoClass.bind(this, ":" + name), + }); + + if (isSelectionElement) { + let checked = this.selection.nodeFront.hasPseudoClassLock(":" + name); + menuitem.checked = checked; + } else { + menuitem.disabled = true; + } + + menu.append(menuitem); + } + + menu.append(new MenuItem({ + type: "separator", + })); + + let copySubmenu = new Menu(); + copySubmenu.append(new MenuItem({ + id: "node-menu-copyinner", + label: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.accesskey"), + disabled: !isSelectionElement, + click: () => this.copyInnerHTML(), + })); + copySubmenu.append(new MenuItem({ + id: "node-menu-copyouter", + label: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.accesskey"), + disabled: !isSelectionElement, + click: () => this.copyOuterHTML(), + })); + copySubmenu.append(new MenuItem({ + id: "node-menu-copyuniqueselector", + label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"), + accesskey: + INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"), + disabled: !isSelectionElement, + hidden: !this.canGetUniqueSelector, + click: () => this.copyUniqueSelector(), + })); + copySubmenu.append(new MenuItem({ + id: "node-menu-copyimagedatauri", + label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"), + disabled: !isSelectionElement || !markupContainer || + !markupContainer.isPreviewable(), + click: () => this.copyImageDataUri(), + })); + + menu.append(new MenuItem({ + label: INSPECTOR_L10N.getStr("inspectorCopyHTMLSubmenu.label"), + submenu: copySubmenu, + })); + + menu.append(new MenuItem({ + label: INSPECTOR_L10N.getStr("inspectorPasteHTMLSubmenu.label"), + submenu: this._getPasteSubmenu(isEditableElement), + })); + + menu.append(new MenuItem({ + type: "separator", + })); + + let isNodeWithChildren = this.selection.isNode() && + markupContainer.hasChildren; + menu.append(new MenuItem({ + id: "node-menu-expand", + label: INSPECTOR_L10N.getStr("inspectorExpandNode.label"), + disabled: !isNodeWithChildren, + click: () => this.expandNode(), + })); + menu.append(new MenuItem({ + id: "node-menu-collapse", + label: INSPECTOR_L10N.getStr("inspectorCollapseNode.label"), + disabled: !isNodeWithChildren || !markupContainer.expanded, + click: () => this.collapseNode(), + })); + + menu.append(new MenuItem({ + type: "separator", + })); + + menu.append(new MenuItem({ + id: "node-menu-scrollnodeintoview", + label: INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.label"), + accesskey: + INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.accesskey"), + hidden: !this._supportsScrollIntoView, + disabled: !isSelectionElement, + click: () => this.scrollNodeIntoView(), + })); + menu.append(new MenuItem({ + id: "node-menu-screenshotnode", + label: INSPECTOR_L10N.getStr("inspectorScreenshotNode.label"), + disabled: !isScreenshotable, + click: () => this.screenshotNode(), + })); + menu.append(new MenuItem({ + id: "node-menu-useinconsole", + label: INSPECTOR_L10N.getStr("inspectorUseInConsole.label"), + click: () => this.useInConsole(), + })); + menu.append(new MenuItem({ + id: "node-menu-showdomproperties", + label: INSPECTOR_L10N.getStr("inspectorShowDOMProperties.label"), + click: () => this.showDOMProperties(), + })); + + let nodeLinkMenuItems = this._getNodeLinkMenuItems(); + if (nodeLinkMenuItems.filter(item => item.visible).length > 0) { + menu.append(new MenuItem({ + id: "node-menu-link-separator", + type: "separator", + })); + } + + for (let menuitem of nodeLinkMenuItems) { + menu.append(menuitem); + } + + menu.popup(screenX, screenY, this._toolbox); + return menu; + }, + + _getPasteSubmenu: function (isEditableElement) { + let isPasteable = isEditableElement && this._getClipboardContentForPaste(); + let disableAdjacentPaste = !isPasteable || + !this.canPasteInnerOrAdjacentHTML || this.selection.isRoot() || + this.selection.isBodyNode() || this.selection.isHeadNode(); + let disableFirstLastPaste = !isPasteable || + !this.canPasteInnerOrAdjacentHTML || (this.selection.isHTMLNode() && + this.selection.isRoot()); + + let pasteSubmenu = new Menu(); + pasteSubmenu.append(new MenuItem({ + id: "node-menu-pasteinnerhtml", + label: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.accesskey"), + disabled: !isPasteable || !this.canPasteInnerOrAdjacentHTML, + click: () => this.pasteInnerHTML(), + })); + pasteSubmenu.append(new MenuItem({ + id: "node-menu-pasteouterhtml", + label: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.accesskey"), + disabled: !isPasteable || !this.isOuterHTMLEditable, + click: () => this.pasteOuterHTML(), + })); + pasteSubmenu.append(new MenuItem({ + id: "node-menu-pastebefore", + label: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.label"), + accesskey: + INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.accesskey"), + disabled: disableAdjacentPaste, + click: () => this.pasteAdjacentHTML("beforeBegin"), + })); + pasteSubmenu.append(new MenuItem({ + id: "node-menu-pasteafter", + label: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.label"), + accesskey: + INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.accesskey"), + disabled: disableAdjacentPaste, + click: () => this.pasteAdjacentHTML("afterEnd"), + })); + pasteSubmenu.append(new MenuItem({ + id: "node-menu-pastefirstchild", + label: INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.label"), + accesskey: + INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.accesskey"), + disabled: disableFirstLastPaste, + click: () => this.pasteAdjacentHTML("afterBegin"), + })); + pasteSubmenu.append(new MenuItem({ + id: "node-menu-pastelastchild", + label: INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.label"), + accesskey: + INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.accesskey"), + disabled: disableFirstLastPaste, + click: () => this.pasteAdjacentHTML("beforeEnd"), + })); + + return pasteSubmenu; + }, + + _getAttributesSubmenu: function (isEditableElement) { + let attributesSubmenu = new Menu(); + let nodeInfo = this.nodeMenuTriggerInfo; + let isAttributeClicked = isEditableElement && nodeInfo && + nodeInfo.type === "attribute"; + + attributesSubmenu.append(new MenuItem({ + id: "node-menu-add-attribute", + label: INSPECTOR_L10N.getStr("inspectorAddAttribute.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorAddAttribute.accesskey"), + disabled: !isEditableElement, + click: () => this.onAddAttribute(), + })); + attributesSubmenu.append(new MenuItem({ + id: "node-menu-edit-attribute", + label: INSPECTOR_L10N.getFormatStr("inspectorEditAttribute.label", + isAttributeClicked ? `"${nodeInfo.name}"` : ""), + accesskey: INSPECTOR_L10N.getStr("inspectorEditAttribute.accesskey"), + disabled: !isAttributeClicked, + click: () => this.onEditAttribute(), + })); + + attributesSubmenu.append(new MenuItem({ + id: "node-menu-remove-attribute", + label: INSPECTOR_L10N.getFormatStr("inspectorRemoveAttribute.label", + isAttributeClicked ? `"${nodeInfo.name}"` : ""), + accesskey: + INSPECTOR_L10N.getStr("inspectorRemoveAttribute.accesskey"), + disabled: !isAttributeClicked, + click: () => this.onRemoveAttribute(), + })); + + return attributesSubmenu; + }, + + /** + * Link menu items can be shown or hidden depending on the context and + * selected node, and their labels can vary. + * + * @return {Array} list of visible menu items related to links. + */ + _getNodeLinkMenuItems: function () { + let linkFollow = new MenuItem({ + id: "node-menu-link-follow", + visible: false, + click: () => this.onFollowLink(), + }); + let linkCopy = new MenuItem({ + id: "node-menu-link-copy", + visible: false, + click: () => this.onCopyLink(), + }); + + // Get information about the right-clicked node. + let popupNode = this.contextMenuTarget; + if (!popupNode || !popupNode.classList.contains("link")) { + return [linkFollow, linkCopy]; + } + + let type = popupNode.dataset.type; + if (this._supportsResolveRelativeURL && + (type === "uri" || type === "cssresource" || type === "jsresource")) { + // Links can't be opened in new tabs in the browser toolbox. + if (type === "uri" && !this.target.chrome) { + linkFollow.visible = true; + linkFollow.label = INSPECTOR_L10N.getStr( + "inspector.menu.openUrlInNewTab.label"); + } else if (type === "cssresource") { + linkFollow.visible = true; + linkFollow.label = TOOLBOX_L10N.getStr( + "toolbox.viewCssSourceInStyleEditor.label"); + } else if (type === "jsresource") { + linkFollow.visible = true; + linkFollow.label = TOOLBOX_L10N.getStr( + "toolbox.viewJsSourceInDebugger.label"); + } + + linkCopy.visible = true; + linkCopy.label = INSPECTOR_L10N.getStr( + "inspector.menu.copyUrlToClipboard.label"); + } else if (type === "idref") { + linkFollow.visible = true; + linkFollow.label = INSPECTOR_L10N.getFormatStr( + "inspector.menu.selectElement.label", popupNode.dataset.link); + } + + return [linkFollow, linkCopy]; + }, + + _initMarkup: function () { + let doc = this.panelDoc; + + this._markupBox = doc.getElementById("markup-box"); + + // create tool iframe + this._markupFrame = doc.createElement("iframe"); + this._markupFrame.setAttribute("flex", "1"); + this._markupFrame.setAttribute("tooltip", "aHTMLTooltip"); + this._markupFrame.addEventListener("contextmenu", this._onContextMenu); + + // This is needed to enable tooltips inside the iframe document. + this._markupFrame.addEventListener("load", this._onMarkupFrameLoad, true); + + this._markupBox.setAttribute("collapsed", true); + this._markupBox.appendChild(this._markupFrame); + this._markupFrame.setAttribute("src", "chrome://devtools/content/inspector/markup/markup.xhtml"); + this._markupFrame.setAttribute("aria-label", + INSPECTOR_L10N.getStr("inspector.panelLabel.markupView")); + }, + + _onMarkupFrameLoad: function () { + this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true); + + this._markupFrame.contentWindow.focus(); + + this._markupBox.removeAttribute("collapsed"); + + this.markup = new MarkupView(this, this._markupFrame, this._toolbox.win); + + this.emit("markuploaded"); + }, + + _destroyMarkup: function () { + let destroyPromise; + + if (this._markupFrame) { + this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true); + this._markupFrame.removeEventListener("contextmenu", this._onContextMenu); + } + + if (this.markup) { + destroyPromise = this.markup.destroy(); + this.markup = null; + } else { + destroyPromise = promise.resolve(); + } + + if (this._markupFrame) { + this._markupFrame.parentNode.removeChild(this._markupFrame); + this._markupFrame = null; + } + + this._markupBox = null; + + return destroyPromise; + }, + + /** + * When the pane toggle button is clicked or pressed, toggle the pane, change the button + * state and tooltip. + */ + onPaneToggleButtonClicked: function (e) { + let sidePaneContainer = this.panelDoc.querySelector( + "#inspector-splitter-box .controlled"); + let isVisible = !this._sidebarToggle.state.collapsed; + + // Make sure the sidebar has width and height attributes before collapsing + // because ViewHelpers needs it. + if (isVisible) { + let rect = sidePaneContainer.getBoundingClientRect(); + if (!sidePaneContainer.hasAttribute("width")) { + sidePaneContainer.setAttribute("width", rect.width); + } + // always refresh the height attribute before collapsing, it could have + // been modified by resizing the container. + sidePaneContainer.setAttribute("height", rect.height); + } + + let onAnimationDone = () => { + if (isVisible) { + this._sidebarToggle.setState({collapsed: true}); + } else { + this._sidebarToggle.setState({collapsed: false}); + } + }; + + ViewHelpers.togglePane({ + visible: !isVisible, + animated: true, + delayed: true, + callback: onAnimationDone + }, sidePaneContainer); + }, + + onEyeDropperButtonClicked: function () { + this.eyeDropperButton.hasAttribute("checked") + ? this.hideEyeDropper() + : this.showEyeDropper(); + }, + + startEyeDropperListeners: function () { + this.inspector.once("color-pick-canceled", this.onEyeDropperDone); + this.inspector.once("color-picked", this.onEyeDropperDone); + this.walker.once("new-root", this.onEyeDropperDone); + }, + + stopEyeDropperListeners: function () { + this.inspector.off("color-pick-canceled", this.onEyeDropperDone); + this.inspector.off("color-picked", this.onEyeDropperDone); + this.walker.off("new-root", this.onEyeDropperDone); + }, + + onEyeDropperDone: function () { + this.eyeDropperButton.removeAttribute("checked"); + this.stopEyeDropperListeners(); + }, + + /** + * Show the eyedropper on the page. + * @return {Promise} resolves when the eyedropper is visible. + */ + showEyeDropper: function () { + // The eyedropper button doesn't exist, most probably because the actor doesn't + // support the pickColorFromPage, or because the page isn't HTML. + if (!this.eyeDropperButton) { + return null; + } + + this.telemetry.toolOpened("toolbareyedropper"); + this.eyeDropperButton.setAttribute("checked", "true"); + this.startEyeDropperListeners(); + return this.inspector.pickColorFromPage(this.toolbox, {copyOnSelect: true}) + .catch(e => console.error(e)); + }, + + /** + * Hide the eyedropper. + * @return {Promise} resolves when the eyedropper is hidden. + */ + hideEyeDropper: function () { + // The eyedropper button doesn't exist, most probably because the actor doesn't + // support the pickColorFromPage, or because the page isn't HTML. + if (!this.eyeDropperButton) { + return null; + } + + this.eyeDropperButton.removeAttribute("checked"); + this.stopEyeDropperListeners(); + return this.inspector.cancelPickColorFromPage() + .catch(e => console.error(e)); + }, + + /** + * Create a new node as the last child of the current selection, expand the + * parent and select the new node. + */ + addNode: Task.async(function* () { + if (!this.canAddHTMLChild()) { + return; + } + + let html = "<div></div>"; + + // Insert the html and expect a childList markup mutation. + let onMutations = this.once("markupmutation"); + let {nodes} = yield this.walker.insertAdjacentHTML(this.selection.nodeFront, + "beforeEnd", html); + yield onMutations; + + // Select the new node (this will auto-expand its parent). + this.selection.setNodeFront(nodes[0], "node-inserted"); + }), + + /** + * Toggle a pseudo class. + */ + togglePseudoClass: function (pseudo) { + if (this.selection.isElementNode()) { + let node = this.selection.nodeFront; + if (node.hasPseudoClassLock(pseudo)) { + return this.walker.removePseudoClassLock(node, pseudo, {parents: true}); + } + + let hierarchical = pseudo == ":hover" || pseudo == ":active"; + return this.walker.addPseudoClassLock(node, pseudo, {parents: hierarchical}); + } + return promise.resolve(); + }, + + /** + * Show DOM properties + */ + showDOMProperties: function () { + this._toolbox.openSplitConsole().then(() => { + let panel = this._toolbox.getPanel("webconsole"); + let jsterm = panel.hud.jsterm; + + jsterm.execute("inspect($0)"); + jsterm.focus(); + }); + }, + + /** + * Use in Console. + * + * Takes the currently selected node in the inspector and assigns it to a + * temp variable on the content window. Also opens the split console and + * autofills it with the temp variable. + */ + useInConsole: function () { + this._toolbox.openSplitConsole().then(() => { + let panel = this._toolbox.getPanel("webconsole"); + let jsterm = panel.hud.jsterm; + + let evalString = `{ let i = 0; + while (window.hasOwnProperty("temp" + i) && i < 1000) { + i++; + } + window["temp" + i] = $0; + "temp" + i; + }`; + + let options = { + selectedNodeActor: this.selection.nodeFront.actorID, + }; + jsterm.requestEvaluation(evalString, options).then((res) => { + jsterm.setInputValue(res.result); + this.emit("console-var-ready"); + }); + }); + }, + + /** + * Edit the outerHTML of the selected Node. + */ + editHTML: function () { + if (!this.selection.isNode()) { + return; + } + if (this.markup) { + this.markup.beginEditingOuterHTML(this.selection.nodeFront); + } + }, + + /** + * Paste the contents of the clipboard into the selected Node's outer HTML. + */ + pasteOuterHTML: function () { + let content = this._getClipboardContentForPaste(); + if (!content) { + return promise.reject("No clipboard content for paste"); + } + + let node = this.selection.nodeFront; + return this.markup.getNodeOuterHTML(node).then(oldContent => { + this.markup.updateNodeOuterHTML(node, content, oldContent); + }); + }, + + /** + * Paste the contents of the clipboard into the selected Node's inner HTML. + */ + pasteInnerHTML: function () { + let content = this._getClipboardContentForPaste(); + if (!content) { + return promise.reject("No clipboard content for paste"); + } + + let node = this.selection.nodeFront; + return this.markup.getNodeInnerHTML(node).then(oldContent => { + this.markup.updateNodeInnerHTML(node, content, oldContent); + }); + }, + + /** + * Paste the contents of the clipboard as adjacent HTML to the selected Node. + * @param position + * The position as specified for Element.insertAdjacentHTML + * (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd"). + */ + pasteAdjacentHTML: function (position) { + let content = this._getClipboardContentForPaste(); + if (!content) { + return promise.reject("No clipboard content for paste"); + } + + let node = this.selection.nodeFront; + return this.markup.insertAdjacentHTMLToNode(node, position, content); + }, + + /** + * Copy the innerHTML of the selected Node to the clipboard. + */ + copyInnerHTML: function () { + if (!this.selection.isNode()) { + return; + } + this._copyLongString(this.walker.innerHTML(this.selection.nodeFront)); + }, + + /** + * Copy the outerHTML of the selected Node to the clipboard. + */ + copyOuterHTML: function () { + if (!this.selection.isNode()) { + return; + } + let node = this.selection.nodeFront; + + switch (node.nodeType) { + case nodeConstants.ELEMENT_NODE : + this._copyLongString(this.walker.outerHTML(node)); + break; + case nodeConstants.COMMENT_NODE : + this._getLongString(node.getNodeValue()).then(comment => { + clipboardHelper.copyString("<!--" + comment + "-->"); + }); + break; + case nodeConstants.DOCUMENT_TYPE_NODE : + clipboardHelper.copyString(node.doctypeString); + break; + } + }, + + /** + * Copy the data-uri for the currently selected image in the clipboard. + */ + copyImageDataUri: function () { + let container = this.markup.getContainer(this.selection.nodeFront); + if (container && container.isPreviewable()) { + container.copyImageDataUri(); + } + }, + + /** + * Copy the content of a longString (via a promise resolving a + * LongStringActor) to the clipboard + * @param {Promise} longStringActorPromise + * promise expected to resolve a LongStringActor instance + * @return {Promise} promise resolving (with no argument) when the + * string is sent to the clipboard + */ + _copyLongString: function (longStringActorPromise) { + return this._getLongString(longStringActorPromise).then(string => { + clipboardHelper.copyString(string); + }).catch(e => console.error(e)); + }, + + /** + * Retrieve the content of a longString (via a promise resolving a LongStringActor) + * @param {Promise} longStringActorPromise + * promise expected to resolve a LongStringActor instance + * @return {Promise} promise resolving with the retrieved string as argument + */ + _getLongString: function (longStringActorPromise) { + return longStringActorPromise.then(longStringActor => { + return longStringActor.string().then(string => { + longStringActor.release().catch(e => console.error(e)); + return string; + }); + }).catch(e => console.error(e)); + }, + + /** + * Copy a unique selector of the selected Node to the clipboard. + */ + copyUniqueSelector: function () { + if (!this.selection.isNode()) { + return; + } + + this.selection.nodeFront.getUniqueSelector().then((selector) => { + clipboardHelper.copyString(selector); + }).then(null, console.error); + }, + + /** + * Initiate gcli screenshot command on selected node + */ + screenshotNode: function () { + CommandUtils.createRequisition(this._target, { + environment: CommandUtils.createEnvironment(this, "_target") + }).then(requisition => { + // Bug 1180314 - CssSelector might contain white space so need to make sure it is + // passed to screenshot as a single parameter. More work *might* be needed if + // CssSelector could contain escaped single- or double-quotes, backslashes, etc. + requisition.updateExec("screenshot --selector '" + this.selectionCssSelector + "'"); + }); + }, + + /** + * Scroll the node into view. + */ + scrollNodeIntoView: function () { + if (!this.selection.isNode()) { + return; + } + + this.selection.nodeFront.scrollIntoView(); + }, + + /** + * Duplicate the selected node + */ + duplicateNode: function () { + let selection = this.selection; + if (!selection.isElementNode() || + selection.isRoot() || + selection.isAnonymousNode() || + selection.isPseudoElementNode()) { + return; + } + this.walker.duplicateNode(selection.nodeFront).catch(e => console.error(e)); + }, + + /** + * Delete the selected node. + */ + deleteNode: function () { + if (!this.selection.isNode() || + this.selection.isRoot()) { + return; + } + + // If the markup panel is active, use the markup panel to delete + // the node, making this an undoable action. + if (this.markup) { + this.markup.deleteNode(this.selection.nodeFront); + } else { + // remove the node from content + this.walker.removeNode(this.selection.nodeFront); + } + }, + + /** + * Add attribute to node. + * Used for node context menu and shouldn't be called directly. + */ + onAddAttribute: function () { + let container = this.markup.getContainer(this.selection.nodeFront); + container.addAttribute(); + }, + + /** + * Edit attribute for node. + * Used for node context menu and shouldn't be called directly. + */ + onEditAttribute: function () { + let container = this.markup.getContainer(this.selection.nodeFront); + container.editAttribute(this.nodeMenuTriggerInfo.name); + }, + + /** + * Remove attribute from node. + * Used for node context menu and shouldn't be called directly. + */ + onRemoveAttribute: function () { + let container = this.markup.getContainer(this.selection.nodeFront); + container.removeAttribute(this.nodeMenuTriggerInfo.name); + }, + + expandNode: function () { + this.markup.expandAll(this.selection.nodeFront); + }, + + collapseNode: function () { + this.markup.collapseNode(this.selection.nodeFront); + }, + + /** + * This method is here for the benefit of the node-menu-link-follow menu item + * in the inspector contextual-menu. + */ + onFollowLink: function () { + let type = this.contextMenuTarget.dataset.type; + let link = this.contextMenuTarget.dataset.link; + + this.followAttributeLink(type, link); + }, + + /** + * Given a type and link found in a node's attribute in the markup-view, + * attempt to follow that link (which may result in opening a new tab, the + * style editor or debugger). + */ + followAttributeLink: function (type, link) { + if (!type || !link) { + return; + } + + if (type === "uri" || type === "cssresource" || type === "jsresource") { + // Open link in a new tab. + // When the inspector menu was setup on click (see _getNodeLinkMenuItems), we + // already checked that resolveRelativeURL existed. + this.inspector.resolveRelativeURL( + link, this.selection.nodeFront).then(url => { + if (type === "uri") { + let browserWin = this.target.tab.ownerDocument.defaultView; + browserWin.openUILinkIn(url, "tab"); + } else if (type === "cssresource") { + return this.toolbox.viewSourceInStyleEditor(url); + } else if (type === "jsresource") { + return this.toolbox.viewSourceInDebugger(url); + } + return null; + }).catch(e => console.error(e)); + } else if (type == "idref") { + // Select the node in the same document. + this.walker.document(this.selection.nodeFront).then(doc => { + return this.walker.querySelector(doc, "#" + CSS.escape(link)).then(node => { + if (!node) { + this.emit("idref-attribute-link-failed"); + return; + } + this.selection.setNodeFront(node); + }); + }).catch(e => console.error(e)); + } + }, + + /** + * This method is here for the benefit of the node-menu-link-copy menu item + * in the inspector contextual-menu. + */ + onCopyLink: function () { + let link = this.contextMenuTarget.dataset.link; + + this.copyAttributeLink(link); + }, + + /** + * This method is here for the benefit of copying links. + */ + copyAttributeLink: function (link) { + // When the inspector menu was setup on click (see _getNodeLinkMenuItems), we + // already checked that resolveRelativeURL existed. + this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => { + clipboardHelper.copyString(url); + }, console.error); + } +}; + +// URL constructor doesn't support chrome: scheme +let href = window.location.href.replace(/chrome:/, "http://"); +let url = new window.URL(href); + +// Only use this method to attach the toolbox if some query parameters are given +if (url.search.length > 1) { + const { targetFromURL } = require("devtools/client/framework/target-from-url"); + const { attachThread } = require("devtools/client/framework/attach-thread"); + const { BrowserLoader } = + Cu.import("resource://devtools/client/shared/browser-loader.js", {}); + + const { Selection } = require("devtools/client/framework/selection"); + const { InspectorFront } = require("devtools/shared/fronts/inspector"); + const { getHighlighterUtils } = require("devtools/client/framework/toolbox-highlighter-utils"); + + Task.spawn(function* () { + let target = yield targetFromURL(url); + + let notImplemented = function () { + throw new Error("Not implemented in a tab"); + }; + let fakeToolbox = { + target, + hostType: "bottom", + doc: window.document, + win: window, + on() {}, emit() {}, off() {}, + initInspector() {}, + browserRequire: BrowserLoader({ + window: window, + useOnlyShared: true + }).require, + get React() { + return this.browserRequire("devtools/client/shared/vendor/react"); + }, + get ReactDOM() { + return this.browserRequire("devtools/client/shared/vendor/react-dom"); + }, + isToolRegistered() { + return false; + }, + currentToolId: "inspector", + getCurrentPanel() { + return "inspector"; + }, + get textboxContextMenuPopup() { + notImplemented(); + }, + getPanel: notImplemented, + openSplitConsole: notImplemented, + viewCssSourceInStyleEditor: notImplemented, + viewJsSourceInDebugger: notImplemented, + viewSource: notImplemented, + viewSourceInDebugger: notImplemented, + viewSourceInStyleEditor: notImplemented, + + // For attachThread: + highlightTool() {}, + unhighlightTool() {}, + selectTool() {}, + raise() {}, + getNotificationBox() {} + }; + + // attachThread also expect a toolbox as argument + fakeToolbox.threadClient = yield attachThread(fakeToolbox); + + let inspector = InspectorFront(target.client, target.form); + let showAllAnonymousContent = + Services.prefs.getBoolPref("devtools.inspector.showAllAnonymousContent"); + let walker = yield inspector.getWalker({ showAllAnonymousContent }); + let selection = new Selection(walker); + let highlighter = yield inspector.getHighlighter(false); + + fakeToolbox.inspector = inspector; + fakeToolbox.walker = walker; + fakeToolbox.selection = selection; + fakeToolbox.highlighter = highlighter; + fakeToolbox.highlighterUtils = getHighlighterUtils(fakeToolbox); + + let inspectorUI = new Inspector(fakeToolbox); + inspectorUI.init(); + }).then(null, e => { + window.alert("Unable to start the inspector:" + e.message + "\n" + e.stack); + }); +} diff --git a/devtools/client/inspector/inspector.xhtml b/devtools/client/inspector/inspector.xhtml new file mode 100644 index 000000000..f43f6bb8c --- /dev/null +++ b/devtools/client/inspector/inspector.xhtml @@ -0,0 +1,231 @@ +<?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/. --> +<!DOCTYPE html> + +<html xmlns="http://www.w3.org/1999/xhtml" dir=""> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + + <link rel="stylesheet" href="chrome://devtools/skin/widgets.css"/> + <link rel="stylesheet" href="chrome://devtools/skin/inspector.css"/> + <link rel="stylesheet" href="chrome://devtools/skin/rules.css"/> + <link rel="stylesheet" href="chrome://devtools/skin/computed.css"/> + <link rel="stylesheet" href="chrome://devtools/skin/fonts.css"/> + <link rel="stylesheet" href="chrome://devtools/skin/boxmodel.css"/> + <link rel="stylesheet" href="chrome://devtools/skin/layout.css"/> + <link rel="stylesheet" href="chrome://devtools/skin/animationinspector.css"/> + <link rel="stylesheet" href="resource://devtools/client/shared/components/sidebar-toggle.css"/> + <link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/tabs.css"/> + <link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/tabbar.css"/> + <link rel="stylesheet" href="resource://devtools/client/inspector/components/inspector-tab-panel.css"/> + <link rel="stylesheet" href="resource://devtools/client/shared/components/splitter/split-box.css"/> + <link rel="stylesheet" href="resource://devtools/client/inspector/layout/components/Accordion.css"/> + + <script type="application/javascript;version=1.8" + src="chrome://devtools/content/shared/theme-switching.js"></script> + <script type="application/javascript;version=1.8" src="inspector.js" defer="true"></script> +</head> +<body class="theme-body" role="application"> + <div class="inspector-responsive-container theme-body inspector"> + + <!-- Main Panel Content --> + <div id="inspector-main-content" class="devtools-main-content"> + <div id="inspector-toolbar" class="devtools-toolbar" nowindowdrag="true" + data-localization-bundle="devtools/client/locales/inspector.properties"> + <button id="inspector-element-add-button" class="devtools-button" + data-localization="title=inspectorAddNode.label"></button> + <div class="devtools-toolbar-spacer"></div> + <span id="inspector-searchlabel"></span> + <div id="inspector-search" class="devtools-searchbox has-clear-btn"> + <input id="inspector-searchbox" class="devtools-searchinput" + type="search" + data-localization="placeholder=inspectorSearchHTML.label3"/> + <button id="inspector-searchinput-clear" class="devtools-searchinput-clear" tabindex="-1"></button> + </div> + <button id="inspector-eyedropper-toggle" + class="devtools-button command-button-invertable"></button> + <div id="inspector-sidebar-toggle-box"></div> + </div> + <div id="markup-box"></div> + <div id="inspector-breadcrumbs-toolbar" class="devtools-toolbar"> + <div id="inspector-breadcrumbs" class="breadcrumbs-widget-container" + role="group" data-localization="aria-label=inspector.breadcrumbs.label" tabindex="0"></div> + </div> + </div> + + <!-- Splitter --> + <div + xmlns="http://www.w3.org/1999/xhtml" + id="inspector-splitter-box"> + </div> + + <!-- Sidebar Container --> + <div id="inspector-sidebar-container"> + <div + xmlns="http://www.w3.org/1999/xhtml" + id="inspector-sidebar" + hidden="true"></div> + </div> + + <!-- Sidebar panel definitions --> + <div id="tabpanels" style="visibility:collapse"> + <div id="sidebar-panel-ruleview" class="devtools-monospace theme-sidebar inspector-tabpanel" + data-localization-bundle="devtools/client/locales/inspector.properties"> + <div id="ruleview-toolbar-container" class="devtools-toolbar"> + <div id="ruleview-toolbar"> + <div class="devtools-searchbox has-clear-btn"> + <input id="ruleview-searchbox" + class="devtools-filterinput devtools-rule-searchbox" + type="search" + data-localization="placeholder=inspector.filterStyles.placeholder"/> + <button id="ruleview-searchinput-clear" class="devtools-searchinput-clear"></button> + </div> + <div id="ruleview-command-toolbar"> + <button id="ruleview-add-rule-button" data-localization="title=inspector.addRule.tooltip" class="devtools-button"></button> + <button id="pseudo-class-panel-toggle" data-localization="title=inspector.togglePseudo.tooltip" class="devtools-button"></button> + </div> + </div> + <div id="pseudo-class-panel" hidden="true"> + <label><input id="pseudo-hover-toggle" type="checkbox" value=":hover" tabindex="-1" />:hover</label> + <label><input id="pseudo-active-toggle" type="checkbox" value=":active" tabindex="-1" />:active</label> + <label><input id="pseudo-focus-toggle" type="checkbox" value=":focus" tabindex="-1" />:focus</label> + </div> + </div> + + <div id="ruleview-container" class="ruleview"> + <div id="ruleview-container-focusable" tabindex="-1"> + </div> + </div> + </div> + + <div id="sidebar-panel-computedview" class="devtools-monospace theme-sidebar inspector-tabpanel" + data-localization-bundle="devtools/client/locales/inspector.properties"> + <div id="computedview-toolbar" class="devtools-toolbar"> + <div class="devtools-searchbox has-clear-btn"> + <input id="computedview-searchbox" + class="devtools-filterinput devtools-rule-searchbox" + type="search" + data-localization="placeholder=inspector.filterStyles.placeholder"/> + <button id="computedview-searchinput-clear" class="devtools-searchinput-clear"></button> + </div> + <input id="browser-style-checkbox" + type="checkbox" + class="includebrowserstyles"/> + <label id="browser-style-checkbox-label" for="browser-style-checkbox" + data-localization="content=inspector.browserStyles.label"></label> + </div> + + <div id="computedview-container"> + <div id="computedview-container-focusable" tabindex="-1"> + <div id="boxmodel-wrapper" tabindex="0" + data-localization-bundle="devtools/client/locales/boxmodel.properties"> + <div id="boxmodel-header"> + <div id="boxmodel-expander" class="expander theme-twisty expandable" open=""></div> + <span data-localization="content=boxmodel.title"></span> + </div> + + <div id="boxmodel-container"> + <div id="boxmodel-main"> + <span class="boxmodel-legend" data-box="margin" data-localization="content=boxmodel.margin;title=boxmodel.margin"></span> + <div id="boxmodel-margins" data-box="margin" data-localization="title=boxmodel.margin"> + <span class="boxmodel-legend" data-box="border" data-localization="content=boxmodel.border;title=boxmodel.border"></span> + <div id="boxmodel-borders" data-box="border" data-localization="title=boxmodel.border"> + <span class="boxmodel-legend" data-box="padding" data-localization="content=boxmodel.padding;title=boxmodel.padding"></span> + <div id="boxmodel-padding" data-box="padding" data-localization="title=boxmodel.padding"> + <div id="boxmodel-content" data-box="content" data-localization="title=boxmodel.content"> + </div> + </div> + </div> + </div> + + <p class="boxmodel-margin boxmodel-top"><span data-box="margin" class="boxmodel-editable" title="margin-top"></span></p> + <p class="boxmodel-margin boxmodel-right"><span data-box="margin" class="boxmodel-editable" title="margin-right"></span></p> + <p class="boxmodel-margin boxmodel-bottom"><span data-box="margin" class="boxmodel-editable" title="margin-bottom"></span></p> + <p class="boxmodel-margin boxmodel-left"><span data-box="margin" class="boxmodel-editable" title="margin-left"></span></p> + + <p class="boxmodel-border boxmodel-top"><span data-box="border" class="boxmodel-editable" title="border-top"></span></p> + <p class="boxmodel-border boxmodel-right"><span data-box="border" class="boxmodel-editable" title="border-right"></span></p> + <p class="boxmodel-border boxmodel-bottom"><span data-box="border" class="boxmodel-editable" title="border-bottom"></span></p> + <p class="boxmodel-border boxmodel-left"><span data-box="border" class="boxmodel-editable" title="border-left"></span></p> + + <p class="boxmodel-padding boxmodel-top"><span data-box="padding" class="boxmodel-editable" title="padding-top"></span></p> + <p class="boxmodel-padding boxmodel-right"><span data-box="padding" class="boxmodel-editable" title="padding-right"></span></p> + <p class="boxmodel-padding boxmodel-bottom"><span data-box="padding" class="boxmodel-editable" title="padding-bottom"></span></p> + <p class="boxmodel-padding boxmodel-left"><span data-box="padding" class="boxmodel-editable" title="padding-left"></span></p> + + <p class="boxmodel-size"> + <span data-box="content" data-localization="title=boxmodel.content"></span> + </p> + </div> + + <div id="boxmodel-info"> + <span id="boxmodel-element-size"></span> + <section id="boxmodel-position-group"> + <button class="devtools-button" id="layout-geometry-editor" + data-localization="title=boxmodel.geometryButton.tooltip"></button> + <span id="boxmodel-element-position"></span> + </section> + </div> + + <div style="display: none"> + <p id="boxmodel-dummy"></p> + </div> + </div> + </div> + + <div id="propertyContainer" class="theme-separator" tabindex="0"> + </div> + + <div id="computedview-no-results" hidden="" data-localization="content=inspector.noProperties"></div> + </div> + </div> + </div> + + <div id="sidebar-panel-fontinspector" class="devtools-monospace theme-sidebar inspector-tabpanel" + data-localization-bundle="devtools/client/locales/font-inspector.properties"> + <div class="devtools-toolbar"> + <div class="devtools-searchbox"> + <input id="font-preview-text-input" class="devtools-textinput" type="search" + data-localization="placeholder=fontinspector.previewText"/> + </div> + <label id="font-showall" class="theme-link" + data-localization="content=fontinspector.seeAll; + title=fontinspector.seeAll.tooltip"></label> + </div> + + <div id="font-container"> + <ul id="all-fonts"></ul> + </div> + + <div id="font-template"> + <section class="font"> + <div class="font-preview-container"> + <img class="font-preview"></img> + </div> + <div class="font-info"> + <h1 class="font-name"></h1> + <span class="font-is-local" data-localization="content=fontinspector.system"></span> + <span class="font-is-remote" data-localization="content=fontinspector.remote"></span> + <p class="font-format-url"> + <input readonly="readonly" class="font-url"></input> + <span class="font-format"></span> + </p> + <p class="font-css"> + <span data-localization="content=fontinspector.usedAs"></span> "<span class="font-css-name"></span>" + </p> + <pre class="font-css-code"></pre> + </div> + </section> + </div> + </div> + + <div id="sidebar-panel-animationinspector" class="devtools-monospace theme-sidebar inspector-tabpanel"> + <iframe class="devtools-inspector-tab-frame"></iframe> + </div> + </div> + + </div> +</body> +</html> diff --git a/devtools/client/inspector/layout/actions/index.js b/devtools/client/inspector/layout/actions/index.js new file mode 100644 index 000000000..66735cddb --- /dev/null +++ b/devtools/client/inspector/layout/actions/index.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; diff --git a/devtools/client/inspector/layout/actions/moz.build b/devtools/client/inspector/layout/actions/moz.build new file mode 100644 index 000000000..568f361a5 --- /dev/null +++ b/devtools/client/inspector/layout/actions/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. diff --git a/devtools/client/inspector/layout/components/Accordion.css b/devtools/client/inspector/layout/components/Accordion.css new file mode 100644 index 000000000..4076d30fa --- /dev/null +++ b/devtools/client/inspector/layout/components/Accordion.css @@ -0,0 +1,42 @@ +/* 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/. */ + +/** + * This file should not be modified and is a duplicate from the debugger.html project. + * Any changes to this file should be imported from the upstream debugger.html project. + */ + +.accordion { + background-color: var(--theme-body-background); + width: 100%; +} + +.accordion ._header { + background-color: var(--theme-toolbar-background); + border-bottom: 1px solid var(--theme-splitter-color); + cursor: pointer; + font-size: 11px; + padding: 5px; + transition: all 0.25s ease; + width: 100%; + -moz-user-select: none; +} + +.accordion ._header:hover { + background-color: var(--theme-selection-color); +} + +.accordion ._header:hover svg { + fill: var(--theme-comment-alt); +} + +.accordion ._content { + border-bottom: 1px solid var(--theme-splitter-color); + font-size: 11px; +} + +.arrow { + vertical-align: middle; + display: inline-block; +} diff --git a/devtools/client/inspector/layout/components/Accordion.js b/devtools/client/inspector/layout/components/Accordion.js new file mode 100644 index 000000000..d69dc3c7e --- /dev/null +++ b/devtools/client/inspector/layout/components/Accordion.js @@ -0,0 +1,82 @@ +/* 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/. */ + +/** + * This file should not be modified and is a duplicate from the debugger.html project. + * Any changes to this file should be imported from the upstream debugger.html project. + */ + +"use strict"; + +const React = require("devtools/client/shared/vendor/react"); +const { DOM: dom, PropTypes } = React; + +const { div, span } = dom; + +const Accordion = React.createClass({ + displayName: "Accordion", + + propTypes: { + items: PropTypes.array + }, + + getInitialState: function () { + return { opened: this.props.items.map(item => item.opened), + created: [] }; + }, + + handleHeaderClick: function (i) { + const opened = [...this.state.opened]; + const created = [...this.state.created]; + const item = this.props.items[i]; + + opened[i] = !opened[i]; + created[i] = true; + + if (opened[i] && item.onOpened) { + item.onOpened(); + } + + this.setState({ opened, created }); + }, + + renderContainer: function (item, i) { + const { opened, created } = this.state; + const containerClassName = + item.header.toLowerCase().replace(/\s/g, "-") + "-pane"; + let arrowClassName = "arrow theme-twisty"; + if (opened[i]) { + arrowClassName += " open"; + } + + return div( + { className: containerClassName, key: i }, + + div( + { className: "_header", + onClick: () => this.handleHeaderClick(i) }, + span({ className: arrowClassName }), + item.header + ), + + (created[i] || opened[i]) ? + div( + { className: "_content", + style: { display: opened[i] ? "block" : "none" } + }, + React.createElement(item.component, item.componentProps || {}) + ) : + null + ); + }, + + render: function () { + return div( + { className: "accordion" }, + this.props.items.map(this.renderContainer) + ); + } +}); + +module.exports = Accordion; diff --git a/devtools/client/inspector/layout/components/App.js b/devtools/client/inspector/layout/components/App.js new file mode 100644 index 000000000..887175b99 --- /dev/null +++ b/devtools/client/inspector/layout/components/App.js @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { getStr } = require("../utils/l10n"); +const { DOM: dom, createClass, createFactory } = require("devtools/client/shared/vendor/react"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); + +const Accordion = createFactory(require("./Accordion")); +const Grid = createFactory(require("./Grid")); + +const App = createClass({ + + displayName: "App", + + render() { + return dom.div( + { + id: "layoutview-container", + }, + Accordion({ + items: [ + { header: getStr("layout.header"), + component: Grid, + opened: true } + ] + }) + ); + }, + +}); + +module.exports = connect(state => state)(App); diff --git a/devtools/client/inspector/layout/components/Grid.js b/devtools/client/inspector/layout/components/Grid.js new file mode 100644 index 000000000..8081ce9ef --- /dev/null +++ b/devtools/client/inspector/layout/components/Grid.js @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { getStr } = require("../utils/l10n"); +const { DOM: dom, createClass } = require("devtools/client/shared/vendor/react"); + +const Grid = createClass({ + + displayName: "Grid", + + render() { + return dom.div( + { + id: "layoutview-grid-container", + }, + dom.div( + { + className: "layoutview-no-grids" + }, + getStr("layout.noGrids") + ) + ); + }, + +}); + +module.exports = Grid; diff --git a/devtools/client/inspector/layout/components/moz.build b/devtools/client/inspector/layout/components/moz.build new file mode 100644 index 000000000..0ae19f4f6 --- /dev/null +++ b/devtools/client/inspector/layout/components/moz.build @@ -0,0 +1,12 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DevToolsModules( + 'Accordion.css', + 'Accordion.js', + 'App.js', + 'Grid.js', +) diff --git a/devtools/client/inspector/layout/layout.js b/devtools/client/inspector/layout/layout.js new file mode 100644 index 000000000..cf0955b27 --- /dev/null +++ b/devtools/client/inspector/layout/layout.js @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Services = require("Services"); +const { createFactory, createElement } = require("devtools/client/shared/vendor/react"); +const { Provider } = require("devtools/client/shared/vendor/react-redux"); + +const App = createFactory(require("./components/App")); +const Store = require("./store"); + +const { LocalizationHelper } = require("devtools/shared/l10n"); +const INSPECTOR_L10N = + new LocalizationHelper("devtools/client/locales/inspector.properties"); + +function LayoutView(inspector, window) { + this.inspector = inspector; + this.document = window.document; + this.store = null; + + this.init(); +} + +LayoutView.prototype = { + + init() { + let store = this.store = Store(); + let provider = createElement(Provider, { + store, + id: "layoutview", + title: INSPECTOR_L10N.getStr("inspector.sidebar.layoutViewTitle"), + key: "layoutview", + }, App()); + + let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar"); + + this.inspector.addSidebarTab( + "layoutview", + INSPECTOR_L10N.getStr("inspector.sidebar.layoutViewTitle"), + provider, + defaultTab == "layoutview" + ); + }, + + destroy() { + this.inspector = null; + this.document = null; + this.store = null; + }, +}; + +exports.LayoutView = LayoutView; + diff --git a/devtools/client/inspector/layout/moz.build b/devtools/client/inspector/layout/moz.build new file mode 100644 index 000000000..8575deedf --- /dev/null +++ b/devtools/client/inspector/layout/moz.build @@ -0,0 +1,18 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DIRS += [ + 'actions', + 'components', + 'reducers', + 'utils', +] + +DevToolsModules( + 'layout.js', + 'store.js', + 'types.js', +) diff --git a/devtools/client/inspector/layout/reducers/grids.js b/devtools/client/inspector/layout/reducers/grids.js new file mode 100644 index 000000000..3a1c26fd4 --- /dev/null +++ b/devtools/client/inspector/layout/reducers/grids.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const INITIAL_GRIDS = { + +}; + +let reducers = { + +}; + +module.exports = function (grids = INITIAL_GRIDS, action) { + let reducer = reducers[action.type]; + if (!reducer) { + return grids; + } + return reducer(grids, action); +}; diff --git a/devtools/client/inspector/layout/reducers/index.js b/devtools/client/inspector/layout/reducers/index.js new file mode 100644 index 000000000..3fed406d7 --- /dev/null +++ b/devtools/client/inspector/layout/reducers/index.js @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +exports.grids = require("./grids"); diff --git a/devtools/client/inspector/layout/reducers/moz.build b/devtools/client/inspector/layout/reducers/moz.build new file mode 100644 index 000000000..7c6955914 --- /dev/null +++ b/devtools/client/inspector/layout/reducers/moz.build @@ -0,0 +1,10 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DevToolsModules( + 'grids.js', + 'index.js', +) diff --git a/devtools/client/inspector/layout/store.js b/devtools/client/inspector/layout/store.js new file mode 100644 index 000000000..5069dda26 --- /dev/null +++ b/devtools/client/inspector/layout/store.js @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { combineReducers } = require("devtools/client/shared/vendor/redux"); +const createStore = require("devtools/client/shared/redux/create-store"); +const reducers = require("./reducers/index"); +const flags = require("devtools/shared/flags"); + +module.exports = function () { + let shouldLog = false; + let history; + + // If testing, store the action history in an array + // we'll later attach to the store + if (flags.testing) { + history = []; + shouldLog = true; + } + + let store = createStore({ + log: shouldLog, + history + })(combineReducers(reducers), {}); + + if (history) { + store.history = history; + } + + return store; +}; diff --git a/devtools/client/inspector/layout/types.js b/devtools/client/inspector/layout/types.js new file mode 100644 index 000000000..66735cddb --- /dev/null +++ b/devtools/client/inspector/layout/types.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; diff --git a/devtools/client/inspector/layout/utils/l10n.js b/devtools/client/inspector/layout/utils/l10n.js new file mode 100644 index 000000000..96ab21768 --- /dev/null +++ b/devtools/client/inspector/layout/utils/l10n.js @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { LocalizationHelper } = require("devtools/shared/l10n"); +const L10N = new LocalizationHelper("devtools/client/locales/layout.properties"); + +module.exports = { + getStr: (...args) => L10N.getStr(...args), + getFormatStr: (...args) => L10N.getFormatStr(...args), + getFormatStrWithNumbers: (...args) => L10N.getFormatStrWithNumbers(...args), + numberWithDecimals: (...args) => L10N.numberWithDecimals(...args), +}; diff --git a/devtools/client/inspector/layout/utils/moz.build b/devtools/client/inspector/layout/utils/moz.build new file mode 100644 index 000000000..e3053b63f --- /dev/null +++ b/devtools/client/inspector/layout/utils/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DevToolsModules( + 'l10n.js', +) diff --git a/devtools/client/inspector/markup/markup.js b/devtools/client/inspector/markup/markup.js new file mode 100644 index 000000000..d6e9f8c11 --- /dev/null +++ b/devtools/client/inspector/markup/markup.js @@ -0,0 +1,1878 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const promise = require("promise"); +const Services = require("Services"); +const defer = require("devtools/shared/defer"); +const {Task} = require("devtools/shared/task"); +const nodeConstants = require("devtools/shared/dom-node-constants"); +const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants"); +const EventEmitter = require("devtools/shared/event-emitter"); +const {LocalizationHelper} = require("devtools/shared/l10n"); +const {PluralForm} = require("devtools/shared/plural-form"); +const {template} = require("devtools/shared/gcli/templater"); +const {AutocompletePopup} = require("devtools/client/shared/autocomplete-popup"); +const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts"); +const {scrollIntoViewIfNeeded} = require("devtools/client/shared/scroll"); +const {UndoStack} = require("devtools/client/shared/undo"); +const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip"); +const {PrefObserver} = require("devtools/client/styleeditor/utils"); +const HTMLEditor = require("devtools/client/inspector/markup/views/html-editor"); +const MarkupElementContainer = require("devtools/client/inspector/markup/views/element-container"); +const MarkupReadOnlyContainer = require("devtools/client/inspector/markup/views/read-only-container"); +const MarkupTextContainer = require("devtools/client/inspector/markup/views/text-container"); +const RootContainer = require("devtools/client/inspector/markup/views/root-container"); + +const INSPECTOR_L10N = + new LocalizationHelper("devtools/client/locales/inspector.properties"); + +// Page size for pageup/pagedown +const PAGE_SIZE = 10; +const DEFAULT_MAX_CHILDREN = 100; +const NEW_SELECTION_HIGHLIGHTER_TIMER = 1000; +const DRAG_DROP_AUTOSCROLL_EDGE_MAX_DISTANCE = 50; +const DRAG_DROP_AUTOSCROLL_EDGE_RATIO = 0.1; +const DRAG_DROP_MIN_AUTOSCROLL_SPEED = 2; +const DRAG_DROP_MAX_AUTOSCROLL_SPEED = 8; +const DRAG_DROP_HEIGHT_TO_SPEED = 500; +const DRAG_DROP_HEIGHT_TO_SPEED_MIN = 0.5; +const DRAG_DROP_HEIGHT_TO_SPEED_MAX = 1; +const ATTR_COLLAPSE_ENABLED_PREF = "devtools.markup.collapseAttributes"; +const ATTR_COLLAPSE_LENGTH_PREF = "devtools.markup.collapseAttributeLength"; + +/** + * Vocabulary for the purposes of this file: + * + * MarkupContainer - the structure that holds an editor and its + * immediate children in the markup panel. + * - MarkupElementContainer: markup container for element nodes + * - MarkupTextContainer: markup container for text / comment nodes + * - MarkupReadonlyContainer: markup container for other nodes + * Node - A content node. + * object.elt - A UI element in the markup panel. + */ + +/** + * The markup tree. Manages the mapping of nodes to MarkupContainers, + * updating based on mutations, and the undo/redo bindings. + * + * @param {Inspector} inspector + * The inspector we're watching. + * @param {iframe} frame + * An iframe in which the caller has kindly loaded markup.xhtml. + */ +function MarkupView(inspector, frame, controllerWindow) { + this.inspector = inspector; + this.walker = this.inspector.walker; + this._frame = frame; + this.win = this._frame.contentWindow; + this.doc = this._frame.contentDocument; + this._elt = this.doc.querySelector("#root"); + this.htmlEditor = new HTMLEditor(this.doc); + + try { + this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize"); + } catch (ex) { + this.maxChildren = DEFAULT_MAX_CHILDREN; + } + + this.collapseAttributes = + Services.prefs.getBoolPref(ATTR_COLLAPSE_ENABLED_PREF); + this.collapseAttributeLength = + Services.prefs.getIntPref(ATTR_COLLAPSE_LENGTH_PREF); + + // Creating the popup to be used to show CSS suggestions. + // The popup will be attached to the toolbox document. + this.popup = new AutocompletePopup(inspector.toolbox.doc, { + autoSelect: true, + theme: "auto", + }); + + this.undo = new UndoStack(); + this.undo.installController(controllerWindow); + + this._containers = new Map(); + + // Binding functions that need to be called in scope. + this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this); + this._mutationObserver = this._mutationObserver.bind(this); + this._onDisplayChange = this._onDisplayChange.bind(this); + this._onMouseClick = this._onMouseClick.bind(this); + this._onMouseUp = this._onMouseUp.bind(this); + this._onNewSelection = this._onNewSelection.bind(this); + this._onCopy = this._onCopy.bind(this); + this._onFocus = this._onFocus.bind(this); + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseOut = this._onMouseOut.bind(this); + this._onToolboxPickerCanceled = this._onToolboxPickerCanceled.bind(this); + this._onToolboxPickerHover = this._onToolboxPickerHover.bind(this); + this._onCollapseAttributesPrefChange = + this._onCollapseAttributesPrefChange.bind(this); + this._isImagePreviewTarget = this._isImagePreviewTarget.bind(this); + this._onBlur = this._onBlur.bind(this); + + EventEmitter.decorate(this); + + // Listening to various events. + this._elt.addEventListener("click", this._onMouseClick, false); + this._elt.addEventListener("mousemove", this._onMouseMove, false); + this._elt.addEventListener("mouseout", this._onMouseOut, false); + this._elt.addEventListener("blur", this._onBlur, true); + this.win.addEventListener("mouseup", this._onMouseUp); + this.win.addEventListener("copy", this._onCopy); + this._frame.addEventListener("focus", this._onFocus, false); + this.walker.on("mutations", this._mutationObserver); + this.walker.on("display-change", this._onDisplayChange); + this.inspector.selection.on("new-node-front", this._onNewSelection); + this.toolbox.on("picker-canceled", this._onToolboxPickerCanceled); + this.toolbox.on("picker-node-hovered", this._onToolboxPickerHover); + + this._onNewSelection(); + this._initTooltips(); + + this._prefObserver = new PrefObserver("devtools.markup"); + this._prefObserver.on(ATTR_COLLAPSE_ENABLED_PREF, + this._onCollapseAttributesPrefChange); + this._prefObserver.on(ATTR_COLLAPSE_LENGTH_PREF, + this._onCollapseAttributesPrefChange); + + this._initShortcuts(); +} + +MarkupView.prototype = { + /** + * How long does a node flash when it mutates (in ms). + */ + CONTAINER_FLASHING_DURATION: 500, + + _selectedContainer: null, + + get toolbox() { + return this.inspector.toolbox; + }, + + /** + * Handle promise rejections for various asynchronous actions, and only log errors if + * the markup view still exists. + * This is useful to silence useless errors that happen when the markup view is + * destroyed while still initializing (and making protocol requests). + */ + _handleRejectionIfNotDestroyed: function (e) { + if (!this._destroyer) { + console.error(e); + } + }, + + _initTooltips: function () { + // The tooltips will be attached to the toolbox document. + this.eventDetailsTooltip = new HTMLTooltip(this.toolbox.doc, + {type: "arrow"}); + this.imagePreviewTooltip = new HTMLTooltip(this.toolbox.doc, + {type: "arrow", useXulWrapper: "true"}); + this._enableImagePreviewTooltip(); + }, + + _enableImagePreviewTooltip: function () { + this.imagePreviewTooltip.startTogglingOnHover(this._elt, + this._isImagePreviewTarget); + }, + + _disableImagePreviewTooltip: function () { + this.imagePreviewTooltip.stopTogglingOnHover(); + }, + + _onToolboxPickerHover: function (event, nodeFront) { + this.showNode(nodeFront).then(() => { + this._showContainerAsHovered(nodeFront); + }, e => console.error(e)); + }, + + /** + * If the element picker gets canceled, make sure and re-center the view on the + * current selected element. + */ + _onToolboxPickerCanceled: function () { + if (this._selectedContainer) { + scrollIntoViewIfNeeded(this._selectedContainer.editor.elt); + } + }, + + isDragging: false, + + _onMouseMove: function (event) { + let target = event.target; + + // Auto-scroll if we're dragging. + if (this.isDragging) { + event.preventDefault(); + this._autoScroll(event); + return; + } + + // Show the current container as hovered and highlight it. + // This requires finding the current MarkupContainer (walking up the DOM). + while (!target.container) { + if (target.tagName.toLowerCase() === "body") { + return; + } + target = target.parentNode; + } + + let container = target.container; + if (this._hoveredNode !== container.node) { + this._showBoxModel(container.node); + } + this._showContainerAsHovered(container.node); + + this.emit("node-hover"); + }, + + /** + * If focus is moved outside of the markup view document and there is a + * selected container, make its contents not focusable by a keyboard. + */ + _onBlur: function (event) { + if (!this._selectedContainer) { + return; + } + + let {relatedTarget} = event; + if (relatedTarget && relatedTarget.ownerDocument === this.doc) { + return; + } + + if (this._selectedContainer) { + this._selectedContainer.clearFocus(); + } + }, + + /** + * Executed on each mouse-move while a node is being dragged in the view. + * Auto-scrolls the view to reveal nodes below the fold to drop the dragged + * node in. + */ + _autoScroll: function (event) { + let docEl = this.doc.documentElement; + + if (this._autoScrollAnimationFrame) { + this.win.cancelAnimationFrame(this._autoScrollAnimationFrame); + } + + // Auto-scroll when the mouse approaches top/bottom edge. + let fromBottom = docEl.clientHeight - event.pageY + this.win.scrollY; + let fromTop = event.pageY - this.win.scrollY; + let edgeDistance = Math.min(DRAG_DROP_AUTOSCROLL_EDGE_MAX_DISTANCE, + docEl.clientHeight * DRAG_DROP_AUTOSCROLL_EDGE_RATIO); + + // The smaller the screen, the slower the movement. + let heightToSpeedRatio = + Math.max(DRAG_DROP_HEIGHT_TO_SPEED_MIN, + Math.min(DRAG_DROP_HEIGHT_TO_SPEED_MAX, + docEl.clientHeight / DRAG_DROP_HEIGHT_TO_SPEED)); + + if (fromBottom <= edgeDistance) { + // Map our distance range to a speed range so that the speed is not too + // fast or too slow. + let speed = map( + fromBottom, + 0, edgeDistance, + DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED); + + this._runUpdateLoop(() => { + docEl.scrollTop -= heightToSpeedRatio * + (speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED); + }); + } + + if (fromTop <= edgeDistance) { + let speed = map( + fromTop, + 0, edgeDistance, + DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED); + + this._runUpdateLoop(() => { + docEl.scrollTop += heightToSpeedRatio * + (speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED); + }); + } + }, + + /** + * Run a loop on the requestAnimationFrame. + */ + _runUpdateLoop: function (update) { + let loop = () => { + update(); + this._autoScrollAnimationFrame = this.win.requestAnimationFrame(loop); + }; + loop(); + }, + + _onMouseClick: function (event) { + // From the target passed here, let's find the parent MarkupContainer + // and ask it if the tooltip should be shown + let parentNode = event.target; + let container; + while (parentNode !== this.doc.body) { + if (parentNode.container) { + container = parentNode.container; + break; + } + parentNode = parentNode.parentNode; + } + + if (container instanceof MarkupElementContainer) { + // With the newly found container, delegate the tooltip content creation + // and decision to show or not the tooltip + container._buildEventTooltipContent(event.target, + this.eventDetailsTooltip); + } + }, + + _onMouseUp: function () { + this.indicateDropTarget(null); + this.indicateDragTarget(null); + if (this._autoScrollAnimationFrame) { + this.win.cancelAnimationFrame(this._autoScrollAnimationFrame); + } + }, + + _onCollapseAttributesPrefChange: function () { + this.collapseAttributes = + Services.prefs.getBoolPref(ATTR_COLLAPSE_ENABLED_PREF); + this.collapseAttributeLength = + Services.prefs.getIntPref(ATTR_COLLAPSE_LENGTH_PREF); + this.update(); + }, + + cancelDragging: function () { + if (!this.isDragging) { + return; + } + + for (let [, container] of this._containers) { + if (container.isDragging) { + container.cancelDragging(); + break; + } + } + + this.indicateDropTarget(null); + this.indicateDragTarget(null); + if (this._autoScrollAnimationFrame) { + this.win.cancelAnimationFrame(this._autoScrollAnimationFrame); + } + }, + + _hoveredNode: null, + + /** + * Show a NodeFront's container as being hovered + * + * @param {NodeFront} nodeFront + * The node to show as hovered + */ + _showContainerAsHovered: function (nodeFront) { + if (this._hoveredNode === nodeFront) { + return; + } + + if (this._hoveredNode) { + this.getContainer(this._hoveredNode).hovered = false; + } + + this.getContainer(nodeFront).hovered = true; + this._hoveredNode = nodeFront; + // Emit an event that the container view is actually hovered now, as this function + // can be called by an asynchronous caller. + this.emit("showcontainerhovered"); + }, + + _onMouseOut: function (event) { + // Emulate mouseleave by skipping any relatedTarget inside the markup-view. + if (this._elt.contains(event.relatedTarget)) { + return; + } + + if (this._autoScrollAnimationFrame) { + this.win.cancelAnimationFrame(this._autoScrollAnimationFrame); + } + if (this.isDragging) { + return; + } + + this._hideBoxModel(true); + if (this._hoveredNode) { + this.getContainer(this._hoveredNode).hovered = false; + } + this._hoveredNode = null; + + this.emit("leave"); + }, + + /** + * Show the box model highlighter on a given node front + * + * @param {NodeFront} nodeFront + * The node to show the highlighter for + * @return {Promise} Resolves when the highlighter for this nodeFront is + * shown, taking into account that there could already be highlighter + * requests queued up + */ + _showBoxModel: function (nodeFront) { + return this.toolbox.highlighterUtils.highlightNodeFront(nodeFront); + }, + + /** + * Hide the box model highlighter on a given node front + * + * @param {Boolean} forceHide + * See toolbox-highlighter-utils/unhighlight + * @return {Promise} Resolves when the highlighter for this nodeFront is + * hidden, taking into account that there could already be highlighter + * requests queued up + */ + _hideBoxModel: function (forceHide) { + return this.toolbox.highlighterUtils.unhighlight(forceHide); + }, + + _briefBoxModelTimer: null, + + _clearBriefBoxModelTimer: function () { + if (this._briefBoxModelTimer) { + clearTimeout(this._briefBoxModelTimer); + this._briefBoxModelPromise.resolve(); + this._briefBoxModelPromise = null; + this._briefBoxModelTimer = null; + } + }, + + _brieflyShowBoxModel: function (nodeFront) { + this._clearBriefBoxModelTimer(); + let onShown = this._showBoxModel(nodeFront); + this._briefBoxModelPromise = defer(); + + this._briefBoxModelTimer = setTimeout(() => { + this._hideBoxModel() + .then(this._briefBoxModelPromise.resolve, + this._briefBoxModelPromise.resolve); + }, NEW_SELECTION_HIGHLIGHTER_TIMER); + + return promise.all([onShown, this._briefBoxModelPromise.promise]); + }, + + template: function (name, dest, options = {stack: "markup.xhtml"}) { + let node = this.doc.getElementById("template-" + name).cloneNode(true); + node.removeAttribute("id"); + template(node, dest, options); + return node; + }, + + /** + * Get the MarkupContainer object for a given node, or undefined if + * none exists. + */ + getContainer: function (node) { + return this._containers.get(node); + }, + + update: function () { + let updateChildren = (node) => { + this.getContainer(node).update(); + for (let child of node.treeChildren()) { + updateChildren(child); + } + }; + + // Start with the documentElement + let documentElement; + for (let node of this._rootNode.treeChildren()) { + if (node.isDocumentElement === true) { + documentElement = node; + break; + } + } + + // Recursively update each node starting with documentElement. + updateChildren(documentElement); + }, + + /** + * Executed when the mouse hovers over a target in the markup-view and is used + * to decide whether this target should be used to display an image preview + * tooltip. + * Delegates the actual decision to the corresponding MarkupContainer instance + * if one is found. + * + * @return {Promise} the promise returned by + * MarkupElementContainer._isImagePreviewTarget + */ + _isImagePreviewTarget: Task.async(function* (target) { + // From the target passed here, let's find the parent MarkupContainer + // and ask it if the tooltip should be shown + if (this.isDragging) { + return false; + } + + let parent = target, container; + while (parent !== this.doc.body) { + if (parent.container) { + container = parent.container; + break; + } + parent = parent.parentNode; + } + + if (container instanceof MarkupElementContainer) { + // With the newly found container, delegate the tooltip content creation + // and decision to show or not the tooltip + return container.isImagePreviewTarget(target, this.imagePreviewTooltip); + } + + return false; + }), + + /** + * Given the known reason, should the current selection be briefly highlighted + * In a few cases, we don't want to highlight the node: + * - If the reason is null (used to reset the selection), + * - if it's "inspector-open" (when the inspector opens up, let's not + * highlight the default node) + * - if it's "navigateaway" (since the page is being navigated away from) + * - if it's "test" (this is a special case for mochitest. In tests, we often + * need to select elements but don't necessarily want the highlighter to come + * and go after a delay as this might break test scenarios) + * We also do not want to start a brief highlight timeout if the node is + * already being hovered over, since in that case it will already be + * highlighted. + */ + _shouldNewSelectionBeHighlighted: function () { + let reason = this.inspector.selection.reason; + let unwantedReasons = [ + "inspector-open", + "navigateaway", + "nodeselected", + "test" + ]; + let isHighlight = this._hoveredNode === this.inspector.selection.nodeFront; + return !isHighlight && reason && unwantedReasons.indexOf(reason) === -1; + }, + + /** + * React to new-node-front selection events. + * Highlights the node if needed, and make sure it is shown and selected in + * the view. + */ + _onNewSelection: function () { + let selection = this.inspector.selection; + + this.htmlEditor.hide(); + if (this._hoveredNode && this._hoveredNode !== selection.nodeFront) { + this.getContainer(this._hoveredNode).hovered = false; + this._hoveredNode = null; + } + + if (!selection.isNode()) { + this.unmarkSelectedNode(); + return; + } + + let done = this.inspector.updating("markup-view"); + let onShowBoxModel, onShow; + + // Highlight the element briefly if needed. + if (this._shouldNewSelectionBeHighlighted()) { + onShowBoxModel = this._brieflyShowBoxModel(selection.nodeFront); + } + + onShow = this.showNode(selection.nodeFront).then(() => { + // We could be destroyed by now. + if (this._destroyer) { + return promise.reject("markupview destroyed"); + } + + // Mark the node as selected. + this.markNodeAsSelected(selection.nodeFront); + + // Make sure the new selection is navigated to. + this.maybeNavigateToNewSelection(); + return undefined; + }).catch(this._handleRejectionIfNotDestroyed); + + promise.all([onShowBoxModel, onShow]).then(done); + }, + + /** + * Maybe make selected the current node selection's MarkupContainer depending + * on why the current node got selected. + */ + maybeNavigateToNewSelection: function () { + let {reason, nodeFront} = this.inspector.selection; + + // The list of reasons that should lead to navigating to the node. + let reasonsToNavigate = [ + // If the user picked an element with the element picker. + "picker-node-picked", + // If the user shift-clicked (previewed) an element. + "picker-node-previewed", + // If the user selected an element with the browser context menu. + "browser-context-menu", + // If the user added a new node by clicking in the inspector toolbar. + "node-inserted" + ]; + + if (reasonsToNavigate.includes(reason)) { + this.getContainer(this._rootNode).elt.focus(); + this.navigate(this.getContainer(nodeFront)); + } + }, + + /** + * Create a TreeWalker to find the next/previous + * node for selection. + */ + _selectionWalker: function (start) { + let walker = this.doc.createTreeWalker( + start || this._elt, + nodeFilterConstants.SHOW_ELEMENT, + function (element) { + if (element.container && + element.container.elt === element && + element.container.visible) { + return nodeFilterConstants.FILTER_ACCEPT; + } + return nodeFilterConstants.FILTER_SKIP; + } + ); + walker.currentNode = this._selectedContainer.elt; + return walker; + }, + + _onCopy: function (evt) { + // Ignore copy events from editors + if (this._isInputOrTextarea(evt.target)) { + return; + } + + let selection = this.inspector.selection; + if (selection.isNode()) { + this.inspector.copyOuterHTML(); + } + evt.stopPropagation(); + evt.preventDefault(); + }, + + /** + * Register all key shortcuts. + */ + _initShortcuts: function () { + let shortcuts = new KeyShortcuts({ + window: this.win, + }); + + this._onShortcut = this._onShortcut.bind(this); + + // Process localizable keys + ["markupView.hide.key", + "markupView.edit.key", + "markupView.scrollInto.key"].forEach(name => { + let key = INSPECTOR_L10N.getStr(name); + shortcuts.on(key, (_, event) => this._onShortcut(name, event)); + }); + + // Process generic keys: + ["Delete", "Backspace", "Home", "Left", "Right", "Up", "Down", "PageUp", + "PageDown", "Esc", "Enter", "Space"].forEach(key => { + shortcuts.on(key, this._onShortcut); + }); + }, + + /** + * Key shortcut listener. + */ + _onShortcut(name, event) { + if (this._isInputOrTextarea(event.target)) { + return; + } + switch (name) { + // Localizable keys + case "markupView.hide.key": { + let node = this._selectedContainer.node; + if (node.hidden) { + this.walker.unhideNode(node); + } else { + this.walker.hideNode(node); + } + break; + } + case "markupView.edit.key": { + this.beginEditingOuterHTML(this._selectedContainer.node); + break; + } + case "markupView.scrollInto.key": { + let selection = this._selectedContainer.node; + this.inspector.scrollNodeIntoView(selection); + break; + } + // Generic keys + case "Delete": { + this.deleteNodeOrAttribute(); + break; + } + case "Backspace": { + this.deleteNodeOrAttribute(true); + break; + } + case "Home": { + let rootContainer = this.getContainer(this._rootNode); + this.navigate(rootContainer.children.firstChild.container); + break; + } + case "Left": { + if (this._selectedContainer.expanded) { + this.collapseNode(this._selectedContainer.node); + } else { + let parent = this._selectionWalker().parentNode(); + if (parent) { + this.navigate(parent.container); + } + } + break; + } + case "Right": { + if (!this._selectedContainer.expanded && + this._selectedContainer.hasChildren) { + this._expandContainer(this._selectedContainer); + } else { + let next = this._selectionWalker().nextNode(); + if (next) { + this.navigate(next.container); + } + } + break; + } + case "Up": { + let previousNode = this._selectionWalker().previousNode(); + if (previousNode) { + this.navigate(previousNode.container); + } + break; + } + case "Down": { + let nextNode = this._selectionWalker().nextNode(); + if (nextNode) { + this.navigate(nextNode.container); + } + break; + } + case "PageUp": { + let walker = this._selectionWalker(); + let selection = this._selectedContainer; + for (let i = 0; i < PAGE_SIZE; i++) { + let previousNode = walker.previousNode(); + if (!previousNode) { + break; + } + selection = previousNode.container; + } + this.navigate(selection); + break; + } + case "PageDown": { + let walker = this._selectionWalker(); + let selection = this._selectedContainer; + for (let i = 0; i < PAGE_SIZE; i++) { + let nextNode = walker.nextNode(); + if (!nextNode) { + break; + } + selection = nextNode.container; + } + this.navigate(selection); + break; + } + case "Enter": + case "Space": { + if (!this._selectedContainer.canFocus) { + this._selectedContainer.canFocus = true; + this._selectedContainer.focus(); + } else { + // Return early to prevent cancelling the event. + return; + } + break; + } + case "Esc": { + if (this.isDragging) { + this.cancelDragging(); + } else { + // Return early to prevent cancelling the event when not + // dragging, to allow the split console to be toggled. + return; + } + break; + } + default: + console.error("Unexpected markup-view key shortcut", name); + return; + } + // Prevent default for this action + event.stopPropagation(); + event.preventDefault(); + }, + + /** + * Check if a node is an input or textarea + */ + _isInputOrTextarea: function (element) { + let name = element.tagName.toLowerCase(); + return name === "input" || name === "textarea"; + }, + + /** + * If there's an attribute on the current node that's currently focused, then + * delete this attribute, otherwise delete the node itself. + * + * @param {Boolean} moveBackward + * If set to true and if we're deleting the node, focus the previous + * sibling after deletion, otherwise the next one. + */ + deleteNodeOrAttribute: function (moveBackward) { + let focusedAttribute = this.doc.activeElement + ? this.doc.activeElement.closest(".attreditor") + : null; + if (focusedAttribute) { + // The focused attribute might not be in the current selected container. + let container = focusedAttribute.closest("li.child").container; + container.removeAttribute(focusedAttribute.dataset.attr); + } else { + this.deleteNode(this._selectedContainer.node, moveBackward); + } + }, + + /** + * Delete a node from the DOM. + * This is an undoable action. + * + * @param {NodeFront} node + * The node to remove. + * @param {Boolean} moveBackward + * If set to true, focus the previous sibling, otherwise the next one. + */ + deleteNode: function (node, moveBackward) { + if (node.isDocumentElement || + node.nodeType == nodeConstants.DOCUMENT_TYPE_NODE || + node.isAnonymous) { + return; + } + + let container = this.getContainer(node); + + // Retain the node so we can undo this... + this.walker.retainNode(node).then(() => { + let parent = node.parentNode(); + let nextSibling = null; + this.undo.do(() => { + this.walker.removeNode(node).then(siblings => { + nextSibling = siblings.nextSibling; + let prevSibling = siblings.previousSibling; + let focusNode = moveBackward ? prevSibling : nextSibling; + + // If we can't move as the user wants, we move to the other direction. + // If there is no sibling elements anymore, move to the parent node. + if (!focusNode) { + focusNode = nextSibling || prevSibling || parent; + } + + let isNextSiblingText = nextSibling ? + nextSibling.nodeType === nodeConstants.TEXT_NODE : false; + let isPrevSiblingText = prevSibling ? + prevSibling.nodeType === nodeConstants.TEXT_NODE : false; + + // If the parent had two children and the next or previous sibling + // is a text node, then it now has only a single text node, is about + // to be in-lined; and focus should move to the parent. + if (parent.numChildren === 2 + && (isNextSiblingText || isPrevSiblingText)) { + focusNode = parent; + } + + if (container.selected) { + this.navigate(this.getContainer(focusNode)); + } + }); + }, () => { + let isValidSibling = nextSibling && !nextSibling.isPseudoElement; + nextSibling = isValidSibling ? nextSibling : null; + this.walker.insertBefore(node, parent, nextSibling); + }); + }).then(null, console.error); + }, + + /** + * If an editable item is focused, select its container. + */ + _onFocus: function (event) { + let parent = event.target; + while (!parent.container) { + parent = parent.parentNode; + } + if (parent) { + this.navigate(parent.container); + } + }, + + /** + * Handle a user-requested navigation to a given MarkupContainer, + * updating the inspector's currently-selected node. + * + * @param {MarkupContainer} container + * The container we're navigating to. + */ + navigate: function (container) { + if (!container) { + return; + } + + let node = container.node; + this.markNodeAsSelected(node, "treepanel"); + }, + + /** + * Make sure a node is included in the markup tool. + * + * @param {NodeFront} node + * The node in the content document. + * @param {Boolean} flashNode + * Whether the newly imported node should be flashed + * @return {MarkupContainer} The MarkupContainer object for this element. + */ + importNode: function (node, flashNode) { + if (!node) { + return null; + } + + if (this._containers.has(node)) { + return this.getContainer(node); + } + + let container; + let {nodeType, isPseudoElement} = node; + if (node === this.walker.rootNode) { + container = new RootContainer(this, node); + this._elt.appendChild(container.elt); + this._rootNode = node; + } else if (nodeType == nodeConstants.ELEMENT_NODE && !isPseudoElement) { + container = new MarkupElementContainer(this, node, this.inspector); + } else if (nodeType == nodeConstants.COMMENT_NODE || + nodeType == nodeConstants.TEXT_NODE) { + container = new MarkupTextContainer(this, node, this.inspector); + } else { + container = new MarkupReadOnlyContainer(this, node, this.inspector); + } + + if (flashNode) { + container.flashMutation(); + } + + this._containers.set(node, container); + container.childrenDirty = true; + + this._updateChildren(container); + + this.inspector.emit("container-created", container); + + return container; + }, + + /** + * Mutation observer used for included nodes. + */ + _mutationObserver: function (mutations) { + for (let mutation of mutations) { + let type = mutation.type; + let target = mutation.target; + + if (mutation.type === "documentUnload") { + // Treat this as a childList change of the child (maybe the protocol + // should do this). + type = "childList"; + target = mutation.targetParent; + if (!target) { + continue; + } + } + + let container = this.getContainer(target); + if (!container) { + // Container might not exist if this came from a load event for a node + // we're not viewing. + continue; + } + + if (type === "attributes" && mutation.attributeName === "class") { + container.updateIsDisplayed(); + } + if (type === "attributes" || type === "characterData" + || type === "events" || type === "pseudoClassLock") { + container.update(); + } else if (type === "childList" || type === "nativeAnonymousChildList") { + container.childrenDirty = true; + // Update the children to take care of changes in the markup view DOM + // and update container (and its subtree) DOM tree depth level for + // accessibility where necessary. + this._updateChildren(container, {flash: true}).then(() => + container.updateLevel()); + } else if (type === "inlineTextChild") { + container.childrenDirty = true; + this._updateChildren(container, {flash: true}); + container.update(); + } + } + + this._waitForChildren().then(() => { + if (this._destroyer) { + // Could not fully update after markup mutations, the markup-view was destroyed + // while waiting for children. Bail out silently. + return; + } + this._flashMutatedNodes(mutations); + this.inspector.emit("markupmutation", mutations); + + // Since the htmlEditor is absolutely positioned, a mutation may change + // the location in which it should be shown. + this.htmlEditor.refresh(); + }); + }, + + /** + * React to display-change events from the walker + * + * @param {Array} nodes + * An array of nodeFronts + */ + _onDisplayChange: function (nodes) { + for (let node of nodes) { + let container = this.getContainer(node); + if (container) { + container.updateIsDisplayed(); + } + } + }, + + /** + * Given a list of mutations returned by the mutation observer, flash the + * corresponding containers to attract attention. + */ + _flashMutatedNodes: function (mutations) { + let addedOrEditedContainers = new Set(); + let removedContainers = new Set(); + + for (let {type, target, added, removed, newValue} of mutations) { + let container = this.getContainer(target); + + if (container) { + if (type === "characterData") { + addedOrEditedContainers.add(container); + } else if (type === "attributes" && newValue === null) { + // Removed attributes should flash the entire node. + // New or changed attributes will flash the attribute itself + // in ElementEditor.flashAttribute. + addedOrEditedContainers.add(container); + } else if (type === "childList") { + // If there has been removals, flash the parent + if (removed.length) { + removedContainers.add(container); + } + + // If there has been additions, flash the nodes if their associated + // container exist (so if their parent is expanded in the inspector). + added.forEach(node => { + let addedContainer = this.getContainer(node); + if (addedContainer) { + addedOrEditedContainers.add(addedContainer); + + // The node may be added as a result of an append, in which case + // it will have been removed from another container first, but in + // these cases we don't want to flash both the removal and the + // addition + removedContainers.delete(container); + } + }); + } + } + } + + for (let container of removedContainers) { + container.flashMutation(); + } + for (let container of addedOrEditedContainers) { + container.flashMutation(); + } + }, + + /** + * Make sure the given node's parents are expanded and the + * node is scrolled on to screen. + */ + showNode: function (node, centered = true) { + let parent = node; + + this.importNode(node); + + while ((parent = parent.parentNode())) { + this.importNode(parent); + this.expandNode(parent); + } + + return this._waitForChildren().then(() => { + if (this._destroyer) { + return promise.reject("markupview destroyed"); + } + return this._ensureVisible(node); + }).then(() => { + scrollIntoViewIfNeeded(this.getContainer(node).editor.elt, centered); + }, this._handleRejectionIfNotDestroyed); + }, + + /** + * Expand the container's children. + */ + _expandContainer: function (container) { + return this._updateChildren(container, {expand: true}).then(() => { + if (this._destroyer) { + // Could not expand the node, the markup-view was destroyed in the meantime. Just + // silently give up. + return; + } + container.setExpanded(true); + }); + }, + + /** + * Expand the node's children. + */ + expandNode: function (node) { + let container = this.getContainer(node); + this._expandContainer(container); + }, + + /** + * Expand the entire tree beneath a container. + * + * @param {MarkupContainer} container + * The container to expand. + */ + _expandAll: function (container) { + return this._expandContainer(container).then(() => { + let child = container.children.firstChild; + let promises = []; + while (child) { + promises.push(this._expandAll(child.container)); + child = child.nextSibling; + } + return promise.all(promises); + }).then(null, console.error); + }, + + /** + * Expand the entire tree beneath a node. + * + * @param {DOMNode} node + * The node to expand, or null to start from the top. + */ + expandAll: function (node) { + node = node || this._rootNode; + return this._expandAll(this.getContainer(node)); + }, + + /** + * Collapse the node's children. + */ + collapseNode: function (node) { + let container = this.getContainer(node); + container.setExpanded(false); + }, + + /** + * Returns either the innerHTML or the outerHTML for a remote node. + * + * @param {NodeFront} node + * The NodeFront to get the outerHTML / innerHTML for. + * @param {Boolean} isOuter + * If true, makes the function return the outerHTML, + * otherwise the innerHTML. + * @return {Promise} that will be resolved with the outerHTML / innerHTML. + */ + _getNodeHTML: function (node, isOuter) { + let walkerPromise = null; + + if (isOuter) { + walkerPromise = this.walker.outerHTML(node); + } else { + walkerPromise = this.walker.innerHTML(node); + } + + return walkerPromise.then(longstr => { + return longstr.string().then(html => { + longstr.release().then(null, console.error); + return html; + }); + }); + }, + + /** + * Retrieve the outerHTML for a remote node. + * + * @param {NodeFront} node + * The NodeFront to get the outerHTML for. + * @return {Promise} that will be resolved with the outerHTML. + */ + getNodeOuterHTML: function (node) { + return this._getNodeHTML(node, true); + }, + + /** + * Retrieve the innerHTML for a remote node. + * + * @param {NodeFront} node + * The NodeFront to get the innerHTML for. + * @return {Promise} that will be resolved with the innerHTML. + */ + getNodeInnerHTML: function (node) { + return this._getNodeHTML(node); + }, + + /** + * Listen to mutations, expect a given node to be removed and try and select + * the node that sits at the same place instead. + * This is useful when changing the outerHTML or the tag name so that the + * newly inserted node gets selected instead of the one that just got removed. + */ + reselectOnRemoved: function (removedNode, reason) { + // Only allow one removed node reselection at a time, so that when there are + // more than 1 request in parallel, the last one wins. + this.cancelReselectOnRemoved(); + + // Get the removedNode index in its parent node to reselect the right node. + let isHTMLTag = removedNode.tagName.toLowerCase() === "html"; + let oldContainer = this.getContainer(removedNode); + let parentContainer = this.getContainer(removedNode.parentNode()); + let childIndex = parentContainer.getChildContainers().indexOf(oldContainer); + + let onMutations = this._removedNodeObserver = (e, mutations) => { + let isNodeRemovalMutation = false; + for (let mutation of mutations) { + let containsRemovedNode = mutation.removed && + mutation.removed.some(n => n === removedNode); + if (mutation.type === "childList" && + (containsRemovedNode || isHTMLTag)) { + isNodeRemovalMutation = true; + break; + } + } + if (!isNodeRemovalMutation) { + return; + } + + this.inspector.off("markupmutation", onMutations); + this._removedNodeObserver = null; + + // Don't select the new node if the user has already changed the current + // selection. + if (this.inspector.selection.nodeFront === parentContainer.node || + (this.inspector.selection.nodeFront === removedNode && isHTMLTag)) { + let childContainers = parentContainer.getChildContainers(); + if (childContainers && childContainers[childIndex]) { + this.markNodeAsSelected(childContainers[childIndex].node, reason); + if (childContainers[childIndex].hasChildren) { + this.expandNode(childContainers[childIndex].node); + } + this.emit("reselectedonremoved"); + } + } + }; + + // Start listening for mutations until we find a childList change that has + // removedNode removed. + this.inspector.on("markupmutation", onMutations); + }, + + /** + * Make sure to stop listening for node removal markupmutations and not + * reselect the corresponding node when that happens. + * Useful when the outerHTML/tagname edition failed. + */ + cancelReselectOnRemoved: function () { + if (this._removedNodeObserver) { + this.inspector.off("markupmutation", this._removedNodeObserver); + this._removedNodeObserver = null; + this.emit("canceledreselectonremoved"); + } + }, + + /** + * Replace the outerHTML of any node displayed in the inspector with + * some other HTML code + * + * @param {NodeFront} node + * Node which outerHTML will be replaced. + * @param {String} newValue + * The new outerHTML to set on the node. + * @param {String} oldValue + * The old outerHTML that will be used if the user undoes the update. + * @return {Promise} that will resolve when the outer HTML has been updated. + */ + updateNodeOuterHTML: function (node, newValue) { + let container = this.getContainer(node); + if (!container) { + return promise.reject(); + } + + // Changing the outerHTML removes the node which outerHTML was changed. + // Listen to this removal to reselect the right node afterwards. + this.reselectOnRemoved(node, "outerhtml"); + return this.walker.setOuterHTML(node, newValue).then(null, () => { + this.cancelReselectOnRemoved(); + }); + }, + + /** + * Replace the innerHTML of any node displayed in the inspector with + * some other HTML code + * @param {Node} node + * node which innerHTML will be replaced. + * @param {String} newValue + * The new innerHTML to set on the node. + * @param {String} oldValue + * The old innerHTML that will be used if the user undoes the update. + * @return {Promise} that will resolve when the inner HTML has been updated. + */ + updateNodeInnerHTML: function (node, newValue, oldValue) { + let container = this.getContainer(node); + if (!container) { + return promise.reject(); + } + + let def = defer(); + + container.undo.do(() => { + this.walker.setInnerHTML(node, newValue).then(def.resolve, def.reject); + }, () => { + this.walker.setInnerHTML(node, oldValue); + }); + + return def.promise; + }, + + /** + * Insert adjacent HTML to any node displayed in the inspector. + * + * @param {NodeFront} node + * The reference node. + * @param {String} position + * The position as specified for Element.insertAdjacentHTML + * (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd"). + * @param {String} newValue + * The adjacent HTML. + * @return {Promise} that will resolve when the adjacent HTML has + * been inserted. + */ + insertAdjacentHTMLToNode: function (node, position, value) { + let container = this.getContainer(node); + if (!container) { + return promise.reject(); + } + + let def = defer(); + + let injectedNodes = []; + container.undo.do(() => { + this.walker.insertAdjacentHTML(node, position, value).then(nodeArray => { + injectedNodes = nodeArray.nodes; + return nodeArray; + }).then(def.resolve, def.reject); + }, () => { + this.walker.removeNodes(injectedNodes); + }); + + return def.promise; + }, + + /** + * Open an editor in the UI to allow editing of a node's outerHTML. + * + * @param {NodeFront} node + * The NodeFront to edit. + */ + beginEditingOuterHTML: function (node) { + this.getNodeOuterHTML(node).then(oldValue => { + let container = this.getContainer(node); + if (!container) { + return; + } + this.htmlEditor.show(container.tagLine, oldValue); + this.htmlEditor.once("popuphidden", (e, commit, value) => { + // Need to focus the <html> element instead of the frame / window + // in order to give keyboard focus back to doc (from editor). + this.doc.documentElement.focus(); + + if (commit) { + this.updateNodeOuterHTML(node, value, oldValue); + } + }); + }); + }, + + /** + * Mark the given node expanded. + * + * @param {NodeFront} node + * The NodeFront to mark as expanded. + * @param {Boolean} expanded + * Whether the expand or collapse. + * @param {Boolean} expandDescendants + * Whether to expand all descendants too + */ + setNodeExpanded: function (node, expanded, expandDescendants) { + if (expanded) { + if (expandDescendants) { + this.expandAll(node); + } else { + this.expandNode(node); + } + } else { + this.collapseNode(node); + } + }, + + /** + * Mark the given node selected, and update the inspector.selection + * object's NodeFront to keep consistent state between UI and selection. + * + * @param {NodeFront} aNode + * The NodeFront to mark as selected. + * @param {String} reason + * The reason for marking the node as selected. + * @return {Boolean} False if the node is already marked as selected, true + * otherwise. + */ + markNodeAsSelected: function (node, reason) { + let container = this.getContainer(node); + + if (this._selectedContainer === container) { + return false; + } + + // Un-select and remove focus from the previous container. + if (this._selectedContainer) { + this._selectedContainer.selected = false; + this._selectedContainer.clearFocus(); + } + + // Select the new container. + this._selectedContainer = container; + if (node) { + this._selectedContainer.selected = true; + } + + // Change the current selection if needed. + if (this.inspector.selection.nodeFront !== node) { + this.inspector.selection.setNodeFront(node, reason || "nodeselected"); + } + + return true; + }, + + /** + * Make sure that every ancestor of the selection are updated + * and included in the list of visible children. + */ + _ensureVisible: function (node) { + while (node) { + let container = this.getContainer(node); + let parent = node.parentNode(); + if (!container.elt.parentNode) { + let parentContainer = this.getContainer(parent); + if (parentContainer) { + parentContainer.childrenDirty = true; + this._updateChildren(parentContainer, {expand: true}); + } + } + + node = parent; + } + return this._waitForChildren(); + }, + + /** + * Unmark selected node (no node selected). + */ + unmarkSelectedNode: function () { + if (this._selectedContainer) { + this._selectedContainer.selected = false; + this._selectedContainer = null; + } + }, + + /** + * Check if the current selection is a descendent of the container. + * if so, make sure it's among the visible set for the container, + * and set the dirty flag if needed. + * + * @return The node that should be made visible, if any. + */ + _checkSelectionVisible: function (container) { + let centered = null; + let node = this.inspector.selection.nodeFront; + while (node) { + if (node.parentNode() === container.node) { + centered = node; + break; + } + node = node.parentNode(); + } + + return centered; + }, + + /** + * Make sure all children of the given container's node are + * imported and attached to the container in the right order. + * + * Children need to be updated only in the following circumstances: + * a) We just imported this node and have never seen its children. + * container.childrenDirty will be set by importNode in this case. + * b) We received a childList mutation on the node. + * container.childrenDirty will be set in that case too. + * c) We have changed the selection, and the path to that selection + * wasn't loaded in a previous children request (because we only + * grab a subset). + * container.childrenDirty should be set in that case too! + * + * @param {MarkupContainer} container + * The markup container whose children need updating + * @param {Object} options + * Options are {expand:boolean,flash:boolean} + * @return {Promise} that will be resolved when the children are ready + * (which may be immediately). + */ + _updateChildren: function (container, options) { + let expand = options && options.expand; + let flash = options && options.flash; + + container.hasChildren = container.node.hasChildren; + // Accessibility should either ignore empty children or semantically + // consider them a group. + container.setChildrenRole(); + + if (!this._queuedChildUpdates) { + this._queuedChildUpdates = new Map(); + } + + if (this._queuedChildUpdates.has(container)) { + return this._queuedChildUpdates.get(container); + } + + if (!container.childrenDirty) { + return promise.resolve(container); + } + + if (container.inlineTextChild + && container.inlineTextChild != container.node.inlineTextChild) { + // This container was doing double duty as a container for a single + // text child, back that out. + this._containers.delete(container.inlineTextChild); + container.clearInlineTextChild(); + + if (container.hasChildren && container.selected) { + container.setExpanded(true); + } + } + + if (container.node.inlineTextChild) { + container.setExpanded(false); + // this container will do double duty as the container for the single + // text child. + while (container.children.firstChild) { + container.children.removeChild(container.children.firstChild); + } + + container.setInlineTextChild(container.node.inlineTextChild); + + this._containers.set(container.node.inlineTextChild, container); + container.childrenDirty = false; + return promise.resolve(container); + } + + if (!container.hasChildren) { + while (container.children.firstChild) { + container.children.removeChild(container.children.firstChild); + } + container.childrenDirty = false; + container.setExpanded(false); + return promise.resolve(container); + } + + // If we're not expanded (or asked to update anyway), we're done for + // now. Note that this will leave the childrenDirty flag set, so when + // expanded we'll refresh the child list. + if (!(container.expanded || expand)) { + return promise.resolve(container); + } + + // We're going to issue a children request, make sure it includes the + // centered node. + let centered = this._checkSelectionVisible(container); + + // Children aren't updated yet, but clear the childrenDirty flag anyway. + // If the dirty flag is re-set while we're fetching we'll need to fetch + // again. + container.childrenDirty = false; + let updatePromise = + this._getVisibleChildren(container, centered).then(children => { + if (!this._containers) { + return promise.reject("markup view destroyed"); + } + this._queuedChildUpdates.delete(container); + + // If children are dirty, we got a change notification for this node + // while the request was in progress, we need to do it again. + if (container.childrenDirty) { + return this._updateChildren(container, {expand: centered}); + } + + let fragment = this.doc.createDocumentFragment(); + + for (let child of children.nodes) { + let childContainer = this.importNode(child, flash); + fragment.appendChild(childContainer.elt); + } + + while (container.children.firstChild) { + container.children.removeChild(container.children.firstChild); + } + + if (!(children.hasFirst && children.hasLast)) { + let nodesCount = container.node.numChildren; + let showAllString = PluralForm.get(nodesCount, + INSPECTOR_L10N.getStr("markupView.more.showAll2")); + let data = { + showing: INSPECTOR_L10N.getStr("markupView.more.showing"), + showAll: showAllString.replace("#1", nodesCount), + allButtonClick: () => { + container.maxChildren = -1; + container.childrenDirty = true; + this._updateChildren(container); + } + }; + + if (!children.hasFirst) { + let span = this.template("more-nodes", data); + fragment.insertBefore(span, fragment.firstChild); + } + if (!children.hasLast) { + let span = this.template("more-nodes", data); + fragment.appendChild(span); + } + } + + container.children.appendChild(fragment); + return container; + }).catch(this._handleRejectionIfNotDestroyed); + this._queuedChildUpdates.set(container, updatePromise); + return updatePromise; + }, + + _waitForChildren: function () { + if (!this._queuedChildUpdates) { + return promise.resolve(undefined); + } + + return promise.all([...this._queuedChildUpdates.values()]); + }, + + /** + * Return a list of the children to display for this container. + */ + _getVisibleChildren: function (container, centered) { + let maxChildren = container.maxChildren || this.maxChildren; + if (maxChildren == -1) { + maxChildren = undefined; + } + + return this.walker.children(container.node, { + maxNodes: maxChildren, + center: centered + }); + }, + + /** + * Tear down the markup panel. + */ + destroy: function () { + if (this._destroyer) { + return this._destroyer; + } + + this._destroyer = promise.resolve(); + + this._clearBriefBoxModelTimer(); + + this._hoveredNode = null; + + this.htmlEditor.destroy(); + this.htmlEditor = null; + + this.undo.destroy(); + this.undo = null; + + this.popup.destroy(); + this.popup = null; + + this._elt.removeEventListener("click", this._onMouseClick, false); + this._elt.removeEventListener("mousemove", this._onMouseMove, false); + this._elt.removeEventListener("mouseout", this._onMouseOut, false); + this._elt.removeEventListener("blur", this._onBlur, true); + this.win.removeEventListener("mouseup", this._onMouseUp); + this.win.removeEventListener("copy", this._onCopy); + this._frame.removeEventListener("focus", this._onFocus, false); + this.walker.off("mutations", this._mutationObserver); + this.walker.off("display-change", this._onDisplayChange); + this.inspector.selection.off("new-node-front", this._onNewSelection); + this.toolbox.off("picker-node-hovered", + this._onToolboxPickerHover); + + this._prefObserver.off(ATTR_COLLAPSE_ENABLED_PREF, + this._onCollapseAttributesPrefChange); + this._prefObserver.off(ATTR_COLLAPSE_LENGTH_PREF, + this._onCollapseAttributesPrefChange); + this._prefObserver.destroy(); + + this._elt = null; + + for (let [, container] of this._containers) { + container.destroy(); + } + this._containers = null; + + this.eventDetailsTooltip.destroy(); + this.eventDetailsTooltip = null; + + this.imagePreviewTooltip.destroy(); + this.imagePreviewTooltip = null; + + this.win = null; + this.doc = null; + + this._lastDropTarget = null; + this._lastDragTarget = null; + + return this._destroyer; + }, + + /** + * Find the closest element with class tag-line. These are used to indicate + * drag and drop targets. + * + * @param {DOMNode} el + * @return {DOMNode} + */ + findClosestDragDropTarget: function (el) { + return el.classList.contains("tag-line") + ? el + : el.querySelector(".tag-line") || el.closest(".tag-line"); + }, + + /** + * Takes an element as it's only argument and marks the element + * as the drop target + */ + indicateDropTarget: function (el) { + if (this._lastDropTarget) { + this._lastDropTarget.classList.remove("drop-target"); + } + + if (!el) { + return; + } + + let target = this.findClosestDragDropTarget(el); + if (target) { + target.classList.add("drop-target"); + this._lastDropTarget = target; + } + }, + + /** + * Takes an element to mark it as indicator of dragging target's initial place + */ + indicateDragTarget: function (el) { + if (this._lastDragTarget) { + this._lastDragTarget.classList.remove("drag-target"); + } + + if (!el) { + return; + } + + let target = this.findClosestDragDropTarget(el); + if (target) { + target.classList.add("drag-target"); + this._lastDragTarget = target; + } + }, + + /** + * Used to get the nodes required to modify the markup after dragging the + * element (parent/nextSibling). + */ + get dropTargetNodes() { + let target = this._lastDropTarget; + + if (!target) { + return null; + } + + let parent, nextSibling; + + if (target.previousElementSibling && + target.previousElementSibling.nodeName.toLowerCase() === "ul") { + parent = target.parentNode.container.node; + nextSibling = null; + } else { + parent = target.parentNode.container.node.parentNode(); + nextSibling = target.parentNode.container.node; + } + + if (nextSibling && nextSibling.isBeforePseudoElement) { + nextSibling = target.parentNode.parentNode.children[1].container.node; + } + if (nextSibling && nextSibling.isAfterPseudoElement) { + parent = target.parentNode.container.node.parentNode(); + nextSibling = null; + } + + if (parent.nodeType !== nodeConstants.ELEMENT_NODE) { + return null; + } + + return {parent, nextSibling}; + } +}; + +/** + * Map a number from one range to another. + */ +function map(value, oldMin, oldMax, newMin, newMax) { + let ratio = oldMax - oldMin; + if (ratio == 0) { + return value; + } + return newMin + (newMax - newMin) * ((value - oldMin) / ratio); +} + +module.exports = MarkupView; diff --git a/devtools/client/inspector/markup/markup.xhtml b/devtools/client/inspector/markup/markup.xhtml new file mode 100644 index 000000000..88b06aadd --- /dev/null +++ b/devtools/client/inspector/markup/markup.xhtml @@ -0,0 +1,105 @@ +<?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/. --> +<!DOCTYPE html> + +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <link rel="stylesheet" href="chrome://devtools/skin/markup.css" type="text/css"/> + <link rel="stylesheet" href="chrome://devtools/content/sourceeditor/codemirror/lib/codemirror.css" type="text/css"/> + <link rel="stylesheet" href="chrome://devtools/content/sourceeditor/codemirror/addon/dialog/dialog.css" type="text/css"/> + <link rel="stylesheet" href="chrome://devtools/content/sourceeditor/codemirror/mozilla.css" type="text/css"/> + + <script type="application/javascript;version=1.8" + src="chrome://devtools/content/shared/theme-switching.js"></script> + <script type="application/javascript;version=1.8" + src="chrome://devtools/content/sourceeditor/codemirror/codemirror.bundle.js"></script> + +</head> +<body class="theme-body devtools-monospace" role="application"> + +<!-- NOTE THAT WE MAKE EXTENSIVE USE OF HTML COMMENTS IN THIS FILE IN ORDER --> +<!-- TO MAKE SPANS READABLE WHILST AVOIDING SIGNIFICANT WHITESPACE --> + + <div id="root-wrapper" role="presentation"> + <div id="root" role="presentation"></div> + </div> + <div id="templates" style="display:none"> + + <ul class="children"> + <li id="template-elementcontainer" save="${elt}" class="child collapsed" role="presentation"> + <div save="${tagLine}" id="${id}" class="tag-line" role="treeitem" aria-level="${level}" aria-grabbed="${isDragging}"><!-- + --><span save="${tagState}" class="tag-state" role="presentation"></span><!-- + --><span save="${expander}" class="theme-twisty expander" role="presentation"></span><!-- + --></div> + <ul save="${children}" class="children" role="group"></ul> + </li> + + <li id="template-textcontainer" save="${elt}" class="child collapsed" role="presentation"> + <div save="${tagLine}" id="${id}" class="tag-line" role="treeitem" aria-level="${level}" aria-grabbed="${isDragging}"><span save="${tagState}" class="tag-state" role="presentation"></span></div> + <ul save="${children}" class="children" role="group"></ul> + </li> + + <li id="template-readonlycontainer" save="${elt}" class="child collapsed" role="presentation"> + <div save="${tagLine}" id="${id}" class="tag-line" role="treeitem" aria-level="${level}" aria-grabbed="${isDragging}"><!-- + --><span save="${tagState}" class="tag-state" role="presentation"></span><!-- + --><span save="${expander}" class="theme-twisty expander" role="presentation"></span><!-- + --></div> + <ul save="${children}" class="children" role="group"></ul> + </li> + + <li id="template-more-nodes" + class="more-nodes devtools-class-comment" + save="${elt}"><!-- + --><span>${showing}</span> <!-- + --><button href="#" onclick="${allButtonClick}">${showAll}</button> + </li> + </ul> + + <span id="template-generic" save="${elt}" class="editor"><span save="${tag}" class="tag"></span></span> + + <span id="template-element" save="${elt}" class="editor"><!-- + --><span class="open"><<!-- + --><span save="${tag}" class="tag theme-fg-color3" tabindex="-1"></span><!-- + --><span save="${attrList}"></span><!-- + --><span save="${newAttr}" class="newattr" tabindex="-1"></span><!-- + --><span class="closing-bracket">></span><!-- + --></span><!-- + --><span class="close"></<!-- + --><span save="${closeTag}" class="tag theme-fg-color3"></span><!-- + -->><!-- + --></span><!-- + --><div save="${eventNode}" class="markupview-events" data-event="true">ev</div><!-- + --></span> + + <span id="template-attribute" + save="${attr}" + data-attr="${attrName}" + data-value="${attrValue}" + class="attreditor" + style="display:none"> <!-- + --><span class="editable" save="${inner}" tabindex="${tabindex}"><!-- + --><span save="${name}" class="attr-name theme-fg-color2"></span><!-- + -->="<!-- + --><span save="${val}" class="attr-value theme-fg-color6"></span><!-- + -->"<!-- + --></span><!-- + --></span> + + <span id="template-text" save="${elt}" class="editor text"><!-- + --><pre save="${value}" style="display:inline-block; white-space: normal;" tabindex="-1"></pre><!-- + --></span> + + <span id="template-comment" + save="${elt}" + class="editor comment theme-comment"><!-- + --><span><!--</span><!-- + --><pre save="${value}" style="display:inline-block; white-space: normal;" tabindex="-1"></pre><!-- + --><span>--></span><!-- + --></span> + + </div> +</body> +</html> diff --git a/devtools/client/inspector/markup/moz.build b/devtools/client/inspector/markup/moz.build new file mode 100644 index 000000000..4d721cc3c --- /dev/null +++ b/devtools/client/inspector/markup/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DIRS += [ + 'views', +] + +DevToolsModules( + 'markup.js', + 'utils.js', +) + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/devtools/client/inspector/markup/test/.eslintrc.js b/devtools/client/inspector/markup/test/.eslintrc.js new file mode 100644 index 000000000..698ae9181 --- /dev/null +++ b/devtools/client/inspector/markup/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../../.eslintrc.mochitests.js" +}; diff --git a/devtools/client/inspector/markup/test/actor_events_form.js b/devtools/client/inspector/markup/test/actor_events_form.js new file mode 100644 index 000000000..bd1b1e91a --- /dev/null +++ b/devtools/client/inspector/markup/test/actor_events_form.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This test actor is used for testing the addition of custom form data +// on NodeActor. Custom form property is set when 'form' event is sent +// by NodeActor actor (see 'onNodeActorForm' method). + +const Events = require("sdk/event/core"); +const {ActorClassWithSpec, Actor, FrontClassWithSpec, Front, generateActorSpec} = + require("devtools/shared/protocol"); + +const {NodeActor} = require("devtools/server/actors/inspector"); + +var eventsSpec = generateActorSpec({ + typeName: "eventsFormActor", + + methods: { + attach: { + request: {}, + response: {} + }, + detach: { + request: {}, + response: {} + } + } +}); + +var EventsFormActor = ActorClassWithSpec(eventsSpec, { + initialize: function () { + Actor.prototype.initialize.apply(this, arguments); + }, + + attach: function () { + Events.on(NodeActor, "form", this.onNodeActorForm); + }, + + detach: function () { + Events.off(NodeActor, "form", this.onNodeActorForm); + }, + + onNodeActorForm: function (event) { + let nodeActor = event.target; + if (nodeActor.rawNode.id == "container") { + let form = event.data; + form.setFormProperty("test-property", "test-value"); + } + } +}); + +var EventsFormFront = FrontClassWithSpec(eventsSpec, { + initialize: function (client, form) { + Front.prototype.initialize.apply(this, arguments); + + this.actorID = form[EventsFormActor.prototype.typeName]; + this.manage(this); + } +}); + +exports.EventsFormFront = EventsFormFront; diff --git a/devtools/client/inspector/markup/test/browser.ini b/devtools/client/inspector/markup/test/browser.ini new file mode 100644 index 000000000..3116e4beb --- /dev/null +++ b/devtools/client/inspector/markup/test/browser.ini @@ -0,0 +1,155 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + actor_events_form.js + doc_markup_anonymous.html + doc_markup_dragdrop.html + doc_markup_dragdrop_autoscroll_01.html + doc_markup_dragdrop_autoscroll_02.html + doc_markup_edit.html + doc_markup_events1.html + doc_markup_events2.html + doc_markup_events3.html + doc_markup_events_form.html + doc_markup_events_jquery.html + doc_markup_events-overflow.html + doc_markup_flashing.html + doc_markup_html_mixed_case.html + doc_markup_image_and_canvas.html + doc_markup_image_and_canvas_2.html + doc_markup_links.html + doc_markup_mutation.html + doc_markup_navigation.html + doc_markup_not_displayed.html + doc_markup_pagesize_01.html + doc_markup_pagesize_02.html + doc_markup_search.html + doc_markup_svg_attributes.html + doc_markup_toggle.html + doc_markup_tooltip.png + doc_markup_void_elements.html + doc_markup_void_elements.xhtml + doc_markup_whitespace.html + doc_markup_xul.xul + head.js + helper_attributes_test_runner.js + helper_events_test_runner.js + helper_markup_accessibility_navigation.js + helper_outerhtml_test_runner.js + helper_style_attr_test_runner.js + lib_jquery_1.0.js + lib_jquery_1.1.js + lib_jquery_1.2_min.js + lib_jquery_1.3_min.js + lib_jquery_1.4_min.js + lib_jquery_1.6_min.js + lib_jquery_1.7_min.js + lib_jquery_1.11.1_min.js + lib_jquery_2.1.1_min.js + !/devtools/client/commandline/test/helpers.js + !/devtools/client/framework/test/shared-head.js + !/devtools/client/inspector/test/head.js + !/devtools/client/inspector/test/shared-head.js + !/devtools/client/shared/test/test-actor.js + !/devtools/client/shared/test/test-actor-registry.js + +[browser_markup_accessibility_focus_blur.js] +skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences +[browser_markup_accessibility_navigation.js] +skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences +[browser_markup_accessibility_navigation_after_edit.js] +skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences +[browser_markup_accessibility_semantics.js] +[browser_markup_anonymous_01.js] +[browser_markup_anonymous_02.js] +skip-if = e10s # scratchpad.xul is not loading in e10s window +[browser_markup_anonymous_03.js] +[browser_markup_anonymous_04.js] +[browser_markup_copy_image_data.js] +subsuite = clipboard +[browser_markup_css_completion_style_attribute_01.js] +[browser_markup_css_completion_style_attribute_02.js] +[browser_markup_css_completion_style_attribute_03.js] +[browser_markup_dragdrop_autoscroll_01.js] +[browser_markup_dragdrop_autoscroll_02.js] +[browser_markup_dragdrop_distance.js] +[browser_markup_dragdrop_draggable.js] +[browser_markup_dragdrop_dragRootNode.js] +[browser_markup_dragdrop_escapeKeyPress.js] +[browser_markup_dragdrop_invalidNodes.js] +[browser_markup_dragdrop_reorder.js] +[browser_markup_dragdrop_tooltip.js] +[browser_markup_events1.js] +[browser_markup_events2.js] +[browser_markup_events3.js] +[browser_markup_events_form.js] +[browser_markup_events_jquery_1.0.js] +[browser_markup_events_jquery_1.1.js] +[browser_markup_events_jquery_1.2.js] +[browser_markup_events_jquery_1.3.js] +[browser_markup_events_jquery_1.4.js] +[browser_markup_events_jquery_1.6.js] +[browser_markup_events_jquery_1.7.js] +[browser_markup_events_jquery_1.11.1.js] +[browser_markup_events_jquery_2.1.1.js] +[browser_markup_events-overflow.js] +skip-if = true # Bug 1177550 +[browser_markup_events-windowed-host.js] +[browser_markup_links_01.js] +[browser_markup_links_02.js] +[browser_markup_links_03.js] +[browser_markup_links_04.js] +subsuite = clipboard +[browser_markup_links_05.js] +[browser_markup_links_06.js] +[browser_markup_links_07.js] +[browser_markup_load_01.js] +[browser_markup_html_edit_01.js] +[browser_markup_html_edit_02.js] +[browser_markup_html_edit_03.js] +[browser_markup_image_tooltip.js] +[browser_markup_image_tooltip_mutations.js] +[browser_markup_keybindings_01.js] +[browser_markup_keybindings_02.js] +[browser_markup_keybindings_03.js] +[browser_markup_keybindings_04.js] +[browser_markup_keybindings_delete_attributes.js] +[browser_markup_keybindings_scrolltonode.js] +[browser_markup_mutation_01.js] +[browser_markup_mutation_02.js] +[browser_markup_navigation.js] +[browser_markup_node_names.js] +[browser_markup_node_names_namespaced.js] +[browser_markup_node_not_displayed_01.js] +[browser_markup_node_not_displayed_02.js] +[browser_markup_pagesize_01.js] +[browser_markup_pagesize_02.js] +[browser_markup_remove_xul_attributes.js] +skip-if = e10s # Bug 1036409 - The last selected node isn't reselected +[browser_markup_search_01.js] +[browser_markup_tag_edit_01.js] +[browser_markup_tag_edit_02.js] +[browser_markup_tag_edit_03.js] +[browser_markup_tag_edit_04-backspace.js] +[browser_markup_tag_edit_04-delete.js] +[browser_markup_tag_edit_05.js] +[browser_markup_tag_edit_06.js] +[browser_markup_tag_edit_07.js] +[browser_markup_tag_edit_08.js] +[browser_markup_tag_edit_09.js] +[browser_markup_tag_edit_10.js] +[browser_markup_tag_edit_11.js] +[browser_markup_tag_edit_12.js] +[browser_markup_tag_edit_13-other.js] +[browser_markup_tag_edit_long-classname.js] +[browser_markup_textcontent_display.js] +[browser_markup_textcontent_edit_01.js] +[browser_markup_textcontent_edit_02.js] +[browser_markup_toggle_01.js] +[browser_markup_toggle_02.js] +[browser_markup_toggle_03.js] +[browser_markup_update-on-navigtion.js] +[browser_markup_void_elements_html.js] +[browser_markup_void_elements_xhtml.js] +[browser_markup_whitespace.js] diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_focus_blur.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_focus_blur.js new file mode 100644 index 000000000..7e94669c0 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_focus_blur.js @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test inspector markup view handling focus and blur when moving between markup +// view, its root and other containers, and other parts of inspector. + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL( + "data:text/html;charset=utf-8,<h1>foo</h1><span>bar</span>"); + let markup = inspector.markup; + let doc = markup.doc; + let win = doc.defaultView; + + let spanContainer = yield getContainerForSelector("span", inspector); + let rootContainer = markup.getContainer(markup._rootNode); + + is(doc.activeElement, doc.body, + "Keyboard focus by default is on document body"); + + yield selectNode("span", inspector); + + is(doc.activeElement, doc.body, + "Keyboard focus is still on document body"); + + info("Focusing on the test span node using 'Return' key"); + // Focus on the tree element. + rootContainer.elt.focus(); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + + is(doc.activeElement, spanContainer.editor.tag, + "Keyboard focus should be on tag element of focused container"); + + info("Focusing on search box, external to markup view document"); + yield focusSearchBoxUsingShortcut(inspector.panelWin); + + is(doc.activeElement, doc.body, + "Keyboard focus should be removed from focused container"); + + info("Selecting the test span node again"); + yield selectNode("span", inspector); + + is(doc.activeElement, doc.body, + "Keyboard focus should again be on document body"); + + info("Focusing on the test span node using 'Space' key"); + // Focus on the tree element. + rootContainer.elt.focus(); + EventUtils.synthesizeKey("VK_SPACE", {}, win); + + is(doc.activeElement, spanContainer.editor.tag, + "Keyboard focus should again be on tag element of focused container"); + + yield clickOnInspectMenuItem(testActor, "h1"); + is(doc.activeElement, rootContainer.elt, + "When inspect menu item is used keyboard focus should move to tree."); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js new file mode 100644 index 000000000..41e35afef --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js @@ -0,0 +1,277 @@ +/* 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/. */ +/* import-globals-from helper_markup_accessibility_navigation.js */ + +"use strict"; + +// Test keyboard navigation accessibility of inspector's markup view. + +loadHelperScript("helper_markup_accessibility_navigation.js"); + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * key {String} key event's key + * options {?Object} optional event data such as shiftKey, etc + * focused {String} path to expected focused element relative to + * its container + * activedescendant {String} path to expected aria-activedescendant element + * relative to its container + * waitFor {String} optional event to wait for if keyboard actions + * result in asynchronous updates + * } + */ +const TESTS = [ + { + desc: "Collapse body container", + focused: "root.elt", + activedescendant: "body.tagLine", + key: "VK_LEFT", + options: { }, + waitFor: "collapsed" + }, + { + desc: "Expand body container", + focused: "root.elt", + activedescendant: "body.tagLine", + key: "VK_RIGHT", + options: { }, + waitFor: "expanded" + }, + { + desc: "Select header container", + focused: "root.elt", + activedescendant: "header.tagLine", + key: "VK_DOWN", + options: { }, + waitFor: "inspector-updated" + }, + { + desc: "Expand header container", + focused: "root.elt", + activedescendant: "header.tagLine", + key: "VK_RIGHT", + options: { }, + waitFor: "expanded" + }, + { + desc: "Select text container", + focused: "root.elt", + activedescendant: "container-0.tagLine", + key: "VK_DOWN", + options: { }, + waitFor: "inspector-updated" + }, + { + desc: "Select header container again", + focused: "root.elt", + activedescendant: "header.tagLine", + key: "VK_UP", + options: { }, + waitFor: "inspector-updated" + }, + { + desc: "Collapse header container", + focused: "root.elt", + activedescendant: "header.tagLine", + key: "VK_LEFT", + options: { }, + waitFor: "collapsed" + }, + { + desc: "Focus on header container tag", + focused: "header.focusableElms.0", + activedescendant: "header.tagLine", + key: "VK_RETURN", + options: { } + }, + { + desc: "Remove focus from header container tag", + focused: "root.elt", + activedescendant: "header.tagLine", + key: "VK_ESCAPE", + options: { } + }, + { + desc: "Focus on header container tag again", + focused: "header.focusableElms.0", + activedescendant: "header.tagLine", + key: "VK_SPACE", + options: { } + }, + { + desc: "Focus on header id attribute", + focused: "header.focusableElms.1", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Focus on header class attribute", + focused: "header.focusableElms.2", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Focus on header new attribute", + focused: "header.focusableElms.3", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Circle back and focus on header tag again", + focused: "header.focusableElms.0", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Circle back and focus on header new attribute again", + focused: "header.focusableElms.3", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { shiftKey: true } + }, + { + desc: "Tab back and focus on header class attribute", + focused: "header.focusableElms.2", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { shiftKey: true } + }, + { + desc: "Tab back and focus on header id attribute", + focused: "header.focusableElms.1", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { shiftKey: true } + }, + { + desc: "Tab back and focus on header tag", + focused: "header.focusableElms.0", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { shiftKey: true } + }, + { + desc: "Expand header container, ensure that focus is still on header tag", + focused: "header.focusableElms.0", + activedescendant: "header.tagLine", + key: "VK_RIGHT", + options: { }, + waitFor: "expanded" + }, + { + desc: "Activate header tag editor", + focused: "header.editor.tag.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_RETURN", + options: { } + }, + { + desc: "Activate header id attribute editor", + focused: "header.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Deselect text in header id attribute editor", + focused: "header.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Activate header class attribute editor", + focused: "header.editor.attrList.children.1.children.1.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Deselect text in header class attribute editor", + focused: "header.editor.attrList.children.1.children.1.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Activate header new attribute editor", + focused: "header.editor.newAttr.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Circle back and activate header tag editor again", + focused: "header.editor.tag.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Circle back and activate header new attribute editor again", + focused: "header.editor.newAttr.inplaceEditor.input", + activedescendant: "header.tagLine", + key: "VK_TAB", + options: { shiftKey: true } + }, + { + desc: "Exit edit mode and keep focus on header new attribute", + focused: "header.focusableElms.3", + activedescendant: "header.tagLine", + key: "VK_ESCAPE", + options: { } + }, + { + desc: "Move the selection to body and reset focus to container tree", + focused: "docBody", + activedescendant: "body.tagLine", + key: "VK_UP", + options: { }, + waitFor: "inspector-updated" + }, +]; + +let containerID = 0; +let elms = {}; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(`data:text/html;charset=utf-8, + <h1 id="some-id" class="some-class">foo<span>Child span<span></h1>`); + + // Record containers that are created after inspector is initialized to be + // useful in testing. + inspector.on("container-created", memorizeContainer); + registerCleanupFunction(() => { + inspector.off("container-created", memorizeContainer); + }); + + elms.docBody = inspector.markup.doc.body; + elms.root = inspector.markup.getContainer(inspector.markup._rootNode); + elms.header = yield getContainerForSelector("h1", inspector); + elms.body = yield getContainerForSelector("body", inspector); + + // Initial focus is on root element and active descendant should be set on + // body tag line. + testNavigationState(inspector, elms, elms.docBody, elms.body.tagLine); + + // Focus on the tree element. + elms.root.elt.focus(); + + for (let testData of TESTS) { + yield runAccessibilityNavigationTest(inspector, elms, testData); + } + + elms = null; +}); + +// Record all containers that are created dynamically into elms object. +function memorizeContainer(event, container) { + elms[`container-${containerID++}`] = container; +} diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js new file mode 100644 index 000000000..ec217db09 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js @@ -0,0 +1,126 @@ +/* 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/. */ +/* import-globals-from helper_markup_accessibility_navigation.js */ + +"use strict"; + +// Test keyboard navigation accessibility is preserved after editing attributes. + +loadHelperScript("helper_markup_accessibility_navigation.js"); + +const TEST_URI = '<div id="some-id" class="some-class"></div>'; + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * key {String} key event's key + * options {?Object} optional event data such as shiftKey, etc + * focused {String} path to expected focused element relative to + * its container + * activedescendant {String} path to expected aria-activedescendant element + * relative to its container + * waitFor {String} optional event to wait for if keyboard actions + * result in asynchronous updates + * } + */ +const TESTS = [ + { + desc: "Select header container", + focused: "root.elt", + activedescendant: "div.tagLine", + key: "VK_DOWN", + options: { }, + waitFor: "inspector-updated" + }, + { + desc: "Focus on header tag", + focused: "div.focusableElms.0", + activedescendant: "div.tagLine", + key: "VK_RETURN", + options: { } + }, + { + desc: "Activate header tag editor", + focused: "div.editor.tag.inplaceEditor.input", + activedescendant: "div.tagLine", + key: "VK_RETURN", + options: { } + }, + { + desc: "Activate header id attribute editor", + focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "div.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Deselect text in header id attribute editor", + focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "div.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Move the cursor to the left", + focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "div.tagLine", + key: "VK_LEFT", + options: { } + }, + { + desc: "Modify the attribute", + focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input", + activedescendant: "div.tagLine", + key: "A", + options: { } + }, + { + desc: "Commit the attribute change", + focused: "div.focusableElms.1", + activedescendant: "div.tagLine", + key: "VK_RETURN", + options: { }, + waitFor: "inspector-updated" + }, + { + desc: "Tab and focus on header class attribute", + focused: "div.focusableElms.2", + activedescendant: "div.tagLine", + key: "VK_TAB", + options: { } + }, + { + desc: "Tab and focus on header new attribute node", + focused: "div.focusableElms.3", + activedescendant: "div.tagLine", + key: "VK_TAB", + options: { } + }, +]; + +let elms = {}; + +add_task(function* () { + let url = `data:text/html;charset=utf-8,${TEST_URI}`; + let { inspector } = yield openInspectorForURL(url); + + elms.docBody = inspector.markup.doc.body; + elms.root = inspector.markup.getContainer(inspector.markup._rootNode); + elms.div = yield getContainerForSelector("div", inspector); + elms.body = yield getContainerForSelector("body", inspector); + + // Initial focus is on root element and active descendant should be set on + // body tag line. + testNavigationState(inspector, elms, elms.docBody, elms.body.tagLine); + + // Focus on the tree element. + elms.root.elt.focus(); + + for (let testData of TESTS) { + yield runAccessibilityNavigationTest(inspector, elms, testData); + } + + elms = null; +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_accessibility_semantics.js b/devtools/client/inspector/markup/test/browser_markup_accessibility_semantics.js new file mode 100644 index 000000000..b38a68c10 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_semantics.js @@ -0,0 +1,100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that inspector markup view has all expected ARIA properties set and +// updated. + +const TOP_CONTAINER_LEVEL = 3; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(` + data:text/html;charset=utf-8, + <h1>foo</h1> + <span>bar</span> + <ul> + <li></li> + </ul>`); + let markup = inspector.markup; + let doc = markup.doc; + let win = doc.defaultView; + + let rootElt = markup.getContainer(markup._rootNode).elt; + let bodyContainer = yield getContainerForSelector("body", inspector); + let spanContainer = yield getContainerForSelector("span", inspector); + let headerContainer = yield getContainerForSelector("h1", inspector); + let listContainer = yield getContainerForSelector("ul", inspector); + + // Focus on the tree element. + rootElt.focus(); + + // Test tree related semantics + is(rootElt.getAttribute("role"), "tree", + "Root container should have tree semantics"); + is(rootElt.getAttribute("aria-dropeffect"), "none", + "By default root container's drop effect should be set to none"); + is(rootElt.getAttribute("aria-activedescendant"), + bodyContainer.tagLine.getAttribute("id"), + "Default active descendant should be set to body"); + is(bodyContainer.tagLine.getAttribute("aria-level"), TOP_CONTAINER_LEVEL - 1, + "Body container tagLine should have nested level up to date"); + [spanContainer, headerContainer, listContainer].forEach(container => { + let treeitem = container.tagLine; + is(treeitem.getAttribute("role"), "treeitem", + "Child container tagLine elements should have tree item semantics"); + is(treeitem.getAttribute("aria-level"), TOP_CONTAINER_LEVEL, + "Child container tagLine should have nested level up to date"); + is(treeitem.getAttribute("aria-grabbed"), "false", + "Child container should be draggable but not grabbed by default"); + is(container.children.getAttribute("role"), "group", + "Container with children should have its children element have group " + + "semantics"); + ok(treeitem.id, "Tree item should have id assigned"); + if (container.closeTagLine) { + is(container.closeTagLine.getAttribute("role"), "presentation", + "Ignore closing tag"); + } + if (container.expander) { + is(container.expander.getAttribute("role"), "presentation", + "Ignore expander"); + } + }); + + // Test expanding/expandable semantics + ok(!spanContainer.tagLine.hasAttribute("aria-expanded"), + "Non expandable tree items should not have aria-expanded attribute"); + ok(!headerContainer.tagLine.hasAttribute("aria-expanded"), + "Non expandable tree items should not have aria-expanded attribute"); + is(listContainer.tagLine.getAttribute("aria-expanded"), "false", + "Closed tree item should have aria-expanded unset"); + + info("Selecting and expanding list container"); + let updated = waitForMultipleChildrenUpdates(inspector); + yield selectNode("ul", inspector); + EventUtils.synthesizeKey("VK_RIGHT", {}, win); + yield updated; + + is(rootElt.getAttribute("aria-activedescendant"), + listContainer.tagLine.getAttribute("id"), + "Active descendant should not be set to list container tagLine"); + is(listContainer.tagLine.getAttribute("aria-expanded"), "true", + "Open tree item should have aria-expanded set"); + let listItemContainer = yield getContainerForSelector("li", inspector); + is(listItemContainer.tagLine.getAttribute("aria-level"), + TOP_CONTAINER_LEVEL + 1, + "Grand child container tagLine should have nested level up to date"); + is(listItemContainer.children.getAttribute("role"), "presentation", + "Container with no children should have its children element ignored by " + + "accessibility"); + + info("Collapsing list container"); + updated = waitForMultipleChildrenUpdates(inspector); + EventUtils.synthesizeKey("VK_LEFT", {}, win); + yield updated; + + is(listContainer.tagLine.getAttribute("aria-expanded"), "false", + "Closed tree item should have aria-expanded unset"); +}); + diff --git a/devtools/client/inspector/markup/test/browser_markup_anonymous_01.js b/devtools/client/inspector/markup/test/browser_markup_anonymous_01.js new file mode 100644 index 000000000..fd32251d0 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_anonymous_01.js @@ -0,0 +1,44 @@ +/* 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 native anonymous content in the markupview. +const TEST_URL = URL_ROOT + "doc_markup_anonymous.html"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + let pseudo = yield getNodeFront("#pseudo", inspector); + + // Markup looks like: <div><::before /><span /><::after /></div> + let children = yield inspector.walker.children(pseudo); + is(children.nodes.length, 3, "Children returned from walker"); + + info("Checking the ::before pseudo element"); + let before = children.nodes[0]; + yield isEditingMenuDisabled(before, inspector); + + info("Checking the normal child element"); + let span = children.nodes[1]; + yield isEditingMenuEnabled(span, inspector); + + info("Checking the ::after pseudo element"); + let after = children.nodes[2]; + yield isEditingMenuDisabled(after, inspector); + + let native = yield getNodeFront("#native", inspector); + + // Markup looks like: <div><video controls /></div> + let nativeChildren = yield inspector.walker.children(native); + is(nativeChildren.nodes.length, 1, "Children returned from walker"); + + info("Checking the video element"); + let video = nativeChildren.nodes[0]; + ok(!video.isAnonymous, "<video> is not anonymous"); + + let videoChildren = yield inspector.walker.children(video); + is(videoChildren.nodes.length, 0, + "No native children returned from walker for <video> by default"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_anonymous_02.js b/devtools/client/inspector/markup/test/browser_markup_anonymous_02.js new file mode 100644 index 000000000..b6221c5c3 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_anonymous_02.js @@ -0,0 +1,31 @@ +/* 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"; + +requestLongerTimeout(2); + +// Test XBL anonymous content in the markupview +const TEST_URL = "chrome://devtools/content/scratchpad/scratchpad.xul"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + let toolbarbutton = yield getNodeFront("toolbarbutton", inspector); + let children = yield inspector.walker.children(toolbarbutton); + + is(toolbarbutton.numChildren, 3, "Correct number of children"); + is(children.nodes.length, 3, "Children returned from walker"); + + is(toolbarbutton.isAnonymous, false, "Toolbarbutton is not anonymous"); + yield isEditingMenuEnabled(toolbarbutton, inspector); + + for (let node of children.nodes) { + ok(node.isAnonymous, "Child is anonymous"); + ok(node._form.isXBLAnonymous, "Child is XBL anonymous"); + ok(!node._form.isShadowAnonymous, "Child is not shadow anonymous"); + ok(!node._form.isNativeAnonymous, "Child is not native anonymous"); + yield isEditingMenuDisabled(node, inspector); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_anonymous_03.js b/devtools/client/inspector/markup/test/browser_markup_anonymous_03.js new file mode 100644 index 000000000..010ce06e0 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_anonymous_03.js @@ -0,0 +1,34 @@ +/* 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 shadow DOM content in the markupview. +// Note that many features are not yet enabled, but basic listing +// of elements should be working. +const TEST_URL = URL_ROOT + "doc_markup_anonymous.html"; + +add_task(function* () { + Services.prefs.setBoolPref("dom.webcomponents.enabled", true); + + let {inspector} = yield openInspectorForURL(TEST_URL); + + let shadow = yield getNodeFront("#shadow", inspector.markup); + let children = yield inspector.walker.children(shadow); + + is(shadow.numChildren, 3, "Children of the shadow root are counted"); + is(children.nodes.length, 3, "Children returned from walker"); + + info("Checking the ::before pseudo element"); + let before = children.nodes[0]; + yield isEditingMenuDisabled(before, inspector); + + info("Checking the <h3> shadow element"); + let shadowChild1 = children.nodes[1]; + yield isEditingMenuDisabled(shadowChild1, inspector); + + info("Checking the <select> shadow element"); + let shadowChild2 = children.nodes[2]; + yield isEditingMenuDisabled(shadowChild2, inspector); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_anonymous_04.js b/devtools/client/inspector/markup/test/browser_markup_anonymous_04.js new file mode 100644 index 000000000..da5e4567d --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_anonymous_04.js @@ -0,0 +1,37 @@ +/* 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 native anonymous content in the markupview with +// devtools.inspector.showAllAnonymousContent set to true +const TEST_URL = URL_ROOT + "doc_markup_anonymous.html"; +const PREF = "devtools.inspector.showAllAnonymousContent"; + +add_task(function* () { + Services.prefs.setBoolPref(PREF, true); + + let {inspector} = yield openInspectorForURL(TEST_URL); + + let native = yield getNodeFront("#native", inspector); + + // Markup looks like: <div><video controls /></div> + let nativeChildren = yield inspector.walker.children(native); + is(nativeChildren.nodes.length, 1, "Children returned from walker"); + + info("Checking the video element"); + let video = nativeChildren.nodes[0]; + ok(!video.isAnonymous, "<video> is not anonymous"); + + let videoChildren = yield inspector.walker.children(video); + is(videoChildren.nodes.length, 3, "<video> has native anonymous children"); + + for (let node of videoChildren.nodes) { + ok(node.isAnonymous, "Child is anonymous"); + ok(!node._form.isXBLAnonymous, "Child is not XBL anonymous"); + ok(!node._form.isShadowAnonymous, "Child is not shadow anonymous"); + ok(node._form.isNativeAnonymous, "Child is native anonymous"); + yield isEditingMenuDisabled(node, inspector); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_copy_image_data.js b/devtools/client/inspector/markup/test/browser_markup_copy_image_data.js new file mode 100644 index 000000000..275bff0b7 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_copy_image_data.js @@ -0,0 +1,67 @@ +/* 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 image nodes have the "copy data-uri" contextual menu item enabled +// and that clicking it puts the image data into the clipboard + +add_task(function* () { + yield addTab(URL_ROOT + "doc_markup_image_and_canvas.html"); + let {inspector, testActor} = yield openInspector(); + + yield selectNode("div", inspector); + yield assertCopyImageDataNotAvailable(inspector); + + yield selectNode("img", inspector); + yield assertCopyImageDataAvailable(inspector); + let expectedSrc = yield testActor.getAttribute("img", "src"); + yield triggerCopyImageUrlAndWaitForClipboard(expectedSrc, inspector); + + yield selectNode("canvas", inspector); + yield assertCopyImageDataAvailable(inspector); + let expectedURL = yield testActor.eval(` + content.document.querySelector(".canvas").toDataURL();`); + yield triggerCopyImageUrlAndWaitForClipboard(expectedURL, inspector); + + // Check again that the menu isn't available on the DIV (to make sure our + // menu updating mechanism works) + yield selectNode("div", inspector); + yield assertCopyImageDataNotAvailable(inspector); +}); + +function* assertCopyImageDataNotAvailable(inspector) { + let allMenuItems = openContextMenuAndGetAllItems(inspector); + let item = allMenuItems.find(i => i.id === "node-menu-copyimagedatauri"); + + ok(item, "The menu item was found in the contextual menu"); + ok(item.disabled, "The menu item is disabled"); +} + +function* assertCopyImageDataAvailable(inspector) { + let allMenuItems = openContextMenuAndGetAllItems(inspector); + let item = allMenuItems.find(i => i.id === "node-menu-copyimagedatauri"); + + ok(item, "The menu item was found in the contextual menu"); + ok(!item.disabled, "The menu item is enabled"); +} + +function triggerCopyImageUrlAndWaitForClipboard(expected, inspector) { + let def = defer(); + + SimpleTest.waitForClipboard(expected, () => { + inspector.markup.getContainer(inspector.selection.nodeFront) + .copyImageDataUri(); + }, () => { + ok(true, "The clipboard contains the expected value " + + expected.substring(0, 50) + "..."); + def.resolve(); + }, () => { + ok(false, "The clipboard doesn't contain the expected value " + + expected.substring(0, 50) + "..."); + def.resolve(); + }); + + return def.promise; +} diff --git a/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js new file mode 100644 index 000000000..f860456d1 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js @@ -0,0 +1,76 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_style_attr_test_runner.js */ + +"use strict"; + +// Test CSS state is correctly determined and the corresponding suggestions are +// displayed. i.e. CSS property suggestions are shown when cursor is like: +// ```style="di|"``` where | is the cursor; And CSS value suggestion is +// displayed when the cursor is like: ```style="display:n|"``` properly. No +// suggestions should ever appear when the attribute is not a style attribute. +// The correctness and cycling of the suggestions is covered in the ruleview +// tests. + +loadHelperScript("helper_style_attr_test_runner.js"); + +const TEST_URL = URL_ROOT + "doc_markup_edit.html"; + +// test data format : +// [ +// what key to press, +// expected input box value after keypress, +// expected input.selectionStart, +// expected input.selectionEnd, +// is popup expected to be open ? +// ] +const TEST_DATA = [ + ["s", "s", 1, 1, false], + ["t", "st", 2, 2, false], + ["y", "sty", 3, 3, false], + ["l", "styl", 4, 4, false], + ["e", "style", 5, 5, false], + ["=", "style=", 6, 6, false], + ["\"", "style=\"", 7, 7, false], + ["d", "style=\"display", 8, 14, true], + ["VK_TAB", "style=\"display", 14, 14, true], + ["VK_TAB", "style=\"dominant-baseline", 24, 24, true], + ["VK_TAB", "style=\"direction", 16, 16, true], + ["click_1", "style=\"display", 14, 14, false], + [":", "style=\"display:block", 15, 20, true], + ["n", "style=\"display:none", 16, 19, false], + ["VK_BACK_SPACE", "style=\"display:n", 16, 16, false], + ["VK_BACK_SPACE", "style=\"display:", 15, 15, false], + [" ", "style=\"display: block", 16, 21, true], + [" ", "style=\"display: block", 17, 22, true], + ["i", "style=\"display: inherit", 18, 24, true], + ["VK_RIGHT", "style=\"display: inherit", 24, 24, false], + [";", "style=\"display: inherit;", 25, 25, false], + [" ", "style=\"display: inherit; ", 26, 26, false], + [" ", "style=\"display: inherit; ", 27, 27, false], + ["VK_LEFT", "style=\"display: inherit; ", 26, 26, false], + ["c", "style=\"display: inherit; color ", 27, 31, true], + ["VK_RIGHT", "style=\"display: inherit; color ", 31, 31, false], + [" ", "style=\"display: inherit; color ", 32, 32, false], + ["c", "style=\"display: inherit; color c ", 33, 33, false], + ["VK_BACK_SPACE", "style=\"display: inherit; color ", 32, 32, false], + [":", "style=\"display: inherit; color :aliceblue ", 33, 42, true], + ["c", "style=\"display: inherit; color :cadetblue ", 34, 42, true], + ["VK_DOWN", "style=\"display: inherit; color :chartreuse ", 34, 43, true], + ["VK_RIGHT", "style=\"display: inherit; color :chartreuse ", 43, 43, false], + [" ", "style=\"display: inherit; color :chartreuse aliceblue ", + 44, 53, true], + ["!", "style=\"display: inherit; color :chartreuse !important; ", + 45, 55, false], + ["VK_RIGHT", "style=\"display: inherit; color :chartreuse !important; ", + 55, 55, false], + ["VK_RETURN", "style=\"display: inherit; color :chartreuse !important;\"", + -1, -1, false] +]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js new file mode 100644 index 000000000..345ee4866 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js @@ -0,0 +1,106 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_style_attr_test_runner.js */ + +"use strict"; + +// Test CSS autocompletion of the style attributes stops after closing the +// attribute using a matching quote. + +loadHelperScript("helper_style_attr_test_runner.js"); + +const TEST_URL = URL_ROOT + "doc_markup_edit.html"; + +// test data format : +// [ +// what key to press, +// expected input box value after keypress, +// expected input.selectionStart, +// expected input.selectionEnd, +// is popup expected to be open ? +// ] +const TEST_DATA_DOUBLE = [ + ["s", "s", 1, 1, false], + ["t", "st", 2, 2, false], + ["y", "sty", 3, 3, false], + ["l", "styl", 4, 4, false], + ["e", "style", 5, 5, false], + ["=", "style=", 6, 6, false], + ["\"", "style=\"", 7, 7, false], + ["c", "style=\"color", 8, 12, true], + ["VK_RIGHT", "style=\"color", 12, 12, false], + [":", "style=\"color:aliceblue", 13, 22, true], + ["b", "style=\"color:beige", 14, 18, true], + ["VK_RIGHT", "style=\"color:beige", 18, 18, false], + ["\"", "style=\"color:beige\"", 19, 19, false], + [" ", "style=\"color:beige\" ", 20, 20, false], + ["d", "style=\"color:beige\" d", 21, 21, false], + ["a", "style=\"color:beige\" da", 22, 22, false], + ["t", "style=\"color:beige\" dat", 23, 23, false], + ["a", "style=\"color:beige\" data", 24, 24, false], + ["VK_RETURN", "style=\"color:beige\"", + -1, -1, false] +]; + +// Check that single quote attribute is also supported +const TEST_DATA_SINGLE = [ + ["s", "s", 1, 1, false], + ["t", "st", 2, 2, false], + ["y", "sty", 3, 3, false], + ["l", "styl", 4, 4, false], + ["e", "style", 5, 5, false], + ["=", "style=", 6, 6, false], + ["'", "style='", 7, 7, false], + ["c", "style='color", 8, 12, true], + ["VK_RIGHT", "style='color", 12, 12, false], + [":", "style='color:aliceblue", 13, 22, true], + ["b", "style='color:beige", 14, 18, true], + ["VK_RIGHT", "style='color:beige", 18, 18, false], + ["'", "style='color:beige'", 19, 19, false], + [" ", "style='color:beige' ", 20, 20, false], + ["d", "style='color:beige' d", 21, 21, false], + ["a", "style='color:beige' da", 22, 22, false], + ["t", "style='color:beige' dat", 23, 23, false], + ["a", "style='color:beige' data", 24, 24, false], + ["VK_RETURN", "style=\"color:beige\"", + -1, -1, false] +]; + +// Check that autocompletion is still enabled after using url('1) +const TEST_DATA_INNER = [ + ["s", "s", 1, 1, false], + ["t", "st", 2, 2, false], + ["y", "sty", 3, 3, false], + ["l", "styl", 4, 4, false], + ["e", "style", 5, 5, false], + ["=", "style=", 6, 6, false], + ["\"", "style=\"", 7, 7, false], + ["b", "style=\"border", 8, 13, true], + ["a", "style=\"background", 9, 17, true], + ["VK_RIGHT", "style=\"background", 17, 17, false], + [":", "style=\"background:aliceblue", 18, 27, true], + ["u", "style=\"background:unset", 19, 23, true], + ["r", "style=\"background:url", 20, 21, false], + ["l", "style=\"background:url", 21, 21, false], + ["(", "style=\"background:url(", 22, 22, false], + ["'", "style=\"background:url('", 23, 23, false], + ["1", "style=\"background:url('1", 24, 24, false], + ["'", "style=\"background:url('1'", 25, 25, false], + [")", "style=\"background:url('1')", 26, 26, false], + [";", "style=\"background:url('1');", 27, 27, false], + [" ", "style=\"background:url('1'); ", 28, 28, false], + ["c", "style=\"background:url('1'); color", 29, 33, true], + ["VK_RIGHT", "style=\"background:url('1'); color", 33, 33, false], + [":", "style=\"background:url('1'); color:aliceblue", 34, 43, true], + ["b", "style=\"background:url('1'); color:beige", 35, 39, true], + ["VK_RETURN", "style=\"background:url('1'); color:beige\"", -1, -1, false] +]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA_DOUBLE); + yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA_SINGLE); + yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA_INNER); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_03.js b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_03.js new file mode 100644 index 000000000..3dbc3e6b2 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_03.js @@ -0,0 +1,54 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_style_attr_test_runner.js */ + +"use strict"; + +// Test CSS autocompletion of the style attribute can be triggered when the +// caret is before a non-word character. + +loadHelperScript("helper_style_attr_test_runner.js"); + +const TEST_URL = URL_ROOT + "doc_markup_edit.html"; + +// test data format : +// [ +// what key to press, +// expected input box value after keypress, +// expected input.selectionStart, +// expected input.selectionEnd, +// is popup expected to be open ? +// ] +const TEST_DATA = [ + ["s", "s", 1, 1, false], + ["t", "st", 2, 2, false], + ["y", "sty", 3, 3, false], + ["l", "styl", 4, 4, false], + ["e", "style", 5, 5, false], + ["=", "style=", 6, 6, false], + ["\"", "style=\"", 7, 7, false], + ["\"", "style=\"\"", 8, 8, false], + ["VK_LEFT", "style=\"\"", 7, 7, false], + ["c", "style=\"color\"", 8, 12, true], + ["o", "style=\"color\"", 9, 12, true], + ["VK_RIGHT", "style=\"color\"", 12, 12, false], + [":", "style=\"color:aliceblue\"", 13, 22, true], + ["b", "style=\"color:beige\"", 14, 18, true], + ["VK_RIGHT", "style=\"color:beige\"", 18, 18, false], + [";", "style=\"color:beige;\"", 19, 19, false], + [";", "style=\"color:beige;;\"", 20, 20, false], + ["VK_LEFT", "style=\"color:beige;;\"", 19, 19, false], + ["p", "style=\"color:beige;padding;\"", 20, 26, true], + ["VK_RIGHT", "style=\"color:beige;padding;\"", 26, 26, false], + [":", "style=\"color:beige;padding:calc;\"", 27, 31, true], + ["0", "style=\"color:beige;padding:0;\"", 28, 28, false], + ["VK_RETURN", "style=\"color:beige;padding:0;\"", + -1, -1, false] +]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.js new file mode 100644 index 000000000..0c25e2fc6 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.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 dragging a node near the top or bottom edge of the markup-view +// auto-scrolls the view on a large toolbox. + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop_autoscroll_01.html"; + +add_task(function* () { + // Set the toolbox as large as it would get. The toolbox automatically shrinks + // to not overflow to window. + yield pushPref("devtools.toolbox.footer.height", 10000); + + let {inspector} = yield openInspectorForURL(TEST_URL); + let markup = inspector.markup; + let viewHeight = markup.doc.documentElement.clientHeight; + + info("Pretend the markup-view is dragging"); + markup.isDragging = true; + + info("Simulate a mousemove on the view, at the bottom, and expect scrolling"); + let onScrolled = waitForScrollStop(markup.doc); + + markup._onMouseMove({ + preventDefault: () => {}, + target: markup.doc.body, + pageY: viewHeight + markup.doc.defaultView.scrollY + }); + + let bottomScrollPos = yield onScrolled; + ok(bottomScrollPos > 0, "The view was scrolled down"); + + info("Simulate a mousemove at the top and expect more scrolling"); + onScrolled = waitForScrollStop(markup.doc); + + markup._onMouseMove({ + preventDefault: () => {}, + target: markup.doc.body, + pageY: markup.doc.defaultView.scrollY + }); + + let topScrollPos = yield onScrolled; + ok(topScrollPos < bottomScrollPos, "The view was scrolled up"); + is(topScrollPos, 0, "The view was scrolled up to the top"); + + info("Simulate a mouseup to stop dragging"); + markup._onMouseUp(); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js new file mode 100644 index 000000000..4aca6f424 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js @@ -0,0 +1,49 @@ +/* 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 dragging a node near the top or bottom edge of the markup-view +// auto-scrolls the view on a small toolbox. + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop_autoscroll_02.html"; + +add_task(function* () { + // Set the toolbox to very small in size. + yield pushPref("devtools.toolbox.footer.height", 150); + + let {inspector} = yield openInspectorForURL(TEST_URL); + let markup = inspector.markup; + let viewHeight = markup.doc.documentElement.clientHeight; + + info("Pretend the markup-view is dragging"); + markup.isDragging = true; + + info("Simulate a mousemove on the view, at the bottom, and expect scrolling"); + let onScrolled = waitForScrollStop(markup.doc); + + markup._onMouseMove({ + preventDefault: () => {}, + target: markup.doc.body, + pageY: viewHeight + markup.doc.defaultView.scrollY + }); + + let bottomScrollPos = yield onScrolled; + ok(bottomScrollPos > 0, "The view was scrolled down"); + info("Simulate a mousemove at the top and expect more scrolling"); + onScrolled = waitForScrollStop(markup.doc); + + markup._onMouseMove({ + preventDefault: () => {}, + target: markup.doc.body, + pageY: markup.doc.defaultView.scrollY + }); + + let topScrollPos = yield onScrolled; + ok(topScrollPos < bottomScrollPos, "The view was scrolled up"); + is(topScrollPos, 0, "The view was scrolled up to the top"); + + info("Simulate a mouseup to stop dragging"); + markup._onMouseUp(); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_distance.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_distance.js new file mode 100644 index 000000000..e94b02191 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_distance.js @@ -0,0 +1,49 @@ +/* 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 nodes don't start dragging before the mouse has moved by at least +// the minimum vertical distance defined in markup-view.js by +// DRAG_DROP_MIN_INITIAL_DISTANCE. + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html"; +const TEST_NODE = "#test"; + +// Keep this in sync with DRAG_DROP_MIN_INITIAL_DISTANCE in markup-view.js +const MIN_DISTANCE = 10; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + info("Drag the test node by half of the minimum distance"); + yield simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE / 2); + yield checkIsDragging(inspector, TEST_NODE, false); + + info("Drag the test node by exactly the minimum distance"); + yield simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE); + yield checkIsDragging(inspector, TEST_NODE, true); + inspector.markup.cancelDragging(); + + info("Drag the test node by more than the minimum distance"); + yield simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE * 2); + yield checkIsDragging(inspector, TEST_NODE, true); + inspector.markup.cancelDragging(); + + info("Drag the test node by minus the minimum distance"); + yield simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE * -1); + yield checkIsDragging(inspector, TEST_NODE, true); + inspector.markup.cancelDragging(); +}); + +function* checkIsDragging(inspector, selector, isDragging) { + let container = yield getContainerForSelector(selector, inspector); + if (isDragging) { + ok(container.isDragging, "The container is being dragged"); + ok(inspector.markup.isDragging, "And the markup-view knows it"); + } else { + ok(!container.isDragging, "The container hasn't been marked as dragging"); + ok(!inspector.markup.isDragging, "And the markup-view either"); + } +} diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_dragRootNode.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_dragRootNode.js new file mode 100644 index 000000000..8bb4779d5 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_dragRootNode.js @@ -0,0 +1,22 @@ +/* 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 the root node isn't draggable (as well as head and body). + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html"; +const TEST_DATA = ["html", "head", "body"]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + for (let selector of TEST_DATA) { + info("Try to drag/drop node " + selector); + yield simulateNodeDrag(inspector, selector); + + let container = yield getContainerForSelector(selector, inspector); + ok(!container.isDragging, "The container hasn't been marked as dragging"); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_draggable.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_draggable.js new file mode 100644 index 000000000..1853ab4f7 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_draggable.js @@ -0,0 +1,63 @@ +/* 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 which nodes are consider draggable by the markup-view. + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html"; + +// Test cases should be objects with the following properties: +// - node {String|Function} A CSS selector that uniquely identifies the node to +// be tested. Or a generator function called in a Task that should return the +// corresponding MarkupContainer object to be tested. +// - draggable {Boolean} Whether or not the node should be draggable. +const TEST_DATA = [ + { node: "head", draggable: false }, + { node: "body", draggable: false }, + { node: "html", draggable: false }, + { node: "style", draggable: true }, + { node: "a", draggable: true }, + { node: "p", draggable: true }, + { node: "input", draggable: true }, + { node: "div", draggable: true }, + { + node: function* (inspector) { + let parentFront = yield getNodeFront("#before", inspector); + let {nodes} = yield inspector.walker.children(parentFront); + // Getting the comment node. + return getContainerForNodeFront(nodes[1], inspector); + }, + draggable: true + }, + { + node: function* (inspector) { + let parentFront = yield getNodeFront("#test", inspector); + let {nodes} = yield inspector.walker.children(parentFront); + // Getting the ::before pseudo element. + return getContainerForNodeFront(nodes[0], inspector); + }, + draggable: false + } +]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + yield inspector.markup.expandAll(); + + for (let {node, draggable} of TEST_DATA) { + let container; + let name; + if (typeof node === "string") { + container = yield getContainerForSelector(node, inspector); + name = node; + } else { + container = yield node(inspector); + name = container.toString(); + } + + let status = draggable ? "draggable" : "not draggable"; + info(`Testing ${name}, expecting it to be ${status}`); + is(container.isDraggable(), draggable, `The node is ${status}`); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_escapeKeyPress.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_escapeKeyPress.js new file mode 100644 index 000000000..075d14352 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_escapeKeyPress.js @@ -0,0 +1,34 @@ +/* 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 whether ESCAPE keypress cancels dragging of an element. + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + let {markup} = inspector; + + info("Get a test container"); + yield selectNode("#test", inspector); + let container = yield getContainerForSelector("#test", inspector); + + info("Simulate a drag/drop on this container"); + yield simulateNodeDrag(inspector, "#test"); + + ok(container.isDragging && markup.isDragging, + "The container is being dragged"); + ok(markup.doc.body.classList.contains("dragging"), + "The dragging css class was added"); + + info("Simulate ESCAPE keypress"); + EventUtils.sendKey("escape", inspector.panelWin); + + ok(!container.isDragging && !markup.isDragging, + "The dragging has stopped"); + ok(!markup.doc.body.classList.contains("dragging"), + "The dragging css class was removed"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js new file mode 100644 index 000000000..9eea6a102 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js @@ -0,0 +1,48 @@ +/* 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"; + +// Check that pseudo-elements and anonymous nodes are not draggable. + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html"; +const PREF = "devtools.inspector.showAllAnonymousContent"; + +add_task(function* () { + Services.prefs.setBoolPref(PREF, true); + + let {inspector} = yield openInspectorForURL(TEST_URL); + + info("Expanding nodes below #test"); + let parentFront = yield getNodeFront("#test", inspector); + yield inspector.markup.expandNode(parentFront); + yield waitForMultipleChildrenUpdates(inspector); + + info("Getting the ::before pseudo element and selecting it"); + let parentContainer = yield getContainerForNodeFront(parentFront, inspector); + let beforePseudo = parentContainer.elt.children[1].firstChild.container; + parentContainer.elt.scrollIntoView(true); + yield selectNode(beforePseudo.node, inspector); + + info("Simulate dragging the ::before pseudo element"); + yield simulateNodeDrag(inspector, beforePseudo); + + ok(!beforePseudo.isDragging, "::before pseudo element isn't dragging"); + + info("Expanding nodes below #anonymousParent"); + let inputFront = yield getNodeFront("#anonymousParent", inspector); + yield inspector.markup.expandNode(inputFront); + yield waitForMultipleChildrenUpdates(inspector); + + info("Getting the anonymous node and selecting it"); + let inputContainer = yield getContainerForNodeFront(inputFront, inspector); + let anonymousDiv = inputContainer.elt.children[1].firstChild.container; + inputContainer.elt.scrollIntoView(true); + yield selectNode(anonymousDiv.node, inspector); + + info("Simulate dragging the anonymous node"); + yield simulateNodeDrag(inspector, anonymousDiv); + + ok(!anonymousDiv.isDragging, "anonymous node isn't dragging"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_reorder.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_reorder.js new file mode 100644 index 000000000..f74b50147 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_reorder.js @@ -0,0 +1,109 @@ +/* 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"; + +requestLongerTimeout(2); + +// Test different kinds of drag and drop node re-ordering. + +const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + let ids; + + info("Expand #test node"); + let parentFront = yield getNodeFront("#test", inspector); + yield inspector.markup.expandNode(parentFront); + yield waitForMultipleChildrenUpdates(inspector); + + info("Scroll #test into view"); + let parentContainer = yield getContainerForNodeFront(parentFront, inspector); + parentContainer.elt.scrollIntoView(true); + + info("Test putting an element back at its original place"); + yield dragElementToOriginalLocation("#firstChild", inspector); + ids = yield getChildrenIDsOf(parentFront, inspector); + is(ids[0], "firstChild", + "#firstChild is still the first child of #test"); + is(ids[1], "middleChild", + "#middleChild is still the second child of #test"); + + info("Testing switching elements inside their parent"); + yield moveElementDown("#firstChild", "#middleChild", inspector); + ids = yield getChildrenIDsOf(parentFront, inspector); + is(ids[0], "middleChild", + "#firstChild is now the second child of #test"); + is(ids[1], "firstChild", + "#middleChild is now the first child of #test"); + + info("Testing switching elements with a last child"); + yield moveElementDown("#firstChild", "#lastChild", inspector); + ids = yield getChildrenIDsOf(parentFront, inspector); + is(ids[1], "lastChild", + "#lastChild is now the second child of #test"); + is(ids[2], "firstChild", + "#firstChild is now the last child of #test"); + + info("Testing appending element to a parent"); + yield moveElementDown("#before", "#test", inspector); + ids = yield getChildrenIDsOf(parentFront, inspector); + is(ids.length, 4, + "New element appended to #test"); + is(ids[0], "before", + "New element is appended at the right place (currently first child)"); + + info("Testing moving element to after it's parent"); + yield moveElementDown("#firstChild", "#test", inspector); + ids = yield getChildrenIDsOf(parentFront, inspector); + is(ids.length, 3, + "#firstChild is no longer #test's child"); + let siblingFront = yield inspector.walker.nextSibling(parentFront); + is(siblingFront.id, "firstChild", + "#firstChild is now #test's nextElementSibling"); +}); + +function* dragElementToOriginalLocation(selector, inspector) { + info("Picking up and putting back down " + selector); + + function onMutation() { + ok(false, "Mutation received from dragging a node back to its location"); + } + inspector.on("markupmutation", onMutation); + yield simulateNodeDragAndDrop(inspector, selector, 0, 0); + + // Wait a bit to make sure the event never fires. + // This doesn't need to catch *all* cases, since the mutation + // will cause failure later in the test when it checks element ordering. + yield wait(500); + inspector.off("markupmutation", onMutation); +} + +function* moveElementDown(selector, next, inspector) { + info("Switching " + selector + " with " + next); + + let container = yield getContainerForSelector(next, inspector); + let height = container.tagLine.getBoundingClientRect().height; + + let onMutated = inspector.once("markupmutation"); + let uiUpdate = inspector.once("inspector-updated"); + + yield simulateNodeDragAndDrop(inspector, selector, 0, Math.round(height) + 2); + + let mutations = yield onMutated; + yield uiUpdate; + + is(mutations.length, 2, "2 mutations were received"); +} + +function* getChildrenIDsOf(parentFront, {walker}) { + let {nodes} = yield walker.children(parentFront); + // Filter out non-element nodes since children also returns pseudo-elements. + return nodes.filter(node => { + return !node.isPseudoElement; + }).map(node => { + return node.id; + }); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_dragdrop_tooltip.js b/devtools/client/inspector/markup/test/browser_markup_dragdrop_tooltip.js new file mode 100644 index 000000000..77472800e --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_tooltip.js @@ -0,0 +1,35 @@ +/* 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 tooltips don't appear when dragging over tooltip targets. + +const TEST_URL = "data:text/html;charset=utf8,<img src=\"about:logo\" /><div>"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + let {markup} = inspector; + + info("Get the tooltip target element for the image's src attribute"); + let img = yield getContainerForSelector("img", inspector); + let target = img.editor.getAttributeElement("src").querySelector(".link"); + + info("Check that the src attribute of the image is a valid tooltip target"); + let isValid = yield isHoverTooltipTarget(markup.imagePreviewTooltip, target); + ok(isValid, "The element is a valid tooltip target"); + + info("Start dragging the test div"); + yield simulateNodeDrag(inspector, "div"); + + info("Now check that the src attribute of the image isn't a valid target"); + isValid = yield isHoverTooltipTarget(markup.imagePreviewTooltip, target); + ok(!isValid, "The element is not a valid tooltip target"); + + info("Stop dragging the test div"); + yield simulateNodeDrop(inspector, "div"); + + info("Check again the src attribute of the image"); + isValid = yield isHoverTooltipTarget(markup.imagePreviewTooltip, target); + ok(isValid, "The element is a valid tooltip target"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events-overflow.js b/devtools/client/inspector/markup/test/browser_markup_events-overflow.js new file mode 100644 index 000000000..3e73921f4 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events-overflow.js @@ -0,0 +1,91 @@ +/* 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"; + +const TEST_URL = URL_ROOT + "doc_markup_events-overflow.html"; +const TEST_DATA = [ + { + desc: "editor overflows container", + // scroll to bottom + initialScrollTop: -1, + // last header + headerToClick: 49, + alignBottom: true, + alignTop: false, + }, + { + desc: "header overflows the container", + initialScrollTop: 2, + headerToClick: 0, + alignBottom: false, + alignTop: true, + }, + { + desc: "neither header nor editor overflows the container", + initialScrollTop: 2, + headerToClick: 5, + alignBottom: false, + alignTop: false, + }, +]; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URL); + + let markupContainer = yield getContainerForSelector("#events", inspector); + let evHolder = markupContainer.elt.querySelector(".markupview-events"); + let tooltip = inspector.markup.eventDetailsTooltip; + + info("Clicking to open event tooltip."); + EventUtils.synthesizeMouseAtCenter(evHolder, {}, + inspector.markup.doc.defaultView); + yield tooltip.once("shown"); + info("EventTooltip visible."); + + let container = tooltip.panel; + let containerRect = container.getBoundingClientRect(); + let headers = container.querySelectorAll(".event-header"); + + for (let data of TEST_DATA) { + info("Testing scrolling when " + data.desc); + + if (data.initialScrollTop < 0) { + info("Scrolling container to the bottom."); + let newScrollTop = container.scrollHeight - container.clientHeight; + data.initialScrollTop = container.scrollTop = newScrollTop; + } else { + info("Scrolling container by " + data.initialScrollTop + "px"); + container.scrollTop = data.initialScrollTop; + } + + is(container.scrollTop, data.initialScrollTop, "Container scrolled."); + + info("Clicking on header #" + data.headerToClick); + let header = headers[data.headerToClick]; + + let ready = tooltip.once("event-tooltip-ready"); + EventUtils.synthesizeMouseAtCenter(header, {}, header.ownerGlobal); + yield ready; + + info("Event handler expanded."); + + // Wait for any scrolling to finish. + yield promiseNextTick(); + + if (data.alignTop) { + let headerRect = header.getBoundingClientRect(); + + is(Math.round(headerRect.top), Math.round(containerRect.top), + "Clicked header is aligned with the container top."); + } else if (data.alignBottom) { + let editorRect = header.nextElementSibling.getBoundingClientRect(); + + is(Math.round(editorRect.bottom), Math.round(containerRect.bottom), + "Clicked event handler code is aligned with the container bottom."); + } else { + is(container.scrollTop, data.initialScrollTop, + "Container did not scroll, as expected."); + } + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events-windowed-host.js b/devtools/client/inspector/markup/test/browser_markup_events-windowed-host.js new file mode 100644 index 000000000..cfcb0a8ab --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events-windowed-host.js @@ -0,0 +1,61 @@ +/* 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 the event details tooltip can be hidden by clicking outside of the tooltip + * after switching hosts. + */ + +const TEST_URL = URL_ROOT + "doc_markup_events-overflow.html"; + +registerCleanupFunction(() => { + // Restore the default Toolbox host position after the test. + Services.prefs.clearUserPref("devtools.toolbox.host"); +}); + +add_task(function* () { + let { inspector, toolbox } = yield openInspectorForURL(TEST_URL); + yield runTests(inspector); + + yield toolbox.switchHost("window"); + yield runTests(inspector); + + yield toolbox.switchHost("bottom"); + yield runTests(inspector); + + yield toolbox.destroy(); +}); + +function* runTests(inspector) { + let markupContainer = yield getContainerForSelector("#events", inspector); + let evHolder = markupContainer.elt.querySelector(".markupview-events"); + let tooltip = inspector.markup.eventDetailsTooltip; + + info("Clicking to open event tooltip."); + + let onInspectorUpdated = inspector.once("inspector-updated"); + let onTooltipShown = tooltip.once("shown"); + EventUtils.synthesizeMouseAtCenter(evHolder, {}, inspector.markup.doc.defaultView); + + yield onTooltipShown; + // New node is selected when clicking on the events bubble, wait for inspector-updated. + yield onInspectorUpdated; + + ok(tooltip.isVisible(), "EventTooltip visible."); + + onInspectorUpdated = inspector.once("inspector-updated"); + let onTooltipHidden = tooltip.once("hidden"); + + info("Click on another tag to hide the event tooltip"); + let h1 = yield getContainerForSelector("h1", inspector); + let tag = h1.elt.querySelector(".tag"); + EventUtils.synthesizeMouseAtCenter(tag, {}, inspector.markup.doc.defaultView); + + yield onTooltipHidden; + // New node is selected, wait for inspector-updated. + yield onInspectorUpdated; + + ok(!tooltip.isVisible(), "EventTooltip hidden."); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_events1.js b/devtools/client/inspector/markup/test/browser_markup_events1.js new file mode 100644 index 000000000..dbfd4a5c3 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events1.js @@ -0,0 +1,149 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ + +"use strict"; + +// Test that markup view event bubbles show the correct event info for DOM +// events. + +const TEST_URL = URL_ROOT + "doc_markup_events1.html"; + +loadHelperScript("helper_events_test_runner.js"); + +const TEST_DATA = [ // eslint-disable-line + { + selector: "html", + expected: [ + { + type: "load", + filename: TEST_URL, + attributes: [ + "Bubbling", + "DOM0" + ], + handler: "init();" + } + ] + }, + { + selector: "#container", + expected: [ + { + type: "mouseover", + filename: TEST_URL + ":45", + attributes: [ + "Capturing", + "DOM2" + ], + handler: "function mouseoverHandler(event) {\n" + + " if (event.target.id !== \"container\") {\n" + + " let output = document.getElementById(\"output\");\n" + + " output.textContent = event.target.textContent;\n" + + " }\n" + + "}" + } + ] + }, + { + selector: "#multiple", + expected: [ + { + type: "click", + filename: TEST_URL + ":52", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "function clickHandler(event) {\n" + + " let output = document.getElementById(\"output\");\n" + + " output.textContent = \"click\";\n" + + "}" + }, + { + type: "mouseup", + filename: TEST_URL + ":57", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "function mouseupHandler(event) {\n" + + " let output = document.getElementById(\"output\");\n" + + " output.textContent = \"mouseup\";\n" + + "}" + } + ] + }, + // #noevents tests check that dynamically added events are properly displayed + // in the markupview + { + selector: "#noevents", + expected: [] + }, + { + selector: "#noevents", + beforeTest: function* (inspector, testActor) { + let nodeMutated = inspector.once("markupmutation"); + yield testActor.eval("window.wrappedJSObject.addNoeventsClickHandler();"); + yield nodeMutated; + }, + expected: [ + { + type: "click", + filename: TEST_URL + ":72", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "function noeventsClickHandler(event) {\n" + + " alert(\"noevents has an event listener\");\n" + + "}" + } + ] + }, + { + selector: "#noevents", + beforeTest: function* (inspector, testActor) { + let nodeMutated = inspector.once("markupmutation"); + yield testActor.eval( + "window.wrappedJSObject.removeNoeventsClickHandler();"); + yield nodeMutated; + }, + expected: [] + }, + { + selector: "#DOM0", + expected: [ + { + type: "click", + filename: TEST_URL, + attributes: [ + "Bubbling", + "DOM0" + ], + handler: "alert('DOM0')" + } + ] + }, + { + selector: "#handleevent", + expected: [ + { + type: "click", + filename: TEST_URL + ":67", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "handleEvent: function(blah) {\n" + + " alert(\"handleEvent\");\n" + + "}" + } + ] + } +]; + +add_task(function* () { + yield runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events2.js b/devtools/client/inspector/markup/test/browser_markup_events2.js new file mode 100644 index 000000000..3e741cf1f --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events2.js @@ -0,0 +1,163 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ + +"use strict"; + +// Test that markup view event bubbles show the correct event info for DOM +// events. + +const TEST_URL = URL_ROOT + "doc_markup_events2.html"; + +loadHelperScript("helper_events_test_runner.js"); + +const TEST_DATA = [ // eslint-disable-line + { + selector: "#fatarrow", + expected: [ + { + type: "click", + filename: TEST_URL + ":39", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "() => {\n" + + " alert(\"Fat arrow without params!\");\n" + + "}" + }, + { + type: "click", + filename: TEST_URL + ":43", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "event => {\n" + + " alert(\"Fat arrow with 1 param!\");\n" + + "}" + }, + { + type: "click", + filename: TEST_URL + ":47", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "(event, foo, bar) => {\n" + + " alert(\"Fat arrow with 3 params!\");\n" + + "}" + }, + { + type: "click", + filename: TEST_URL + ":51", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "b => b" + } + ] + }, + { + selector: "#bound", + expected: [ + { + type: "click", + filename: TEST_URL + ":62", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "function boundClickHandler(event) {\n" + + " alert(\"Bound event\");\n" + + "}" + } + ] + }, + { + selector: "#boundhe", + expected: [ + { + type: "click", + filename: TEST_URL + ":85", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "handleEvent: function() {\n" + + " alert(\"boundHandleEvent\");\n" + + "}" + } + ] + }, + { + selector: "#comment-inline", + expected: [ + { + type: "click", + filename: TEST_URL + ":91", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "function functionProceededByInlineComment() {\n" + + " alert(\"comment-inline\");\n" + + "}" + } + ] + }, + { + selector: "#comment-streaming", + expected: [ + { + type: "click", + filename: TEST_URL + ":96", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "function functionProceededByStreamingComment() {\n" + + " alert(\"comment-streaming\");\n" + + "}" + } + ] + }, + { + selector: "#anon-object-method", + expected: [ + { + type: "click", + filename: TEST_URL + ":71", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "anonObjectMethod: function() {\n" + + " alert(\"obj.anonObjectMethod\");\n" + + "}" + } + ] + }, + { + selector: "#object-method", + expected: [ + { + type: "click", + filename: TEST_URL + ":75", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "objectMethod: function kay() {\n" + + " alert(\"obj.objectMethod\");\n" + + "}" + } + ] + } +]; + +add_task(function* () { + yield runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events3.js b/devtools/client/inspector/markup/test/browser_markup_events3.js new file mode 100644 index 000000000..a9dc2a499 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events3.js @@ -0,0 +1,161 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ + +"use strict"; + +// Test that markup view event bubbles show the correct event info for DOM +// events. + +const TEST_URL = URL_ROOT + "doc_markup_events3.html"; + +loadHelperScript("helper_events_test_runner.js"); + +const TEST_DATA = [ // eslint-disable-line + { + selector: "#es6-method", + expected: [ + { + type: "click", + filename: TEST_URL + ":91", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "es6Method() {\n" + + " alert(\"obj.es6Method\");\n" + + "}" + } + ] + }, + { + selector: "#generator", + expected: [ + { + type: "click", + filename: TEST_URL + ":96", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "function* generator() {\n" + + " alert(\"generator\");\n" + + "}" + } + ] + }, + { + selector: "#anon-generator", + expected: [ + { + type: "click", + filename: TEST_URL + ":55", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "function*() {\n" + + " alert(\"anonGenerator\");\n" + + "}" + } + ] + }, + { + selector: "#named-function-expression", + expected: [ + { + type: "click", + filename: TEST_URL + ":23", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "let namedFunctionExpression =\n" + + " function foo() {\n" + + " alert(\"namedFunctionExpression\");\n" + + " }" + } + ] + }, + { + selector: "#anon-function-expression", + expected: [ + { + type: "click", + filename: TEST_URL + ":27", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "let anonFunctionExpression = function() {\n" + + " alert(\"anonFunctionExpression\");\n" + + "}" + } + ] + }, + { + selector: "#returned-function", + expected: [ + { + type: "click", + filename: TEST_URL + ":32", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "function bar() {\n" + + " alert(\"returnedFunction\");\n" + + "}" + } + ] + }, + { + selector: "#constructed-function", + expected: [ + { + type: "click", + filename: TEST_URL + ":1", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "" + } + ] + }, + { + selector: "#constructed-function-with-body-string", + expected: [ + { + type: "click", + filename: TEST_URL + ":1", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "alert(\"constructedFuncWithBodyString\");" + } + ] + }, + { + selector: "#multiple-assignment", + expected: [ + { + type: "click", + filename: TEST_URL + ":42", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "let multipleAssignment = foo = bar = function multi() {\n" + + " alert(\"multipleAssignment\");\n" + + "}" + } + ] + }, +]; + +add_task(function* () { + yield runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_form.js b/devtools/client/inspector/markup/test/browser_markup_events_form.js new file mode 100644 index 000000000..ab029720c --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_form.js @@ -0,0 +1,61 @@ +/* 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"; + +// Testing the feature whereby custom registered actors can listen to +// 'form' events sent by the NodeActor to hook custom data to it. +// The test registers one backend actor providing custom form data +// and checks that the value is properly sent to the client (NodeFront). + +const TEST_PAGE_URL = URL_ROOT + "doc_markup_events_form.html"; +const TEST_ACTOR_URL = CHROME_URL_ROOT + "actor_events_form.js"; + +var {EventsFormFront} = require(TEST_ACTOR_URL); + +add_task(function* () { + info("Opening the Toolbox"); + let tab = yield addTab(TEST_PAGE_URL); + let toolbox = yield openToolboxForTab(tab, "webconsole"); + + info("Registering test actor"); + let {registrar, front} = yield registerTestActor(toolbox); + + info("Selecting the Inspector panel"); + let inspector = yield toolbox.selectTool("inspector"); + let container = yield getContainerForSelector("#container", inspector); + isnot(container, null, "There must be requested container"); + + let nodeFront = container.node; + let value = nodeFront.getFormProperty("test-property"); + is(value, "test-value", "There must be custom property"); + + info("Unregistering actor"); + yield unregisterActor(registrar, front); +}); + +function registerTestActor(toolbox) { + let deferred = defer(); + + let options = { + prefix: "eventsFormActor", + actorClass: "EventsFormActor", + moduleUrl: TEST_ACTOR_URL, + }; + + // Register as a tab actor + let client = toolbox.target.client; + registerTabActor(client, options).then(({registrar, form}) => { + // Attach to the registered actor + let front = EventsFormFront(client, form); + front.attach().then(() => { + deferred.resolve({ + front: front, + registrar: registrar, + }); + }); + }); + + return deferred.promise; +} diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.0.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.0.js new file mode 100644 index 000000000..7413ea660 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.0.js @@ -0,0 +1,237 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 1.0). + +const TEST_LIB = "lib_jquery_1.0.js"; +const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "load", + filename: URL_ROOT + TEST_LIB, + attributes: [ + "jQuery" + ], + handler: "ready: function() {\n" + + " // Make sure that the DOM is not already loaded\n" + + " if (!jQuery.isReady) {\n" + + " // Remember that the DOM is ready\n" + + " jQuery.isReady = true;\n" + + "\n" + + " // If there are functions bound, to execute\n" + + " if (jQuery.readyList) {\n" + + " // Execute all of them\n" + + " for (var i = 0; i < jQuery.readyList.length; i++)\n" + + " jQuery.readyList[i].apply(document);\n" + + "\n" + + " // Reset the list of functions\n" + + " jQuery.readyList = null;\n" + + " }\n" + + " }\n" + + "}" + }, + { + type: "load", + filename: TEST_URL, + attributes: [ + "Bubbling", + "DOM0" + ], + handler: "() => {\n" + + " var handler1 = function liveDivDblClick() {\n" + + " alert(1);\n" + + " };\n" + + " var handler2 = function liveDivDragStart() {\n" + + " alert(2);\n" + + " };\n" + + " var handler3 = function liveDivDragLeave() {\n" + + " alert(3);\n" + + " };\n" + + " var handler4 = function liveDivDragEnd() {\n" + + " alert(4);\n" + + " };\n" + + " var handler5 = function liveDivDrop() {\n" + + " alert(5);\n" + + " };\n" + + " var handler6 = function liveDivDragOver() {\n" + + " alert(6);\n" + + " };\n" + + " var handler7 = function divClick1() {\n" + + " alert(7);\n" + + " };\n" + + " var handler8 = function divClick2() {\n" + + " alert(8);\n" + + " };\n" + + " var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + " };\n" + + " var handler10 = function divDragOut() {\n" + + " alert(10);\n" + + " };\n" + + "\n" + + " if ($(\"#livediv\").live) {\n" + + " $(\"#livediv\").live(\"dblclick\", handler1);\n" + + " $(\"#livediv\").live(\"dragstart\", handler2);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").delegate) {\n" + + " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" + + " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").on) {\n" + + " $(document).on(\"drop\", \"#livediv\", handler5);\n" + + " $(document).on(\"dragover\", \"#livediv\", handler6);\n" + + " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" + + " }\n" + + "\n" + + " var div = $(\"div\")[0];\n" + + " $(div).click(handler7);\n" + + " $(div).click(handler8);\n" + + " $(div).keydown(handler9);\n" + + "}" + }, + { + type: "load", + filename: URL_ROOT + TEST_LIB, + attributes: [ + "Bubbling", + "DOM0" + ], + handler: "handle: function(event) {\n" + + " if (typeof jQuery == \"undefined\") return;\n" + + "\n" + + " event = event || jQuery.event.fix(window.event);\n" + + "\n" + + " // If no correct event was found, fail\n" + + " if (!event) return;\n" + + "\n" + + " var returnValue = true;\n" + + "\n" + + " var c = this.events[event.type];\n" + + "\n" + + " for (var j in c) {\n" + + " if (c[j].apply(this, [event]) === false) {\n" + + " event.preventDefault();\n" + + " event.stopPropagation();\n" + + " returnValue = false;\n" + + " }\n" + + " }\n" + + "\n" + + " return returnValue;\n" + + "}" + } + ] + }, + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":34", + attributes: [ + "jQuery" + ], + handler: "var handler7 = function divClick1() {\n" + + " alert(7);\n" + + "}" + }, + { + type: "click", + filename: TEST_URL + ":35", + attributes: [ + "jQuery" + ], + handler: "var handler8 = function divClick2() {\n" + + " alert(8);\n" + + "}" + }, + { + type: "click", + filename: URL_ROOT + TEST_LIB + ":894", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "handle: function(event) {\n" + + " if (typeof jQuery == \"undefined\") return;\n" + + "\n" + + " event = event || jQuery.event.fix(window.event);\n" + + "\n" + + " // If no correct event was found, fail\n" + + " if (!event) return;\n" + + "\n" + + " var returnValue = true;\n" + + "\n" + + " var c = this.events[event.type];\n" + + "\n" + + " for (var j in c) {\n" + + " if (c[j].apply(this, [event]) === false) {\n" + + " event.preventDefault();\n" + + " event.stopPropagation();\n" + + " returnValue = false;\n" + + " }\n" + + " }\n" + + "\n" + + " return returnValue;\n" + + "}" + }, + { + type: "keydown", + filename: TEST_URL + ":36", + attributes: [ + "jQuery" + ], + handler: "var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + "}" + }, + { + type: "keydown", + filename: URL_ROOT + TEST_LIB + ":894", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "handle: function(event) {\n" + + " if (typeof jQuery == \"undefined\") return;\n" + + "\n" + + " event = event || jQuery.event.fix(window.event);\n" + + "\n" + + " // If no correct event was found, fail\n" + + " if (!event) return;\n" + + "\n" + + " var returnValue = true;\n" + + "\n" + + " var c = this.events[event.type];\n" + + "\n" + + " for (var j in c) {\n" + + " if (c[j].apply(this, [event]) === false) {\n" + + " event.preventDefault();\n" + + " event.stopPropagation();\n" + + " returnValue = false;\n" + + " }\n" + + " }\n" + + "\n" + + " return returnValue;\n" + + "}" + } + ] + }, +]; +/*eslint-enable */ + +add_task(function* () { + yield runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.1.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.1.js new file mode 100644 index 000000000..e5e995a87 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.1.js @@ -0,0 +1,271 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 1.1). + +const TEST_LIB = "lib_jquery_1.1.js"; +const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "load", + filename: URL_ROOT + TEST_LIB, + attributes: [ + "jQuery" + ], + handler: "ready: function() {\n" + + " // Make sure that the DOM is not already loaded\n" + + " if (!jQuery.isReady) {\n" + + " // Remember that the DOM is ready\n" + + " jQuery.isReady = true;\n" + + "\n" + + " // If there are functions bound, to execute\n" + + " if (jQuery.readyList) {\n" + + " // Execute all of them\n" + + " jQuery.each(jQuery.readyList, function() {\n" + + " this.apply(document);\n" + + " });\n" + + "\n" + + " // Reset the list of functions\n" + + " jQuery.readyList = null;\n" + + " }\n" + + " // Remove event lisenter to avoid memory leak\n" + + " if (jQuery.browser.mozilla || jQuery.browser.opera)\n" + + " document.removeEventListener(\"DOMContentLoaded\", jQuery.ready, false);\n" + + " }\n" + + "}" + }, + { + type: "load", + filename: TEST_URL, + attributes: [ + "Bubbling", + "DOM0" + ], + handler: "() => {\n" + + " var handler1 = function liveDivDblClick() {\n" + + " alert(1);\n" + + " };\n" + + " var handler2 = function liveDivDragStart() {\n" + + " alert(2);\n" + + " };\n" + + " var handler3 = function liveDivDragLeave() {\n" + + " alert(3);\n" + + " };\n" + + " var handler4 = function liveDivDragEnd() {\n" + + " alert(4);\n" + + " };\n" + + " var handler5 = function liveDivDrop() {\n" + + " alert(5);\n" + + " };\n" + + " var handler6 = function liveDivDragOver() {\n" + + " alert(6);\n" + + " };\n" + + " var handler7 = function divClick1() {\n" + + " alert(7);\n" + + " };\n" + + " var handler8 = function divClick2() {\n" + + " alert(8);\n" + + " };\n" + + " var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + " };\n" + + " var handler10 = function divDragOut() {\n" + + " alert(10);\n" + + " };\n" + + "\n" + + " if ($(\"#livediv\").live) {\n" + + " $(\"#livediv\").live(\"dblclick\", handler1);\n" + + " $(\"#livediv\").live(\"dragstart\", handler2);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").delegate) {\n" + + " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" + + " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").on) {\n" + + " $(document).on(\"drop\", \"#livediv\", handler5);\n" + + " $(document).on(\"dragover\", \"#livediv\", handler6);\n" + + " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" + + " }\n" + + "\n" + + " var div = $(\"div\")[0];\n" + + " $(div).click(handler7);\n" + + " $(div).click(handler8);\n" + + " $(div).keydown(handler9);\n" + + "}" + }, + { + type: "load", + filename: URL_ROOT + TEST_LIB, + attributes: [ + "Bubbling", + "DOM0" + ], + handler: "handle: function(event) {\n" + + " if (typeof jQuery == \"undefined\") return false;\n" + + "\n" + + " // Empty object is for triggered events with no data\n" + + " event = jQuery.event.fix(event || window.event || {});\n" + + "\n" + + " // returned undefined or false\n" + + " var returnValue;\n" + + "\n" + + " var c = this.events[event.type];\n" + + "\n" + + " var args = [].slice.call(arguments, 1);\n" + + " args.unshift(event);\n" + + "\n" + + " for (var j in c) {\n" + + " // Pass in a reference to the handler function itself\n" + + " // So that we can later remove it\n" + + " args[0].handler = c[j];\n" + + " args[0].data = c[j].data;\n" + + "\n" + + " if (c[j].apply(this, args) === false) {\n" + + " event.preventDefault();\n" + + " event.stopPropagation();\n" + + " returnValue = false;\n" + + " }\n" + + " }\n" + + "\n" + + " // Clean up added properties in IE to prevent memory leak\n" + + " if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;\n" + + "\n" + + " return returnValue;\n" + + "}" + } + ] + }, + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":34", + attributes: [ + "jQuery" + ], + handler: "var handler7 = function divClick1() {\n" + + " alert(7);\n" + + "}" + }, + { + type: "click", + filename: TEST_URL + ":35", + attributes: [ + "jQuery" + ], + handler: "var handler8 = function divClick2() {\n" + + " alert(8);\n" + + "}" + }, + { + type: "click", + filename: URL_ROOT + TEST_LIB + ":1224", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "handle: function(event) {\n" + + " if (typeof jQuery == \"undefined\") return false;\n" + + "\n" + + " // Empty object is for triggered events with no data\n" + + " event = jQuery.event.fix(event || window.event || {});\n" + + "\n" + + " // returned undefined or false\n" + + " var returnValue;\n" + + "\n" + + " var c = this.events[event.type];\n" + + "\n" + + " var args = [].slice.call(arguments, 1);\n" + + " args.unshift(event);\n" + + "\n" + + " for (var j in c) {\n" + + " // Pass in a reference to the handler function itself\n" + + " // So that we can later remove it\n" + + " args[0].handler = c[j];\n" + + " args[0].data = c[j].data;\n" + + "\n" + + " if (c[j].apply(this, args) === false) {\n" + + " event.preventDefault();\n" + + " event.stopPropagation();\n" + + " returnValue = false;\n" + + " }\n" + + " }\n" + + "\n" + + " // Clean up added properties in IE to prevent memory leak\n" + + " if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;\n" + + "\n" + + " return returnValue;\n" + + "}" + }, + { + type: "keydown", + filename: TEST_URL + ":36", + attributes: [ + "jQuery" + ], + handler: "var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + "}" + }, + { + type: "keydown", + filename: URL_ROOT + TEST_LIB + ":1224", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "handle: function(event) {\n" + + " if (typeof jQuery == \"undefined\") return false;\n" + + "\n" + + " // Empty object is for triggered events with no data\n" + + " event = jQuery.event.fix(event || window.event || {});\n" + + "\n" + + " // returned undefined or false\n" + + " var returnValue;\n" + + "\n" + + " var c = this.events[event.type];\n" + + "\n" + + " var args = [].slice.call(arguments, 1);\n" + + " args.unshift(event);\n" + + "\n" + + " for (var j in c) {\n" + + " // Pass in a reference to the handler function itself\n" + + " // So that we can later remove it\n" + + " args[0].handler = c[j];\n" + + " args[0].data = c[j].data;\n" + + "\n" + + " if (c[j].apply(this, args) === false) {\n" + + " event.preventDefault();\n" + + " event.stopPropagation();\n" + + " returnValue = false;\n" + + " }\n" + + " }\n" + + "\n" + + " // Clean up added properties in IE to prevent memory leak\n" + + " if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;\n" + + "\n" + + " return returnValue;\n" + + "}" + } + ] + } +]; +/*eslint-enable */ + +add_task(function* () { + yield runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.11.1.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.11.1.js new file mode 100644 index 000000000..17d59a317 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.11.1.js @@ -0,0 +1,196 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 1.11.1). + +const TEST_LIB = "lib_jquery_1.11.1_min.js"; +const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "load", + filename: TEST_URL + ":27", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "() => {\n" + + " var handler1 = function liveDivDblClick() {\n" + + " alert(1);\n" + + " };\n" + + " var handler2 = function liveDivDragStart() {\n" + + " alert(2);\n" + + " };\n" + + " var handler3 = function liveDivDragLeave() {\n" + + " alert(3);\n" + + " };\n" + + " var handler4 = function liveDivDragEnd() {\n" + + " alert(4);\n" + + " };\n" + + " var handler5 = function liveDivDrop() {\n" + + " alert(5);\n" + + " };\n" + + " var handler6 = function liveDivDragOver() {\n" + + " alert(6);\n" + + " };\n" + + " var handler7 = function divClick1() {\n" + + " alert(7);\n" + + " };\n" + + " var handler8 = function divClick2() {\n" + + " alert(8);\n" + + " };\n" + + " var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + " };\n" + + " var handler10 = function divDragOut() {\n" + + " alert(10);\n" + + " };\n" + + "\n" + + " if ($(\"#livediv\").live) {\n" + + " $(\"#livediv\").live(\"dblclick\", handler1);\n" + + " $(\"#livediv\").live(\"dragstart\", handler2);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").delegate) {\n" + + " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" + + " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").on) {\n" + + " $(document).on(\"drop\", \"#livediv\", handler5);\n" + + " $(document).on(\"dragover\", \"#livediv\", handler6);\n" + + " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" + + " }\n" + + "\n" + + " var div = $(\"div\")[0];\n" + + " $(div).click(handler7);\n" + + " $(div).click(handler8);\n" + + " $(div).keydown(handler9);\n" + + "}" + } + ] + }, + + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":34", + attributes: [ + "jQuery" + ], + handler: "var handler7 = function divClick1() {\n" + + " alert(7);\n" + + "}" + }, + { + type: "click", + filename: TEST_URL + ":35", + attributes: [ + "jQuery" + ], + handler: "var handler8 = function divClick2() {\n" + + " alert(8);\n" + + "}" + }, + { + type: "click", + filename: URL_ROOT + TEST_LIB + ":3", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "k = r.handle = function(a) {\n" + + " return typeof m === K || a && m.event.triggered === a.type ? void 0 : m.event.dispatch.apply(k.elem, arguments)\n" + + "}" + }, + { + type: "keydown", + filename: TEST_URL + ":36", + attributes: [ + "jQuery" + ], + handler: "var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + "}" + }, + { + type: "keydown", + filename: URL_ROOT + TEST_LIB + ":3", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "k = r.handle = function(a) {\n" + + " return typeof m === K || a && m.event.triggered === a.type ? void 0 : m.event.dispatch.apply(k.elem, arguments)\n" + + "}" + } + ] + }, + + { + selector: "#livediv", + expected: [ + { + type: "dragend", + filename: TEST_URL + ":31", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler4 = function liveDivDragEnd() {\n" + + " alert(4);\n" + + "}" + }, + { + type: "dragleave", + filename: TEST_URL + ":30", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler3 = function liveDivDragLeave() {\n" + + " alert(3);\n" + + "}" + }, + { + type: "dragover", + filename: TEST_URL + ":33", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler6 = function liveDivDragOver() {\n" + + " alert(6);\n" + + "}" + }, + { + type: "drop", + filename: TEST_URL + ":32", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler5 = function liveDivDrop() {\n" + + " alert(5);\n" + + "}" + } + ] + }, +]; +/*eslint-enable */ + +add_task(function* () { + yield runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.2.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.2.js new file mode 100644 index 000000000..c26a14d66 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.2.js @@ -0,0 +1,191 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 1.2). + +const TEST_LIB = "lib_jquery_1.2_min.js"; +const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "load", + filename: TEST_URL + ":27", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "() => {\n" + + " var handler1 = function liveDivDblClick() {\n" + + " alert(1);\n" + + " };\n" + + " var handler2 = function liveDivDragStart() {\n" + + " alert(2);\n" + + " };\n" + + " var handler3 = function liveDivDragLeave() {\n" + + " alert(3);\n" + + " };\n" + + " var handler4 = function liveDivDragEnd() {\n" + + " alert(4);\n" + + " };\n" + + " var handler5 = function liveDivDrop() {\n" + + " alert(5);\n" + + " };\n" + + " var handler6 = function liveDivDragOver() {\n" + + " alert(6);\n" + + " };\n" + + " var handler7 = function divClick1() {\n" + + " alert(7);\n" + + " };\n" + + " var handler8 = function divClick2() {\n" + + " alert(8);\n" + + " };\n" + + " var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + " };\n" + + " var handler10 = function divDragOut() {\n" + + " alert(10);\n" + + " };\n" + + "\n" + + " if ($(\"#livediv\").live) {\n" + + " $(\"#livediv\").live(\"dblclick\", handler1);\n" + + " $(\"#livediv\").live(\"dragstart\", handler2);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").delegate) {\n" + + " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" + + " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").on) {\n" + + " $(document).on(\"drop\", \"#livediv\", handler5);\n" + + " $(document).on(\"dragover\", \"#livediv\", handler6);\n" + + " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" + + " }\n" + + "\n" + + " var div = $(\"div\")[0];\n" + + " $(div).click(handler7);\n" + + " $(div).click(handler8);\n" + + " $(div).keydown(handler9);\n" + + "}" + }, + { + type: "load", + filename: URL_ROOT + TEST_LIB, + attributes: [ + "Bubbling", + "DOM0" + ], + handler: "handle: function(event) {\n" + + " if (typeof jQuery == \"undefined\") return false;\n" + + "\n" + + " // Empty object is for triggered events with no data\n" + + " event = jQuery.event.fix(event || window.event || {});\n" + + "\n" + + " // returned undefined or false\n" + + " var returnValue;\n" + + "\n" + + " var c = this.events[event.type];\n" + + "\n" + + " var args = [].slice.call(arguments, 1);\n" + + " args.unshift(event);\n" + + "\n" + + " for (var j in c) {\n" + + " // Pass in a reference to the handler function itself\n" + + " // So that we can later remove it\n" + + " args[0].handler = c[j];\n" + + " args[0].data = c[j].data;\n" + + "\n" + + " if (c[j].apply(this, args) === false) {\n" + + " event.preventDefault();\n" + + " event.stopPropagation();\n" + + " returnValue = false;\n" + + " }\n" + + " }\n" + + "\n" + + " // Clean up added properties in IE to prevent memory leak\n" + + " if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;\n" + + "\n" + + " return returnValue;\n" + + "}" + } + ] + }, + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":34", + attributes: [ + "jQuery" + ], + handler: "var handler7 = function divClick1() {\n" + + " alert(7);\n" + + "}" + }, + { + type: "click", + filename: TEST_URL + ":35", + attributes: [ + "jQuery" + ], + handler: "var handler8 = function divClick2() {\n" + + " alert(8);\n" + + "}" + }, + { + type: "click", + filename: URL_ROOT + TEST_LIB + ":24", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "function() {\n" + + " var val;\n" + + " if (typeof jQuery == \"undefined\" || jQuery.event.triggered) return val;\n" + + " val = jQuery.event.handle.apply(element, arguments);\n" + + " return val;\n" + + "}" + }, + { + type: "keydown", + filename: TEST_URL + ":36", + attributes: [ + "jQuery" + ], + handler: "var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + "}" + }, + { + type: "keydown", + filename: URL_ROOT + TEST_LIB + ":24", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "function() {\n" + + " var val;\n" + + " if (typeof jQuery == \"undefined\" || jQuery.event.triggered) return val;\n" + + " val = jQuery.event.handle.apply(element, arguments);\n" + + " return val;\n" + + "}" + } + ] + }, +]; +/*eslint-enable */ + +add_task(function* () { + yield runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.3.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.3.js new file mode 100644 index 000000000..e0bdab2fd --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.3.js @@ -0,0 +1,224 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 1.3). + +const TEST_LIB = "lib_jquery_1.3_min.js"; +const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "load", + filename: URL_ROOT + TEST_LIB + ":19", + attributes: [ + "jQuery" + ], + handler: "ready: function() {\n" + + " if (!n.isReady) {\n" + + " n.isReady = true;\n" + + " if (n.readyList) {\n" + + " n.each(n.readyList, function() {\n" + + " this.call(document, n)\n" + + " });\n" + + " n.readyList = null\n" + + " }\n" + + " n(document).triggerHandler(\"ready\")\n" + + " }\n" + + "}" + }, + { + type: "load", + filename: TEST_URL + ":27", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "() => {\n" + + " var handler1 = function liveDivDblClick() {\n" + + " alert(1);\n" + + " };\n" + + " var handler2 = function liveDivDragStart() {\n" + + " alert(2);\n" + + " };\n" + + " var handler3 = function liveDivDragLeave() {\n" + + " alert(3);\n" + + " };\n" + + " var handler4 = function liveDivDragEnd() {\n" + + " alert(4);\n" + + " };\n" + + " var handler5 = function liveDivDrop() {\n" + + " alert(5);\n" + + " };\n" + + " var handler6 = function liveDivDragOver() {\n" + + " alert(6);\n" + + " };\n" + + " var handler7 = function divClick1() {\n" + + " alert(7);\n" + + " };\n" + + " var handler8 = function divClick2() {\n" + + " alert(8);\n" + + " };\n" + + " var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + " };\n" + + " var handler10 = function divDragOut() {\n" + + " alert(10);\n" + + " };\n" + + "\n" + + " if ($(\"#livediv\").live) {\n" + + " $(\"#livediv\").live(\"dblclick\", handler1);\n" + + " $(\"#livediv\").live(\"dragstart\", handler2);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").delegate) {\n" + + " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" + + " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").on) {\n" + + " $(document).on(\"drop\", \"#livediv\", handler5);\n" + + " $(document).on(\"dragover\", \"#livediv\", handler6);\n" + + " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" + + " }\n" + + "\n" + + " var div = $(\"div\")[0];\n" + + " $(div).click(handler7);\n" + + " $(div).click(handler8);\n" + + " $(div).keydown(handler9);\n" + + "}" + }, + { + type: "load", + filename: URL_ROOT + TEST_LIB + ":19", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "function() {\n" + + " return typeof n !== \"undefined\" && !n.event.triggered ? n.event.handle.apply(arguments.callee.elem, arguments) : g\n" + + "}" + }, + { + type: "unload", + filename: URL_ROOT + TEST_LIB + ":19", + attributes: [ + "jQuery" + ], + handler: "function(H) {\n" + + " n(this).unbind(H, D);\n" + + " return (E || G).apply(this, arguments)\n" + + "}" + }, + { + type: "unload", + filename: URL_ROOT + TEST_LIB + ":19", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "function() {\n" + + " return typeof n !== \"undefined\" && !n.event.triggered ? n.event.handle.apply(arguments.callee.elem, arguments) : g\n" + + "}" + } + ] + }, + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":34", + attributes: [ + "jQuery" + ], + handler: "var handler7 = function divClick1() {\n" + + " alert(7);\n" + + "}" + }, + { + type: "click", + filename: TEST_URL + ":35", + attributes: [ + "jQuery" + ], + handler: "var handler8 = function divClick2() {\n" + + " alert(8);\n" + + "}" + }, + { + type: "click", + filename: URL_ROOT + TEST_LIB + ":19", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "function() {\n" + + " return typeof n !== \"undefined\" && !n.event.triggered ? n.event.handle.apply(arguments.callee.elem, arguments) : g\n" + + "}" + }, + { + type: "keydown", + filename: TEST_URL + ":36", + attributes: [ + "jQuery" + ], + handler: "var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + "}" + }, + { + type: "keydown", + filename: URL_ROOT + TEST_LIB + ":19", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "function() {\n" + + " return typeof n !== \"undefined\" && !n.event.triggered ? n.event.handle.apply(arguments.callee.elem, arguments) : g\n" + + "}" + } + ] + }, + { + selector: "#livediv", + expected: [ + { + type: "dblclick", + filename: TEST_URL + ":28", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler1 = function liveDivDblClick() {\n" + + " alert(1);\n" + + "}" + }, + { + type: "dragstart", + filename: TEST_URL + ":29", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler2 = function liveDivDragStart() {\n" + + " alert(2);\n" + + "}" + } + ] + }, +]; +/*eslint-enable */ + +add_task(function* () { + yield runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.4.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.4.js new file mode 100644 index 000000000..9f7d9e241 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.4.js @@ -0,0 +1,287 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 1.4). + +const TEST_LIB = "lib_jquery_1.4_min.js"; +const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "load", + filename: TEST_URL + ":27", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "() => {\n" + + " var handler1 = function liveDivDblClick() {\n" + + " alert(1);\n" + + " };\n" + + " var handler2 = function liveDivDragStart() {\n" + + " alert(2);\n" + + " };\n" + + " var handler3 = function liveDivDragLeave() {\n" + + " alert(3);\n" + + " };\n" + + " var handler4 = function liveDivDragEnd() {\n" + + " alert(4);\n" + + " };\n" + + " var handler5 = function liveDivDrop() {\n" + + " alert(5);\n" + + " };\n" + + " var handler6 = function liveDivDragOver() {\n" + + " alert(6);\n" + + " };\n" + + " var handler7 = function divClick1() {\n" + + " alert(7);\n" + + " };\n" + + " var handler8 = function divClick2() {\n" + + " alert(8);\n" + + " };\n" + + " var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + " };\n" + + " var handler10 = function divDragOut() {\n" + + " alert(10);\n" + + " };\n" + + "\n" + + " if ($(\"#livediv\").live) {\n" + + " $(\"#livediv\").live(\"dblclick\", handler1);\n" + + " $(\"#livediv\").live(\"dragstart\", handler2);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").delegate) {\n" + + " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" + + " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").on) {\n" + + " $(document).on(\"drop\", \"#livediv\", handler5);\n" + + " $(document).on(\"dragover\", \"#livediv\", handler6);\n" + + " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" + + " }\n" + + "\n" + + " var div = $(\"div\")[0];\n" + + " $(div).click(handler7);\n" + + " $(div).click(handler8);\n" + + " $(div).keydown(handler9);\n" + + "}" + }, + { + type: "load", + filename: URL_ROOT + TEST_LIB + ":26", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "ready: function() {\n" + + " if (!c.isReady) {\n" + + " if (!s.body) return setTimeout(c.ready, 13);\n" + + " c.isReady = true;\n" + + " if (Q) {\n" + + " for (var a, b = 0; a = Q[b++];) a.call(s, c);\n" + + " Q = null\n" + + " }\n" + + " c.fn.triggerHandler && c(s).triggerHandler(\"ready\")\n" + + " }\n" + + "}" + } + ] + }, + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":34", + attributes: [ + "jQuery" + ], + handler: "var handler7 = function divClick1() {\n" + + " alert(7);\n" + + "}" + }, + { + type: "click", + filename: TEST_URL + ":35", + attributes: [ + "jQuery" + ], + handler: "var handler8 = function divClick2() {\n" + + " alert(8);\n" + + "}" + }, + { + type: "click", + filename: URL_ROOT + TEST_LIB + ":48", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "j = function() {\n" + + " return typeof c !== \"undefined\" && !c.event.triggered ? c.event.handle.apply(j.elem, arguments) : w\n" + + "}" + }, + { + type: "keydown", + filename: TEST_URL + ":36", + attributes: [ + "jQuery" + ], + handler: "var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + "}" + }, + { + type: "keydown", + filename: URL_ROOT + TEST_LIB + ":48", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "j = function() {\n" + + " return typeof c !== \"undefined\" && !c.event.triggered ? c.event.handle.apply(j.elem, arguments) : w\n" + + "}" + } + ] + }, + { + selector: "#livediv", + expected: [ + { + type: "dblclick", + filename: TEST_URL + ":28", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler1 = function liveDivDblClick() {\n" + + " alert(1);\n" + + "}" + }, + { + type: "dblclick", + filename: URL_ROOT + TEST_LIB + ":17", + attributes: [ + "jQuery", + "Live" + ], + handler: "function qa(a) {\n" + + " var b = true,\n" + + " d = [],\n" + + " f = [],\n" + + " e = arguments,\n" + + " i, j, o, p, n, t = c.extend({}, c.data(this, \"events\").live);\n" + + " for (p in t) {\n" + + " j = t[p];\n" + + " if (j.live === a.type || j.altLive && c.inArray(a.type, j.altLive) > -1) {\n" + + " i = j.data;\n" + + " i.beforeFilter && i.beforeFilter[a.type] && !i.beforeFilter[a.type](a) || f.push(j.selector)\n" + + " } else delete t[p]\n" + + " }\n" + + " i = c(a.target).closest(f, a.currentTarget);\n" + + " n = 0;\n" + + " for (l = i.length; n < l; n++)\n" + + " for (p in t) {\n" + + " j = t[p];\n" + + " o = i[n].elem;\n" + + " f = null;\n" + + " if (i[n].selector === j.selector) {\n" + + " if (j.live === \"mouseenter\" || j.live === \"mouseleave\") f = c(a.relatedTarget).closest(j.selector)[0];\n" + + " if (!f || f !== o) d.push({\n" + + " elem: o,\n" + + " fn: j\n" + + " })\n" + + " }\n" + + " }\n" + + " n = 0;\n" + + " for (l = d.length; n < l; n++) {\n" + + " i = d[n];\n" + + " a.currentTarget = i.elem;\n" + + " a.data = i.fn.data;\n" + + " if (i.fn.apply(i.elem, e) === false) {\n" + + " b = false;\n" + + " break\n" + + " }\n" + + " }\n" + + " return b\n" + + "}" + }, + { + type: "dragstart", + filename: TEST_URL + ":29", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler2 = function liveDivDragStart() {\n" + + " alert(2);\n" + + "}" + }, + { + type: "dragstart", + filename: URL_ROOT + TEST_LIB + ":17", + attributes: [ + "jQuery", + "Live" + ], + handler: "function qa(a) {\n" + + " var b = true,\n" + + " d = [],\n" + + " f = [],\n" + + " e = arguments,\n" + + " i, j, o, p, n, t = c.extend({}, c.data(this, \"events\").live);\n" + + " for (p in t) {\n" + + " j = t[p];\n" + + " if (j.live === a.type || j.altLive && c.inArray(a.type, j.altLive) > -1) {\n" + + " i = j.data;\n" + + " i.beforeFilter && i.beforeFilter[a.type] && !i.beforeFilter[a.type](a) || f.push(j.selector)\n" + + " } else delete t[p]\n" + + " }\n" + + " i = c(a.target).closest(f, a.currentTarget);\n" + + " n = 0;\n" + + " for (l = i.length; n < l; n++)\n" + + " for (p in t) {\n" + + " j = t[p];\n" + + " o = i[n].elem;\n" + + " f = null;\n" + + " if (i[n].selector === j.selector) {\n" + + " if (j.live === \"mouseenter\" || j.live === \"mouseleave\") f = c(a.relatedTarget).closest(j.selector)[0];\n" + + " if (!f || f !== o) d.push({\n" + + " elem: o,\n" + + " fn: j\n" + + " })\n" + + " }\n" + + " }\n" + + " n = 0;\n" + + " for (l = d.length; n < l; n++) {\n" + + " i = d[n];\n" + + " a.currentTarget = i.elem;\n" + + " a.data = i.fn.data;\n" + + " if (i.fn.apply(i.elem, e) === false) {\n" + + " b = false;\n" + + " break\n" + + " }\n" + + " }\n" + + " return b\n" + + "}" + } + ] + }, +]; +/*eslint-enable */ + +add_task(function* () { + yield runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.6.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.6.js new file mode 100644 index 000000000..f89bd0740 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.6.js @@ -0,0 +1,388 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +requestLongerTimeout(2); + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 1.6). + +const TEST_LIB = "lib_jquery_1.6_min.js"; +const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "load", + filename: TEST_URL + ":27", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "() => {\n" + + " var handler1 = function liveDivDblClick() {\n" + + " alert(1);\n" + + " };\n" + + " var handler2 = function liveDivDragStart() {\n" + + " alert(2);\n" + + " };\n" + + " var handler3 = function liveDivDragLeave() {\n" + + " alert(3);\n" + + " };\n" + + " var handler4 = function liveDivDragEnd() {\n" + + " alert(4);\n" + + " };\n" + + " var handler5 = function liveDivDrop() {\n" + + " alert(5);\n" + + " };\n" + + " var handler6 = function liveDivDragOver() {\n" + + " alert(6);\n" + + " };\n" + + " var handler7 = function divClick1() {\n" + + " alert(7);\n" + + " };\n" + + " var handler8 = function divClick2() {\n" + + " alert(8);\n" + + " };\n" + + " var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + " };\n" + + " var handler10 = function divDragOut() {\n" + + " alert(10);\n" + + " };\n" + + "\n" + + " if ($(\"#livediv\").live) {\n" + + " $(\"#livediv\").live(\"dblclick\", handler1);\n" + + " $(\"#livediv\").live(\"dragstart\", handler2);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").delegate) {\n" + + " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" + + " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").on) {\n" + + " $(document).on(\"drop\", \"#livediv\", handler5);\n" + + " $(document).on(\"dragover\", \"#livediv\", handler6);\n" + + " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" + + " }\n" + + "\n" + + " var div = $(\"div\")[0];\n" + + " $(div).click(handler7);\n" + + " $(div).click(handler8);\n" + + " $(div).keydown(handler9);\n" + + "}" + }, + { + type: "load", + filename: URL_ROOT + TEST_LIB + ":16", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "ready: function(a) {\n" + + " if (a === !0 && !--e.readyWait || a !== !0 && !e.isReady) {\n" + + " if (!c.body) return setTimeout(e.ready, 1);\n" + + " e.isReady = !0;\n" + + " if (a !== !0 && --e.readyWait > 0) return;\n" + + " y.resolveWith(c, [e]), e.fn.trigger && e(c).trigger(\"ready\").unbind(\"ready\")\n" + + " }\n" + + "}" + } + ] + }, + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":34", + attributes: [ + "jQuery" + ], + handler: "var handler7 = function divClick1() {\n" + + " alert(7);\n" + + "}" + }, + { + type: "click", + filename: TEST_URL + ":35", + attributes: [ + "jQuery" + ], + handler: "var handler8 = function divClick2() {\n" + + " alert(8);\n" + + "}" + }, + { + type: "click", + filename: URL_ROOT + TEST_LIB + ":16", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "i.handle = k = function(a) {\n" + + " return typeof f != \"undefined\" && (!a || f.event.triggered !== a.type) ? f.event.handle.apply(k.elem, arguments) : b\n" + + "}" + }, + { + type: "keydown", + filename: TEST_URL + ":36", + attributes: [ + "jQuery" + ], + handler: "var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + "}" + }, + { + type: "keydown", + filename: URL_ROOT + TEST_LIB + ":16", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "i.handle = k = function(a) {\n" + + " return typeof f != \"undefined\" && (!a || f.event.triggered !== a.type) ? f.event.handle.apply(k.elem, arguments) : b\n" + + "}" + } + ] + }, + { + selector: "#livediv", + expected: [ + { + type: "dblclick", + filename: TEST_URL + ":28", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler1 = function liveDivDblClick() {\n" + + " alert(1);\n" + + "}" + }, + { + type: "dblclick", + filename: URL_ROOT + TEST_LIB + ":16", + attributes: [ + "jQuery", + "Live" + ], + handler: "function M(a) {\n" + + " var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [],\n" + + " q = [],\n" + + " r = f._data(this, \"events\");\n" + + " if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === \"click\")) {\n" + + " a.namespace && (n = new RegExp(\"(^|\\\\.)\" + a.namespace.split(\".\").join(\"\\\\.(?:.*\\\\.)?\") + \"(\\\\.|$)\")), a.liveFired = this;\n" + + " var s = r.live.slice(0);\n" + + " for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, \"\") === a.type ? q.push(g.selector) : s.splice(i--, 1);\n" + + " e = f(a.target).closest(q, a.currentTarget);\n" + + " for (j = 0, k = e.length; j < k; j++) {\n" + + " m = e[j];\n" + + " for (i = 0; i < s.length; i++) {\n" + + " g = s[i];\n" + + " if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) {\n" + + " h = m.elem, d = null;\n" + + " if (g.preType === \"mouseenter\" || g.preType === \"mouseleave\") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h);\n" + + " (!d || d !== h) && p.push({\n" + + " elem: h,\n" + + " handleObj: g,\n" + + " level: m.level\n" + + " })\n" + + " }\n" + + " }\n" + + " }\n" + + " for (j = 0, k = p.length; j < k; j++) {\n" + + " e = p[j];\n" + + " if (c && e.level > c) break;\n" + + " a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments);\n" + + " if (o === !1 || a.isPropagationStopped()) {\n" + + " c = e.level, o === !1 && (b = !1);\n" + + " if (a.isImmediatePropagationStopped()) break\n" + + " }\n" + + " }\n" + + " return b\n" + + " }\n" + + "}" + }, + { + type: "dragend", + filename: TEST_URL + ":31", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler4 = function liveDivDragEnd() {\n" + + " alert(4);\n" + + "}" + }, + { + type: "dragend", + filename: URL_ROOT + TEST_LIB + ":16", + attributes: [ + "jQuery", + "Live" + ], + handler: "function M(a) {\n" + + " var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [],\n" + + " q = [],\n" + + " r = f._data(this, \"events\");\n" + + " if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === \"click\")) {\n" + + " a.namespace && (n = new RegExp(\"(^|\\\\.)\" + a.namespace.split(\".\").join(\"\\\\.(?:.*\\\\.)?\") + \"(\\\\.|$)\")), a.liveFired = this;\n" + + " var s = r.live.slice(0);\n" + + " for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, \"\") === a.type ? q.push(g.selector) : s.splice(i--, 1);\n" + + " e = f(a.target).closest(q, a.currentTarget);\n" + + " for (j = 0, k = e.length; j < k; j++) {\n" + + " m = e[j];\n" + + " for (i = 0; i < s.length; i++) {\n" + + " g = s[i];\n" + + " if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) {\n" + + " h = m.elem, d = null;\n" + + " if (g.preType === \"mouseenter\" || g.preType === \"mouseleave\") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h);\n" + + " (!d || d !== h) && p.push({\n" + + " elem: h,\n" + + " handleObj: g,\n" + + " level: m.level\n" + + " })\n" + + " }\n" + + " }\n" + + " }\n" + + " for (j = 0, k = p.length; j < k; j++) {\n" + + " e = p[j];\n" + + " if (c && e.level > c) break;\n" + + " a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments);\n" + + " if (o === !1 || a.isPropagationStopped()) {\n" + + " c = e.level, o === !1 && (b = !1);\n" + + " if (a.isImmediatePropagationStopped()) break\n" + + " }\n" + + " }\n" + + " return b\n" + + " }\n" + + "}" + }, + { + type: "dragleave", + filename: TEST_URL + ":30", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler3 = function liveDivDragLeave() {\n" + + " alert(3);\n" + + "}" + }, + { + type: "dragleave", + filename: URL_ROOT + TEST_LIB + ":16", + attributes: [ + "jQuery", + "Live" + ], + handler: "function M(a) {\n" + + " var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [],\n" + + " q = [],\n" + + " r = f._data(this, \"events\");\n" + + " if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === \"click\")) {\n" + + " a.namespace && (n = new RegExp(\"(^|\\\\.)\" + a.namespace.split(\".\").join(\"\\\\.(?:.*\\\\.)?\") + \"(\\\\.|$)\")), a.liveFired = this;\n" + + " var s = r.live.slice(0);\n" + + " for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, \"\") === a.type ? q.push(g.selector) : s.splice(i--, 1);\n" + + " e = f(a.target).closest(q, a.currentTarget);\n" + + " for (j = 0, k = e.length; j < k; j++) {\n" + + " m = e[j];\n" + + " for (i = 0; i < s.length; i++) {\n" + + " g = s[i];\n" + + " if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) {\n" + + " h = m.elem, d = null;\n" + + " if (g.preType === \"mouseenter\" || g.preType === \"mouseleave\") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h);\n" + + " (!d || d !== h) && p.push({\n" + + " elem: h,\n" + + " handleObj: g,\n" + + " level: m.level\n" + + " })\n" + + " }\n" + + " }\n" + + " }\n" + + " for (j = 0, k = p.length; j < k; j++) {\n" + + " e = p[j];\n" + + " if (c && e.level > c) break;\n" + + " a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments);\n" + + " if (o === !1 || a.isPropagationStopped()) {\n" + + " c = e.level, o === !1 && (b = !1);\n" + + " if (a.isImmediatePropagationStopped()) break\n" + + " }\n" + + " }\n" + + " return b\n" + + " }\n" + + "}" + }, + { + type: "dragstart", + filename: TEST_URL + ":29", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler2 = function liveDivDragStart() {\n" + + " alert(2);\n" + + "}" + }, + { + type: "dragstart", + filename: URL_ROOT + TEST_LIB + ":16", + attributes: [ + "jQuery", + "Live" + ], + handler: "function M(a) {\n" + + " var b, c, d, e, g, h, i, j, k, l, m, n, o, p = [],\n" + + " q = [],\n" + + " r = f._data(this, \"events\");\n" + + " if (!(a.liveFired === this || !r || !r.live || a.target.disabled || a.button && a.type === \"click\")) {\n" + + " a.namespace && (n = new RegExp(\"(^|\\\\.)\" + a.namespace.split(\".\").join(\"\\\\.(?:.*\\\\.)?\") + \"(\\\\.|$)\")), a.liveFired = this;\n" + + " var s = r.live.slice(0);\n" + + " for (i = 0; i < s.length; i++) g = s[i], g.origType.replace(x, \"\") === a.type ? q.push(g.selector) : s.splice(i--, 1);\n" + + " e = f(a.target).closest(q, a.currentTarget);\n" + + " for (j = 0, k = e.length; j < k; j++) {\n" + + " m = e[j];\n" + + " for (i = 0; i < s.length; i++) {\n" + + " g = s[i];\n" + + " if (m.selector === g.selector && (!n || n.test(g.namespace)) && !m.elem.disabled) {\n" + + " h = m.elem, d = null;\n" + + " if (g.preType === \"mouseenter\" || g.preType === \"mouseleave\") a.type = g.preType, d = f(a.relatedTarget).closest(g.selector)[0], d && f.contains(h, d) && (d = h);\n" + + " (!d || d !== h) && p.push({\n" + + " elem: h,\n" + + " handleObj: g,\n" + + " level: m.level\n" + + " })\n" + + " }\n" + + " }\n" + + " }\n" + + " for (j = 0, k = p.length; j < k; j++) {\n" + + " e = p[j];\n" + + " if (c && e.level > c) break;\n" + + " a.currentTarget = e.elem, a.data = e.handleObj.data, a.handleObj = e.handleObj, o = e.handleObj.origHandler.apply(e.elem, arguments);\n" + + " if (o === !1 || a.isPropagationStopped()) {\n" + + " c = e.level, o === !1 && (b = !1);\n" + + " if (a.isImmediatePropagationStopped()) break\n" + + " }\n" + + " }\n" + + " return b\n" + + " }\n" + + "}" + } + ] + }, +]; +/*eslint-enable */ + +add_task(function* () { + yield runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.7.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.7.js new file mode 100644 index 000000000..39f1d54e2 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_1.7.js @@ -0,0 +1,234 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +requestLongerTimeout(2); + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 1.7). + +const TEST_LIB = "lib_jquery_1.7_min.js"; +const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "load", + filename: TEST_URL + ":27", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "() => {\n" + + " var handler1 = function liveDivDblClick() {\n" + + " alert(1);\n" + + " };\n" + + " var handler2 = function liveDivDragStart() {\n" + + " alert(2);\n" + + " };\n" + + " var handler3 = function liveDivDragLeave() {\n" + + " alert(3);\n" + + " };\n" + + " var handler4 = function liveDivDragEnd() {\n" + + " alert(4);\n" + + " };\n" + + " var handler5 = function liveDivDrop() {\n" + + " alert(5);\n" + + " };\n" + + " var handler6 = function liveDivDragOver() {\n" + + " alert(6);\n" + + " };\n" + + " var handler7 = function divClick1() {\n" + + " alert(7);\n" + + " };\n" + + " var handler8 = function divClick2() {\n" + + " alert(8);\n" + + " };\n" + + " var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + " };\n" + + " var handler10 = function divDragOut() {\n" + + " alert(10);\n" + + " };\n" + + "\n" + + " if ($(\"#livediv\").live) {\n" + + " $(\"#livediv\").live(\"dblclick\", handler1);\n" + + " $(\"#livediv\").live(\"dragstart\", handler2);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").delegate) {\n" + + " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" + + " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").on) {\n" + + " $(document).on(\"drop\", \"#livediv\", handler5);\n" + + " $(document).on(\"dragover\", \"#livediv\", handler6);\n" + + " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" + + " }\n" + + "\n" + + " var div = $(\"div\")[0];\n" + + " $(div).click(handler7);\n" + + " $(div).click(handler8);\n" + + " $(div).keydown(handler9);\n" + + "}" + }, + { + type: "load", + filename: URL_ROOT + TEST_LIB + ":2", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "ready: function(a) {\n" + + " if (a === !0 && !--e.readyWait || a !== !0 && !e.isReady) {\n" + + " if (!c.body) return setTimeout(e.ready, 1);\n" + + " e.isReady = !0;\n" + + " if (a !== !0 && --e.readyWait > 0) return;\n" + + " B.fireWith(c, [e]), e.fn.trigger && e(c).trigger(\"ready\").unbind(\"ready\")\n" + + " }\n" + + "}" + } + ] + }, + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":34", + attributes: [ + "jQuery" + ], + handler: "var handler7 = function divClick1() {\n" + + " alert(7);\n" + + "}" + }, + { + type: "click", + filename: TEST_URL + ":35", + attributes: [ + "jQuery" + ], + handler: "var handler8 = function divClick2() {\n" + + " alert(8);\n" + + "}" + }, + { + type: "click", + filename: URL_ROOT + TEST_LIB + ":3", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "h.handle = i = function(a) {\n" + + " return typeof f != \"undefined\" && (!a || f.event.triggered !== a.type) ? f.event.dispatch.apply(i.elem, arguments) : b\n" + + "}" + }, + { + type: "keydown", + filename: TEST_URL + ":36", + attributes: [ + "jQuery" + ], + handler: "var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + "}" + }, + { + type: "keydown", + filename: URL_ROOT + TEST_LIB + ":3", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "h.handle = i = function(a) {\n" + + " return typeof f != \"undefined\" && (!a || f.event.triggered !== a.type) ? f.event.dispatch.apply(i.elem, arguments) : b\n" + + "}" + } + ] + }, + { + selector: "#livediv", + expected: [ + { + type: "dblclick", + filename: TEST_URL + ":28", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler1 = function liveDivDblClick() {\n" + + " alert(1);\n" + + "}" + }, + { + type: "dragend", + filename: TEST_URL + ":31", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler4 = function liveDivDragEnd() {\n" + + " alert(4);\n" + + "}" + }, + { + type: "dragleave", + filename: TEST_URL + ":30", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler3 = function liveDivDragLeave() {\n" + + " alert(3);\n" + + "}" + }, + { + type: "dragover", + filename: TEST_URL + ":33", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler6 = function liveDivDragOver() {\n" + + " alert(6);\n" + + "}" + }, + { + type: "dragstart", + filename: TEST_URL + ":29", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler2 = function liveDivDragStart() {\n" + + " alert(2);\n" + + "}" + }, + { + type: "drop", + filename: TEST_URL + ":32", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler5 = function liveDivDrop() {\n" + + " alert(5);\n" + + "}" + } + ] + }, +]; +/*eslint-enable */ + +add_task(function* () { + yield runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_events_jquery_2.1.1.js b/devtools/client/inspector/markup/test/browser_markup_events_jquery_2.1.1.js new file mode 100644 index 000000000..c6a6642ea --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_events_jquery_2.1.1.js @@ -0,0 +1,196 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_events_test_runner.js */ +"use strict"; + +requestLongerTimeout(2); + +// Test that markup view event bubbles show the correct event info for jQuery +// and jQuery Live events (jQuery version 2.1.1). + +const TEST_LIB = "lib_jquery_2.1.1_min.js"; +const TEST_URL = URL_ROOT + "doc_markup_events_jquery.html?" + TEST_LIB; + +loadHelperScript("helper_events_test_runner.js"); + +/*eslint-disable */ +const TEST_DATA = [ + { + selector: "html", + expected: [ + { + type: "load", + filename: TEST_URL + ":27", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "() => {\n" + + " var handler1 = function liveDivDblClick() {\n" + + " alert(1);\n" + + " };\n" + + " var handler2 = function liveDivDragStart() {\n" + + " alert(2);\n" + + " };\n" + + " var handler3 = function liveDivDragLeave() {\n" + + " alert(3);\n" + + " };\n" + + " var handler4 = function liveDivDragEnd() {\n" + + " alert(4);\n" + + " };\n" + + " var handler5 = function liveDivDrop() {\n" + + " alert(5);\n" + + " };\n" + + " var handler6 = function liveDivDragOver() {\n" + + " alert(6);\n" + + " };\n" + + " var handler7 = function divClick1() {\n" + + " alert(7);\n" + + " };\n" + + " var handler8 = function divClick2() {\n" + + " alert(8);\n" + + " };\n" + + " var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + " };\n" + + " var handler10 = function divDragOut() {\n" + + " alert(10);\n" + + " };\n" + + "\n" + + " if ($(\"#livediv\").live) {\n" + + " $(\"#livediv\").live(\"dblclick\", handler1);\n" + + " $(\"#livediv\").live(\"dragstart\", handler2);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").delegate) {\n" + + " $(document).delegate(\"#livediv\", \"dragleave\", handler3);\n" + + " $(document).delegate(\"#livediv\", \"dragend\", handler4);\n" + + " }\n" + + "\n" + + " if ($(\"#livediv\").on) {\n" + + " $(document).on(\"drop\", \"#livediv\", handler5);\n" + + " $(document).on(\"dragover\", \"#livediv\", handler6);\n" + + " $(document).on(\"dragout\", \"#livediv:xxxxx\", handler10);\n" + + " }\n" + + "\n" + + " var div = $(\"div\")[0];\n" + + " $(div).click(handler7);\n" + + " $(div).click(handler8);\n" + + " $(div).keydown(handler9);\n" + + "}" + } + ] + }, + { + selector: "#testdiv", + expected: [ + { + type: "click", + filename: TEST_URL + ":34", + attributes: [ + "jQuery" + ], + handler: "var handler7 = function divClick1() {\n" + + " alert(7);\n" + + "}" + }, + { + type: "click", + filename: TEST_URL + ":35", + attributes: [ + "jQuery" + ], + handler: "var handler8 = function divClick2() {\n" + + " alert(8);\n" + + "}" + }, + { + type: "click", + filename: URL_ROOT + TEST_LIB + ":3", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "g = r.handle = function(b) {\n" + + " return typeof n !== U && n.event.triggered !== b.type ? n.event.dispatch.apply(a, arguments) : void 0\n" + + "}" + }, + { + type: "keydown", + filename: TEST_URL + ":36", + attributes: [ + "jQuery" + ], + handler: "var handler9 = function divKeyDown() {\n" + + " alert(9);\n" + + "}" + }, + { + type: "keydown", + filename: URL_ROOT + TEST_LIB + ":3", + attributes: [ + "Bubbling", + "DOM2" + ], + handler: "g = r.handle = function(b) {\n" + + " return typeof n !== U && n.event.triggered !== b.type ? n.event.dispatch.apply(a, arguments) : void 0\n" + + "}" + } + ] + }, + { + selector: "#livediv", + expected: [ + { + type: "dragend", + filename: TEST_URL + ":31", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler4 = function liveDivDragEnd() {\n" + + " alert(4);\n" + + "}" + }, + { + type: "dragleave", + filename: TEST_URL + ":30", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler3 = function liveDivDragLeave() {\n" + + " alert(3);\n" + + "}" + }, + { + type: "dragover", + filename: TEST_URL + ":33", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler6 = function liveDivDragOver() {\n" + + " alert(6);\n" + + "}" + }, + { + type: "drop", + filename: TEST_URL + ":32", + attributes: [ + "jQuery", + "Live" + ], + handler: "var handler5 = function liveDivDrop() {\n" + + " alert(5);\n" + + "}" + } + ] + }, +]; +/*eslint-enable */ + +add_task(function* () { + yield runEventPopupTests(TEST_URL, TEST_DATA); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_html_edit_01.js b/devtools/client/inspector/markup/test/browser_markup_html_edit_01.js new file mode 100644 index 000000000..e4c271498 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_html_edit_01.js @@ -0,0 +1,84 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_outerhtml_test_runner.js */ +"use strict"; + +// Test outerHTML edition via the markup-view + +requestLongerTimeout(2); + +loadHelperScript("helper_outerhtml_test_runner.js"); + +const TEST_DATA = [{ + selector: "#one", + oldHTML: '<div id="one">First <em>Div</em></div>', + newHTML: '<div id="one">First Div</div>', + validate: function* ({pageNodeFront, selectedNodeFront, testActor}) { + let text = yield testActor.getProperty("#one", "textContent"); + is(text, "First Div", "New div has expected text content"); + let num = yield testActor.getNumberOfElementMatches("#one em"); + is(num, 0, "No em remaining"); + } +}, { + selector: "#removedChildren", + oldHTML: "<div id=\"removedChildren\">removedChild " + + "<i>Italic <b>Bold <u>Underline</u></b></i> Normal</div>", + newHTML: "<div id=\"removedChildren\">removedChild</div>" +}, { + selector: "#addedChildren", + oldHTML: '<div id="addedChildren">addedChildren</div>', + newHTML: "<div id=\"addedChildren\">addedChildren " + + "<i>Italic <b>Bold <u>Underline</u></b></i> Normal</div>" +}, { + selector: "#addedAttribute", + oldHTML: '<div id="addedAttribute">addedAttribute</div>', + newHTML: "<div id=\"addedAttribute\" class=\"important\" disabled checked>" + + "addedAttribute</div>", + validate: function* ({pageNodeFront, selectedNodeFront, testActor}) { + is(pageNodeFront, selectedNodeFront, "Original element is selected"); + let html = yield testActor.getProperty("#addedAttribute", "outerHTML"); + is(html, "<div id=\"addedAttribute\" class=\"important\" disabled=\"\" " + + "checked=\"\">addedAttribute</div>", "Attributes have been added"); + } +}, { + selector: "#changedTag", + oldHTML: '<div id="changedTag">changedTag</div>', + newHTML: '<p id="changedTag" class="important">changedTag</p>' +}, { + selector: "#siblings", + oldHTML: '<div id="siblings">siblings</div>', + newHTML: '<div id="siblings-before-sibling">before sibling</div>' + + '<div id="siblings">siblings (updated)</div>' + + '<div id="siblings-after-sibling">after sibling</div>', + validate: function* ({selectedNodeFront, inspector, testActor}) { + let beforeSiblingFront = yield getNodeFront("#siblings-before-sibling", + inspector); + is(beforeSiblingFront, selectedNodeFront, "Sibling has been selected"); + + let text = yield testActor.getProperty("#siblings", "textContent"); + is(text, "siblings (updated)", "New div has expected text content"); + + let beforeText = yield testActor.getProperty("#siblings-before-sibling", + "textContent"); + is(beforeText, "before sibling", "Sibling has been inserted"); + + let afterText = yield testActor.getProperty("#siblings-after-sibling", + "textContent"); + is(afterText, "after sibling", "Sibling has been inserted"); + } +}]; + +const TEST_URL = "data:text/html," + + "<!DOCTYPE html>" + + "<head><meta charset='utf-8' /></head>" + + "<body>" + + TEST_DATA.map(outer => outer.oldHTML).join("\n") + + "</body>" + + "</html>"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + inspector.markup._frame.focus(); + yield runEditOuterHTMLTests(TEST_DATA, inspector, testActor); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_html_edit_02.js b/devtools/client/inspector/markup/test/browser_markup_html_edit_02.js new file mode 100644 index 000000000..8f6d0fd14 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_html_edit_02.js @@ -0,0 +1,119 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_outerhtml_test_runner.js */ +"use strict"; + +// Test outerHTML edition via the markup-view + +loadHelperScript("helper_outerhtml_test_runner.js"); +requestLongerTimeout(2); + +const TEST_DATA = [ + { + selector: "#badMarkup1", + oldHTML: "<div id=\"badMarkup1\">badMarkup1</div>", + newHTML: "<div id=\"badMarkup1\">badMarkup1</div> hanging</div>", + validate: function* ({pageNodeFront, selectedNodeFront, testActor}) { + is(pageNodeFront, selectedNodeFront, "Original element is selected"); + + let textNodeName = yield testActor.eval(` + content.document.querySelector("#badMarkup1").nextSibling.nodeName + `); + let textNodeData = yield testActor.eval(` + content.document.querySelector("#badMarkup1").nextSibling.data + `); + is(textNodeName, "#text", "Sibling is a text element"); + is(textNodeData, " hanging", "New text node has expected text content"); + } + }, + { + selector: "#badMarkup2", + oldHTML: "<div id=\"badMarkup2\">badMarkup2</div>", + newHTML: "<div id=\"badMarkup2\">badMarkup2</div> hanging<div></div>" + + "</div></div></body>", + validate: function* ({pageNodeFront, selectedNodeFront, testActor}) { + is(pageNodeFront, selectedNodeFront, "Original element is selected"); + + let textNodeName = yield testActor.eval(` + content.document.querySelector("#badMarkup2").nextSibling.nodeName + `); + let textNodeData = yield testActor.eval(` + content.document.querySelector("#badMarkup2").nextSibling.data + `); + is(textNodeName, "#text", "Sibling is a text element"); + is(textNodeData, " hanging", "New text node has expected text content"); + } + }, + { + selector: "#badMarkup3", + oldHTML: "<div id=\"badMarkup3\">badMarkup3</div>", + newHTML: "<div id=\"badMarkup3\">badMarkup3 <em>Emphasized <strong> " + + "and strong</div>", + validate: function* ({pageNodeFront, selectedNodeFront, testActor}) { + is(pageNodeFront, selectedNodeFront, "Original element is selected"); + + let emText = yield testActor.getProperty("#badMarkup3 em", "textContent"); + let strongText = yield testActor.getProperty("#badMarkup3 strong", + "textContent"); + is(emText, "Emphasized and strong", "<em> was auto created"); + is(strongText, " and strong", "<strong> was auto created"); + } + }, + { + selector: "#badMarkup4", + oldHTML: "<div id=\"badMarkup4\">badMarkup4</div>", + newHTML: "<div id=\"badMarkup4\">badMarkup4</p>", + validate: function* ({pageNodeFront, selectedNodeFront, testActor}) { + is(pageNodeFront, selectedNodeFront, "Original element is selected"); + + let divText = yield testActor.getProperty("#badMarkup4", "textContent"); + let divTag = yield testActor.getProperty("#badMarkup4", "tagName"); + + let pText = yield testActor.getProperty("#badMarkup4 p", "textContent"); + let pTag = yield testActor.getProperty("#badMarkup4 p", "tagName"); + + is(divText, "badMarkup4", "textContent is correct"); + is(divTag, "DIV", "did not change to <p> tag"); + is(pText, "", "The <p> tag has no children"); + is(pTag, "P", "Created an empty <p> tag"); + } + }, + { + selector: "#badMarkup5", + oldHTML: "<p id=\"badMarkup5\">badMarkup5</p>", + newHTML: "<p id=\"badMarkup5\">badMarkup5 <div>with a nested div</div></p>", + validate: function* ({pageNodeFront, selectedNodeFront, testActor}) { + is(pageNodeFront, selectedNodeFront, "Original element is selected"); + + let num = yield testActor.getNumberOfElementMatches("#badMarkup5 div"); + + let pText = yield testActor.getProperty("#badMarkup5", "textContent"); + let pTag = yield testActor.getProperty("#badMarkup5", "tagName"); + + let divText = yield testActor.getProperty("#badMarkup5 ~ div", + "textContent"); + let divTag = yield testActor.getProperty("#badMarkup5 ~ div", "tagName"); + + is(num, 0, "The invalid markup got created as a sibling"); + is(pText, "badMarkup5 ", "The p tag does not take in the div content"); + is(pTag, "P", "Did not change to a <div> tag"); + is(divText, "with a nested div", "textContent is correct"); + is(divTag, "DIV", "Did not change to <p> tag"); + } + } +]; + +const TEST_URL = "data:text/html," + + "<!DOCTYPE html>" + + "<head><meta charset='utf-8' /></head>" + + "<body>" + + TEST_DATA.map(outer => outer.oldHTML).join("\n") + + "</body>" + + "</html>"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + inspector.markup._frame.focus(); + yield runEditOuterHTMLTests(TEST_DATA, inspector, testActor); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_html_edit_03.js b/devtools/client/inspector/markup/test/browser_markup_html_edit_03.js new file mode 100644 index 000000000..d72bd1f1d --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_html_edit_03.js @@ -0,0 +1,200 @@ +/* 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 outerHTML editing keybindings work as expected and that *special* +// elements like <html>, <body> and <head> can be edited correctly. + +const TEST_URL = "data:text/html," + + "<!DOCTYPE html>" + + "<head><meta charset='utf-8' /></head>" + + "<body>" + + "<div id=\"keyboard\"></div>" + + "</body>" + + "</html>"; +const SELECTOR = "#keyboard"; +const OLD_HTML = '<div id="keyboard"></div>'; +const NEW_HTML = '<div id="keyboard">Edited</div>'; + +requestLongerTimeout(2); + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + inspector.markup._frame.focus(); + + info("Check that pressing escape cancels edits"); + yield testEscapeCancels(inspector, testActor); + + info("Check that pressing F2 commits edits"); + yield testF2Commits(inspector, testActor); + + info("Check that editing the <body> element works like other nodes"); + yield testBody(inspector, testActor); + + info("Check that editing the <head> element works like other nodes"); + yield testHead(inspector, testActor); + + info("Check that editing the <html> element works like other nodes"); + yield testDocumentElement(inspector, testActor); + + info("Check (again) that editing the <html> element works like other nodes"); + yield testDocumentElement2(inspector, testActor); +}); + +function* testEscapeCancels(inspector, testActor) { + yield selectNode(SELECTOR, inspector); + + let onEditorShown = once(inspector.markup.htmlEditor, "popupshown"); + EventUtils.sendKey("F2", inspector.markup._frame.contentWindow); + yield onEditorShown; + ok(inspector.markup.htmlEditor._visible, "HTML Editor is visible"); + + is((yield testActor.getProperty(SELECTOR, "outerHTML")), OLD_HTML, + "The node is starting with old HTML."); + + inspector.markup.htmlEditor.editor.setText(NEW_HTML); + + let onEditorHiddem = once(inspector.markup.htmlEditor, "popuphidden"); + EventUtils.sendKey("ESCAPE", inspector.markup.htmlEditor.doc.defaultView); + yield onEditorHiddem; + ok(!inspector.markup.htmlEditor._visible, "HTML Editor is not visible"); + + is((yield testActor.getProperty(SELECTOR, "outerHTML")), OLD_HTML, + "Escape cancels edits"); +} + +function* testF2Commits(inspector, testActor) { + let onEditorShown = once(inspector.markup.htmlEditor, "popupshown"); + inspector.markup._frame.contentDocument.documentElement.focus(); + EventUtils.sendKey("F2", inspector.markup._frame.contentWindow); + yield onEditorShown; + ok(inspector.markup.htmlEditor._visible, "HTML Editor is visible"); + + is((yield testActor.getProperty(SELECTOR, "outerHTML")), OLD_HTML, + "The node is starting with old HTML."); + + let onMutations = inspector.once("markupmutation"); + inspector.markup.htmlEditor.editor.setText(NEW_HTML); + EventUtils.sendKey("F2", inspector.markup._frame.contentWindow); + yield onMutations; + + ok(!inspector.markup.htmlEditor._visible, "HTML Editor is not visible"); + + is((yield testActor.getProperty(SELECTOR, "outerHTML")), NEW_HTML, + "F2 commits edits - the node has new HTML."); +} + +function* testBody(inspector, testActor) { + let currentBodyHTML = yield testActor.getProperty("body", "outerHTML"); + let bodyHTML = '<body id="updated"><p></p></body>'; + let bodyFront = yield getNodeFront("body", inspector); + + let onUpdated = inspector.once("inspector-updated"); + let onReselected = inspector.markup.once("reselectedonremoved"); + yield inspector.markup.updateNodeOuterHTML(bodyFront, bodyHTML, + currentBodyHTML); + yield onReselected; + yield onUpdated; + + let newBodyHTML = yield testActor.getProperty("body", "outerHTML"); + is(newBodyHTML, bodyHTML, "<body> HTML has been updated"); + + let headsNum = yield testActor.getNumberOfElementMatches("head"); + is(headsNum, 1, "no extra <head>s have been added"); +} + +function* testHead(inspector, testActor) { + yield selectNode("head", inspector); + + let currentHeadHTML = yield testActor.getProperty("head", "outerHTML"); + let headHTML = "<head id=\"updated\"><title>New Title</title>" + + "<script>window.foo=\"bar\";</script></head>"; + let headFront = yield getNodeFront("head", inspector); + + let onUpdated = inspector.once("inspector-updated"); + let onReselected = inspector.markup.once("reselectedonremoved"); + yield inspector.markup.updateNodeOuterHTML(headFront, headHTML, + currentHeadHTML); + yield onReselected; + yield onUpdated; + + is((yield testActor.eval("content.document.title")), "New Title", + "New title has been added"); + is((yield testActor.eval("content.foo")), undefined, + "Script has not been executed"); + is((yield testActor.getProperty("head", "outerHTML")), headHTML, + "<head> HTML has been updated"); + is((yield testActor.getNumberOfElementMatches("body")), 1, + "no extra <body>s have been added"); +} + +function* testDocumentElement(inspector, testActor) { + let currentDocElementOuterHMTL = yield testActor.eval( + "content.document.documentElement.outerHMTL"); + let docElementHTML = "<html id=\"updated\" foo=\"bar\"><head>" + + "<title>Updated from document element</title>" + + "<script>window.foo=\"bar\";</script></head><body>" + + "<p>Hello</p></body></html>"; + let docElementFront = yield inspector.markup.walker.documentElement(); + + let onReselected = inspector.markup.once("reselectedonremoved"); + yield inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML, + currentDocElementOuterHMTL); + yield onReselected; + + is((yield testActor.eval("content.document.title")), + "Updated from document element", "New title has been added"); + is((yield testActor.eval("content.foo")), + undefined, "Script has not been executed"); + is((yield testActor.getAttribute("html", "id")), + "updated", "<html> ID has been updated"); + is((yield testActor.getAttribute("html", "class")), + null, "<html> class has been updated"); + is((yield testActor.getAttribute("html", "foo")), + "bar", "<html> attribute has been updated"); + is((yield testActor.getProperty("html", "outerHTML")), + docElementHTML, "<html> HTML has been updated"); + is((yield testActor.getNumberOfElementMatches("head")), + 1, "no extra <head>s have been added"); + is((yield testActor.getNumberOfElementMatches("body")), + 1, "no extra <body>s have been added"); + is((yield testActor.getProperty("body", "textContent")), + "Hello", "document.body.textContent has been updated"); +} + +function* testDocumentElement2(inspector, testActor) { + let currentDocElementOuterHMTL = yield testActor.eval( + "content.document.documentElement.outerHMTL"); + let docElementHTML = "<html id=\"somethingelse\" class=\"updated\"><head>" + + "<title>Updated again from document element</title>" + + "<script>window.foo=\"bar\";</script></head><body>" + + "<p>Hello again</p></body></html>"; + let docElementFront = yield inspector.markup.walker.documentElement(); + + let onReselected = inspector.markup.once("reselectedonremoved"); + inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML, + currentDocElementOuterHMTL); + yield onReselected; + + is((yield testActor.eval("content.document.title")), + "Updated again from document element", "New title has been added"); + is((yield testActor.eval("content.foo")), + undefined, "Script has not been executed"); + is((yield testActor.getAttribute("html", "id")), + "somethingelse", "<html> ID has been updated"); + is((yield testActor.getAttribute("html", "class")), + "updated", "<html> class has been updated"); + is((yield testActor.getAttribute("html", "foo")), + null, "<html> attribute has been removed"); + is((yield testActor.getProperty("html", "outerHTML")), + docElementHTML, "<html> HTML has been updated"); + is((yield testActor.getNumberOfElementMatches("head")), + 1, "no extra <head>s have been added"); + is((yield testActor.getNumberOfElementMatches("body")), + 1, "no extra <body>s have been added"); + is((yield testActor.getProperty("body", "textContent")), + "Hello again", "document.body.textContent has been updated"); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_image_tooltip.js b/devtools/client/inspector/markup/test/browser_markup_image_tooltip.js new file mode 100644 index 000000000..7b1611acd --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_image_tooltip.js @@ -0,0 +1,60 @@ +/* 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 image preview tooltips are shown on img and canvas tags in the +// markup-view and that the tooltip actually contains an image and shows the +// right dimension label + +const TEST_NODES = [ + {selector: "img.local", size: "192" + " \u00D7 " + "192"}, + {selector: "img.data", size: "64" + " \u00D7 " + "64"}, + {selector: "img.remote", size: "22" + " \u00D7 " + "23"}, + {selector: ".canvas", size: "600" + " \u00D7 " + "600"} +]; + +add_task(function* () { + yield addTab(URL_ROOT + "doc_markup_image_and_canvas_2.html"); + let {inspector} = yield openInspector(); + + info("Selecting the first <img> tag"); + yield selectNode("img", inspector); + + for (let testNode of TEST_NODES) { + let target = yield getImageTooltipTarget(testNode, inspector); + yield assertTooltipShownOn(target, inspector); + checkImageTooltip(testNode, inspector); + } +}); + +function* getImageTooltipTarget({selector}, inspector) { + let nodeFront = yield getNodeFront(selector, inspector); + let isImg = nodeFront.tagName.toLowerCase() === "img"; + + let container = getContainerForNodeFront(nodeFront, inspector); + + let target = container.editor.tag; + if (isImg) { + target = container.editor.getAttributeElement("src").querySelector(".link"); + } + return target; +} + +function* assertTooltipShownOn(element, {markup}) { + info("Is the element a valid hover target"); + let isValid = yield isHoverTooltipTarget(markup.imagePreviewTooltip, element); + ok(isValid, "The element is a valid hover target for the image tooltip"); +} + +function checkImageTooltip({selector, size}, {markup}) { + let panel = markup.imagePreviewTooltip.panel; + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip for [" + selector + "] contains an image"); + + let label = panel.querySelector(".devtools-tooltip-caption"); + is(label.textContent, size, + "Tooltip label for [" + selector + "] displays the right image size"); + + markup.imagePreviewTooltip.hide(); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js b/devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js new file mode 100644 index 000000000..bc1af2c2a --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_image_tooltip_mutations.js @@ -0,0 +1,83 @@ +/* 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 image preview tooltip shows updated content when the image src +// changes. + +/*eslint-disable */ +const INITIAL_SRC = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAADI5JREFUeNrsWwuQFNUVPf1m5z87szv7HWSWj8CigBFMEFZKiQsB1PgJwUAZg1HBpIQsKmokEhNjWUnFVPnDWBT+KolJYbRMoqUVq0yCClpqiX8sCchPWFwVlt2db7+X93pez7zu6Vn2NxsVWh8987p7pu+9555z7+tZjTGGY3kjOMa34w447oBjfKsY7i/UNM3Y8eFSAkD50Plgw03K5P9gvGv7U5ieeR3PszeREiPNX3/0DL4hjslzhm8THh+OITfXk3dhiv4GDtGPVzCaeJmPLYzuu5qJuWfuw2QTlcN1X9pwQU7LhdZ/ZAseD45cOh9hHvDkc/yAF/DNhdb5Mrr3PvBMaAYW8fMSIi2G497IMEK/YutGtAYr6+ej+nxu/NN8Ks3N7AR6HgcLz0Eg1Ljg1UcxZzi5qewIkMYLRweTr2Kzp+nmyXAd5pS3XQDd+N/4h4zgu9FI7brlXf90nMEnuwQxlvv+hosE3TuexmWeysmT4W+WxkMaLzf9Y8ATgjcUn7T9H1gqrpFq8eV1gMn6t16NhngjfoX6q4DUP032Rd4LJgpSLwJ1yzFqBG69eRkah0MVyo0Acfe+yy9AG4nMiYCkeM53KKFXncBLAXqEm+wCqZwaueq7WCmuLTcKSJmj737ol2hurA9eq9VdyiO8yWa3NNyog+SB5CZodSsQq/dfu34tJpYbBaTMzvVddDZu16q5smXf4G8zEvqm4cyaAmJPuTJk3oJWdS4WzcVtfMZbThSQckb/pYfRGgo3zNOqZnEHbJPGK4abaDCQIIsT8V/qTaBqHkLh6LzXH8XZQhbLhYKyyCC/WeHYcNdmvOgfe8skzbWL270/T3wf7tSx/lGCbTu8xlzzmCSWLc5iwmgikcCHi3Mga0Ry913vBFvQwg90l6M4ImWKfsWOp7DSWxmfpPlCFuPFfsNfKrCnPYpQKIRgqBK7D0SxYaNHwkEiJMtl0ReDp3Lc5D3PGoTo/sKngCl7a5chFqvBatKwjBd7WwqIlzB/78NcoUcp5VSgGxm+7b8eqQRGnHMO634epO4S1EZww09/iFg5UmGoESDuznP1xVhTUX1WWHPzjpd25wyH0hRxI3LGM75nxmuNEEUVpAN0XgxmPoKralakbQnWlIMQyVBD/w+3orkq4lvualjKyWwzt4MaxqspQHVhPOWG64bxYuhZXSFGWhipbSDVragOu5Y9eAsmDDUKyBA703vemVhHoueD6e9wAzJK1WfmN0Umk5GGM4kEMZcuIECqgjm0nldAqmbjwtm4VxZH5AvlADP6mx9Eqy9Q0+KqW8Ch+47FaMMYmnNGfY1iPMshoC6qFxme4wQ+0p+ARE6H3+9veWEDWgUhDhUKyFARn4jM5BNxT0XsMg7bfymGK1ov3wtjDfhL4w0HVGUVBEjDaaE+QNdrcNWch1PG4W6xrjBUXECGivg++Cva3JUT4iQUz3V2RsSVaKLwOuDT89A3HdBQoxhNC+fnVm74ual2EG893P6G+PuP4SfiO4cCBWQooL9qCWKNXPbcI37Aa/lnlZxXRt4RFONGwSDCPAHqOuqjWct1QiEMw5mChM5X4K47FyNqcd3aK9AwFH0CGYLoe1ctxk2eWi57rg5JfGp9rzC6ggCdFlAgHBDw5Yxlcg6G8SyHCjMlsgmDD9zhSeHlF+JnAgWDTQUy2NxfdwOao1UVV3pi3+bE97YSbWpLAbn6zefHNQkp1PMpIBwwvslKgIYTKM2nEpNzrGcH3FXTEal0L38kJ4uDQgEZbO4vnI173LXf5NHZaiUxtaCxyZuo/rK6LpUg54yg3zTWRAArvDcRIPZ6BqzrQ1REpmL+DNw32OKIDCb3X1qPVn8wNNMT4w2bvs+q4bAZrqBh2skaL3yyhhIIZ4i6oHkUK0RckcB8GigEyRIH4A6Mgc8fatl0/+BkkQxC9gIT4ljna1rIZW9rEdNbjJcNjsnoYj7LHWCUwpITzEgzRQKZ3XAFHbTzA3hrz8TEUUZxFBhoKpABQt/97p+w0hMZG68I8R6FtlsJT3FELndZntjM+VMnylKYq8GJI3UZaRMpquGSGFVOEfv0YZBMNzz+uvjbfzS6xQERIhlI9FcvQWNdFVb7x1zCb+QNK8vb9NsiifmI5hBgVoOCBC1sb0ab5RomqENxLO3eA1/0NDRU47q2RQNbRCUDIb7lF2CNL3ZGxEV4n08TVvZWYG4pZyV0zUdS45tyCBByOHWiyvZmxFXDCyRo1ge5+Sy0TA+8lWMiP/6O0S32exGV9Jf4fr8azdUR3zL/CZz4MtvzdX5uOYs6NDOmpkuj5Huh+7qUQSYl0ThHzw0YQzcGo6bhzEqoYq5rN3yRiYiG3Vfe2Ybm/qKA9NNZ3nNm4F7/yDkg9AN+U1mHiBcXP8zuDN76jj8hg1QyiWQigalj02BJPhK8I0zxijAjhp5zhlpLUDvS+BCy2HMAvvB4XDgL9/SXC0g/ou/5+6/xLX8w0uJrOIkXfPvyhY0F6gr7M8H0KWFYikcqAXakB+xwD9CdREBLoau7Gz3cAdSIdLFxFtJTCqRChSjnutvhDcREtzjz2Tswtz+yeNRFUeXZXtWux7C1fuoVcbd3J//ipDX3uZZDLGrwweS+UBLL5TDliVBnF8P7H+XI8aRRGsIBJg/Zlslt1+W+D1JWoSyi+kD9jfhs78t7mhZhSl+fLfY1Bdyv3I8V/qpY3B1McgN7ZFT5/vNO0I5DPLLdPBIJA8qc4h2I0QplYfDpJwHT+aj0246r5S8rToG8OjCle8wk4OLvvYGa+Ovr84uo2qBSwJS9G5egoZFLTfiEqWDtbwGfHgKOdPHcS+ai7XDzMPW/FJRLGGcxnBbK4YJC2K+h+T6Bdu5CqHqCWERd3bawb7JI+iJ735+LNaHaprBLLHBm08U3XxShEsdt+f3eTh3v7aC95Dct4RCWL5OZWh/oXBZThxAIxyOXLzBk8aiEWJID8rK3CpPOmeHaGpvCS+7EHv5FujVHUSJPLXvIFeHcNc+9xrB2gws9KZdxuLFax/WLM5gzzSm/lTXF/OdAcapyvjxPqxqHjr2v4ckX2bS2dRBrc5lSdpKjEJ9/9tdwX2WMd53ZQ2IVo3RES+UwVSpCPvYepNx4gmTGDUKIMQ4eduPnD7mx9xOn/KZKOlFbStjONxHTtR+BYAPmnoZ1Zp8wkBRwP/EL3u0F/C2hGl7vpz7vW37T3vP7if8wroKuoh8ribknX9BK5rcF+mo1qKaKyRPJTgTDjbzY8szcuLb3bpH00u35T47j7prRpwDJTxzyG0dHgxPp5bPG8VdkpfPbUg3SgoOo2mwVukb98D5EqpswZTTulCggTk4gpYhv0++wIhCJxr0+Hq1sondis0SE2oxQe3qWXwWyO4DSQg9gJ8Iiw1VFcGqXxet0N9xE4ygIxv/9W6wo9WyROEX/R+eiobYSq2vHTOR631Eiv2lRfh9dvxkumkXh92Qsx8XrAJ+7YGbWuhxOi/U+31NQmzyqNYG8N/3wfo6CRtRHcN01FzkvojohwLu0VVvDa56IS/xcj2b7nN+O+m0jqpE1wMPXZxAN9iCVThtDvH7gmiRGRpU8Lspv1Uhq4wIVdQoyuGSLNYPKUCS8+CzNURbzMmjK3i8u0U793lmuV0ef9nWQ5MGC/DiUqEUSaCtXna9RJEspZS1lrXINK/pcq+SpT50t98QKMq1FRmDfx3vxty102k0PM4ssEnvuz5+G26Ij4yDpz6z9fV8bkyIkqBFkhej0Ib+ZQ34XJK9AfozaiimqIoX3Jp3tiISrcfYpuN2+iFph/02P36PNC9fVcCnp6H9jYouKyfaWufz5Tp9tVxcUniw7IohZv4dZz81/ns67z3AYPrc2n0+Ix2q8k0PWjgBy88XaibnfK9A+5LdDY2Ivhy36fbT8Zv3Lb1U1qLqUxorXEEXIs0mjjrtxoTZWtdvigNs2sgPiujTv6DIZLld6b/V5742JZV3fUsUVFy5gdsNtKWFzUCEVbNepD1MkSMVbsb6SZm7jI3/zODtQKgUMsOw8wDZ63t5xcV1TnaEAxoc6wrqY+Fj+N4DsqOnhOIdicrQSm1MPYCPlIqHn5bbHg8/bj2D3QfZnCX3mpAICDZV8jH5kpbZqTD0W+DxaA74CWzLN2nd14OlL72J38Lf7+TjC7dadZFDoZJQPrtaIKL/G0L6ktptPZVJ8fMqHYPZOKYPMyQGadIJfDvdXwAFiZOTvDBPydf5vk4rWA+RfdhBlaF/yDDBRoMu9pfnSjv/p7DG+HXfAcQcc49v/BBgAcFAO4DmB2GQAAAAASUVORK5CYII="; +/*eslint-enable */ + +const UPDATED_SRC = URL_ROOT + "doc_markup_tooltip.png"; + +const INITIAL_SRC_SIZE = "64" + " \u00D7 " + "64"; +const UPDATED_SRC_SIZE = "22" + " \u00D7 " + "23"; + +add_task(function* () { + let { inspector } = yield openInspectorForURL( + "data:text/html,<p>markup view tooltip test</p><img>"); + + info("Retrieving NodeFront for the <img> element."); + let img = yield getNodeFront("img", inspector); + + info("Selecting the <img> element"); + yield selectNode(img, inspector); + + info("Adding src attribute to the image."); + yield updateImageSrc(img, INITIAL_SRC, inspector); + + let container = getContainerForNodeFront(img, inspector); + ok(container, "Found markup container for the image."); + + let target = container.editor.getAttributeElement("src") + .querySelector(".link"); + ok(target, "Found the src attribute in the markup view."); + + info("Showing tooltip on the src link."); + yield isHoverTooltipTarget(inspector.markup.imagePreviewTooltip, target); + + checkImageTooltip(INITIAL_SRC_SIZE, inspector); + + info("Updating the image src."); + yield updateImageSrc(img, UPDATED_SRC, inspector); + + target = container.editor.getAttributeElement("src").querySelector(".link"); + ok(target, "Found the src attribute in the markup view after mutation."); + + info("Showing tooltip on the src link."); + yield isHoverTooltipTarget(inspector.markup.imagePreviewTooltip, target); + + info("Checking that the new image was shown."); + checkImageTooltip(UPDATED_SRC_SIZE, inspector); +}); + +/** + * Updates the src attribute of the image. Return a Promise. + */ +function updateImageSrc(img, newSrc, inspector) { + let onMutated = inspector.once("markupmutation"); + let onModified = img.modifyAttributes([{ + attributeName: "src", + newValue: newSrc + }]); + + return Promise.all([onMutated, onModified]); +} + +/** + * Checks that the markup view tooltip contains an image element with the given + * size. + */ +function checkImageTooltip(size, {markup}) { + let panel = markup.imagePreviewTooltip.panel; + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + + let label = panel.querySelector(".devtools-tooltip-caption"); + is(label.textContent, size, "Tooltip label displays the right image size"); + + markup.imagePreviewTooltip.hide(); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_01.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_01.js new file mode 100644 index 000000000..58eccc173 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_01.js @@ -0,0 +1,49 @@ +/* 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"; + +requestLongerTimeout(2); + +// Tests tabbing through attributes on a node + +const TEST_URL = "data:text/html;charset=utf8,<div id='test' a b c d e></div>"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + info("Focusing the tag editor of the test element"); + let {editor} = yield focusNode("div", inspector); + editor.tag.focus(); + + info("Pressing tab and expecting to focus the ID attribute, always first"); + EventUtils.sendKey("tab", inspector.panelWin); + checkFocusedAttribute("id"); + + info("Hit enter to turn the attribute to edit mode"); + EventUtils.sendKey("return", inspector.panelWin); + checkFocusedAttribute("id", true); + + // Check the order of the other attributes in the DOM to the check they appear + // correctly in the markup-view + let attributes = (yield getAttributesFromEditor("div", inspector)).slice(1); + + info("Tabbing forward through attributes in edit mode"); + for (let attribute of attributes) { + collapseSelectionAndTab(inspector); + checkFocusedAttribute(attribute, true); + } + + info("Tabbing backward through attributes in edit mode"); + + // Just reverse the attributes other than id and remove the first one since + // it's already focused now. + let reverseAttributes = attributes.reverse(); + reverseAttributes.shift(); + + for (let attribute of reverseAttributes) { + collapseSelectionAndShiftTab(inspector); + checkFocusedAttribute(attribute, true); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_02.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_02.js new file mode 100644 index 000000000..0e4b8a802 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_02.js @@ -0,0 +1,32 @@ +/* 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"; + +// Tests that pressing ESC when a node in the markup-view is focused toggles +// the split-console (see bug 988278) + +const TEST_URL = "data:text/html;charset=utf8,<div></div>"; + +add_task(function* () { + let {inspector, toolbox} = yield openInspectorForURL(TEST_URL); + + info("Focusing the tag editor of the test element"); + let {editor} = yield getContainerForSelector("div", inspector); + editor.tag.focus(); + + info("Pressing ESC and wait for the split-console to open"); + let onSplitConsole = toolbox.once("split-console"); + let onConsoleReady = toolbox.once("webconsole-ready"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, inspector.panelWin); + yield onSplitConsole; + yield onConsoleReady; + ok(toolbox.splitConsole, "The split console is shown."); + + info("Pressing ESC again and wait for the split-console to close"); + onSplitConsole = toolbox.once("split-console"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, inspector.panelWin); + yield onSplitConsole; + ok(!toolbox.splitConsole, "The split console is hidden."); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_03.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_03.js new file mode 100644 index 000000000..1a94c9270 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_03.js @@ -0,0 +1,50 @@ +/* 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"; + +// Tests that selecting a node with the mouse (by clicking on the line) focuses +// the first focusable element in the corresponding MarkupContainer so that the +// keyboard can be used immediately. + +const TEST_URL = `data:text/html;charset=utf8, + <div class='test-class'></div>Text node`; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + let {walker} = inspector; + + info("Select the test node to have the 2 test containers visible"); + yield selectNode("div", inspector); + + let divFront = yield walker.querySelector(walker.rootNode, "div"); + let textFront = yield walker.nextSibling(divFront); + + info("Click on the MarkupContainer element for the text node"); + yield clickContainer(textFront, inspector); + is(inspector.markup.doc.activeElement, + getContainerForNodeFront(textFront, inspector).editor.value, + "The currently focused element is the node's text content"); + + info("Click on the MarkupContainer element for the <div> node"); + yield clickContainer(divFront, inspector); + is(inspector.markup.doc.activeElement, + getContainerForNodeFront(divFront, inspector).editor.tag, + "The currently focused element is the div's tagname"); + + info("Click on the test-class attribute, to make sure it gets focused"); + let editor = getContainerForNodeFront(divFront, inspector).editor; + let attributeEditor = editor.attrElements.get("class") + .querySelector(".editable"); + + let onFocus = once(attributeEditor, "focus"); + EventUtils.synthesizeMouseAtCenter(attributeEditor, {type: "mousedown"}, + inspector.markup.doc.defaultView); + EventUtils.synthesizeMouseAtCenter(attributeEditor, {type: "mouseup"}, + inspector.markup.doc.defaultView); + yield onFocus; + + is(inspector.markup.doc.activeElement, attributeEditor, + "The currently focused element is the div's class attribute"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_04.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_04.js new file mode 100644 index 000000000..3b6f8bfb3 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_04.js @@ -0,0 +1,58 @@ +/* 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"; + +requestLongerTimeout(2); + +// Tests that selecting a node using the browser context menu (inspect element) +// or the element picker focuses that node so that the keyboard can be used +// immediately. + +const TEST_URL = "data:text/html;charset=utf8,<div>test element</div>"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + info("Select the test node with the browser ctx menu"); + yield clickOnInspectMenuItem(testActor, "div"); + assertNodeSelected(inspector, "div"); + + info("Press arrowUp to focus <body> " + + "(which works if the node was focused properly)"); + yield selectPreviousNodeWithArrowUp(inspector); + assertNodeSelected(inspector, "body"); + + info("Select the test node with the element picker"); + yield selectWithElementPicker(inspector, testActor); + assertNodeSelected(inspector, "div"); + + info("Press arrowUp to focus <body> " + + "(which works if the node was focused properly)"); + yield selectPreviousNodeWithArrowUp(inspector); + assertNodeSelected(inspector, "body"); +}); + +function assertNodeSelected(inspector, tagName) { + is(inspector.selection.nodeFront.tagName.toLowerCase(), tagName, + `The <${tagName}> node is selected`); +} + +function selectPreviousNodeWithArrowUp(inspector) { + let onNodeHighlighted = inspector.toolbox.once("node-highlight"); + let onUpdated = inspector.once("inspector-updated"); + EventUtils.synthesizeKey("VK_UP", {}); + return Promise.all([onUpdated, onNodeHighlighted]); +} + +function* selectWithElementPicker(inspector, testActor) { + yield startPicker(inspector.toolbox); + + yield BrowserTestUtils.synthesizeMouseAtCenter("div", { + type: "mousemove", + }, gBrowser.selectedBrowser); + + yield testActor.synthesizeKey({key: "VK_RETURN", options: {}}); + yield inspector.once("inspector-updated"); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_delete_attributes.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_delete_attributes.js new file mode 100644 index 000000000..a4c121360 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_delete_attributes.js @@ -0,0 +1,63 @@ +/* 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"; + +// Tests that attributes can be deleted from the markup-view with the delete key +// when they are focused. + +const HTML = '<div id="id" class="class" data-id="id"></div>'; +const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML); + +// List of all the test cases. Each item is an object with the following props: +// - selector: the css selector of the node that should be selected +// - attribute: the name of the attribute that should be focused. Do not +// specify an attribute that would make it impossible to find the node using +// selector. +// Note that after each test case, undo is called. +const TEST_DATA = [{ + selector: "#id", + attribute: "class" +}, { + selector: "#id", + attribute: "data-id" +}]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + let {walker} = inspector; + + for (let {selector, attribute} of TEST_DATA) { + info("Get the container for node " + selector); + let {editor} = yield getContainerForSelector(selector, inspector); + + info("Focus attribute " + attribute); + let attr = editor.attrElements.get(attribute).querySelector(".editable"); + attr.focus(); + + info("Delete the attribute by pressing delete"); + let mutated = inspector.once("markupmutation"); + EventUtils.sendKey("delete", inspector.panelWin); + yield mutated; + + info("Check that the node is still here"); + let node = yield walker.querySelector(walker.rootNode, selector); + ok(node, "The node hasn't been deleted"); + + info("Check that the attribute has been deleted"); + node = yield walker.querySelector(walker.rootNode, + selector + "[" + attribute + "]"); + ok(!node, "The attribute does not exist anymore in the DOM"); + ok(!editor.attrElements.get(attribute), + "The attribute has been removed from the container"); + + info("Undo the change"); + yield undoChange(inspector); + node = yield walker.querySelector(walker.rootNode, + selector + "[" + attribute + "]"); + ok(node, "The attribute is back in the DOM"); + ok(editor.attrElements.get(attribute), + "The attribute is back on the container"); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_keybindings_scrolltonode.js b/devtools/client/inspector/markup/test/browser_markup_keybindings_scrolltonode.js new file mode 100644 index 000000000..7b129fc42 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_scrolltonode.js @@ -0,0 +1,87 @@ +/* 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 the keyboard shortcut "S" used to scroll to the selected node. + +const HTML = + `<div style="width: 300px; height: 3000px; position:relative;"> + <div id="scroll-top" + style="height: 50px; top: 0; position:absolute;"> + TOP</div> + <div id="scroll-bottom" + style="height: 50px; bottom: 0; position:absolute;"> + BOTTOM</div> + </div>`; +const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML); + +add_task(function* () { + let { inspector, testActor } = yield openInspectorForURL(TEST_URL); + + info("Make sure the markup frame has the focus"); + inspector.markup._frame.focus(); + + info("Before test starts, #scroll-top is visible, #scroll-bottom is hidden"); + yield checkElementIsInViewport("#scroll-top", true, testActor); + yield checkElementIsInViewport("#scroll-bottom", false, testActor); + + info("Select the #scroll-bottom node"); + yield selectNode("#scroll-bottom", inspector); + info("Press S to scroll to the bottom node"); + let waitForScroll = testActor.waitForEventOnNode("scroll"); + yield EventUtils.synthesizeKey("S", {}, inspector.panelWin); + yield waitForScroll; + ok(true, "Scroll event received"); + + info("#scroll-top should be scrolled out, #scroll-bottom should be visible"); + yield checkElementIsInViewport("#scroll-top", false, testActor); + yield checkElementIsInViewport("#scroll-bottom", true, testActor); + + info("Select the #scroll-top node"); + yield selectNode("#scroll-top", inspector); + info("Press S to scroll to the top node"); + waitForScroll = testActor.waitForEventOnNode("scroll"); + yield EventUtils.synthesizeKey("S", {}, inspector.panelWin); + yield waitForScroll; + ok(true, "Scroll event received"); + + info("#scroll-top should be visible, #scroll-bottom should be scrolled out"); + yield checkElementIsInViewport("#scroll-top", true, testActor); + yield checkElementIsInViewport("#scroll-bottom", false, testActor); + + info("Select #scroll-bottom node"); + yield selectNode("#scroll-bottom", inspector); + info("Press shift + S, nothing should happen due to the modifier"); + yield EventUtils.synthesizeKey("S", {shiftKey: true}, inspector.panelWin); + + info("Same state, #scroll-top is visible, #scroll-bottom is scrolled out"); + yield checkElementIsInViewport("#scroll-top", true, testActor); + yield checkElementIsInViewport("#scroll-bottom", false, testActor); +}); + +/** + * Verify that the element matching the provided selector is either in or out + * of the viewport, depending on the provided "expected" argument. + * Returns a promise that will resolve when the test has been performed. + * + * @param {String} selector + * css selector for the element to test + * @param {Boolean} expected + * true if the element is expected to be in the viewport, false otherwise + * @param {TestActor} testActor + * current test actor + * @return {Promise} promise + */ +function* checkElementIsInViewport(selector, expected, testActor) { + let isInViewport = yield testActor.eval(` + let node = content.document.querySelector("${selector}"); + let rect = node.getBoundingClientRect(); + rect.bottom >= 0 && rect.right >= 0 && + rect.top <= content.innerHeight && rect.left <= content.innerWidth; + `); + + is(isInViewport, expected, + selector + " in the viewport: expected to be " + expected); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_links_01.js b/devtools/client/inspector/markup/test/browser_markup_links_01.js new file mode 100644 index 000000000..4ef3ba4b9 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_links_01.js @@ -0,0 +1,128 @@ +/* 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"; + +// Tests that links are shown in attributes when the values (or part of the +// values) are URIs or pointers to IDs. + +const TEST_URL = URL_ROOT + "doc_markup_links.html"; + +const TEST_DATA = [{ + selector: "link", + attributes: [{ + attributeName: "href", + links: [{type: "cssresource", value: "style.css"}] + }] +}, { + selector: "link[rel=icon]", + attributes: [{ + attributeName: "href", + links: [{type: "uri", + value: "/media/img/firefox/favicon-196.223e1bcaf067.png"}] + }] +}, { + selector: "form", + attributes: [{ + attributeName: "action", + links: [{type: "uri", value: "/post_message"}] + }] +}, { + selector: "label[for=name]", + attributes: [{ + attributeName: "for", + links: [{type: "idref", value: "name"}] + }] +}, { + selector: "label[for=message]", + attributes: [{ + attributeName: "for", + links: [{type: "idref", value: "message"}] + }] +}, { + selector: "output", + attributes: [{ + attributeName: "form", + links: [{type: "idref", value: "message-form"}] + }, { + attributeName: "for", + links: [ + {type: "idref", value: "name"}, + {type: "idref", value: "message"}, + {type: "idref", value: "invalid"} + ] + }] +}, { + selector: "a", + attributes: [{ + attributeName: "href", + links: [{type: "uri", value: "/go/somewhere/else"}] + }, { + attributeName: "ping", + links: [ + {type: "uri", value: "/analytics?page=pageA"}, + {type: "uri", value: "/analytics?user=test"} + ] + }] +}, { + selector: "li[contextmenu=menu1]", + attributes: [{ + attributeName: "contextmenu", + links: [{type: "idref", value: "menu1"}] + }] +}, { + selector: "li[contextmenu=menu2]", + attributes: [{ + attributeName: "contextmenu", + links: [{type: "idref", value: "menu2"}] + }] +}, { + selector: "li[contextmenu=menu3]", + attributes: [{ + attributeName: "contextmenu", + links: [{type: "idref", value: "menu3"}] + }] +}, { + selector: "video", + attributes: [{ + attributeName: "poster", + links: [{type: "uri", value: "doc_markup_tooltip.png"}] + }, { + attributeName: "src", + links: [{type: "uri", value: "code-rush.mp4"}] + }] +}, { + selector: "script", + attributes: [{ + attributeName: "src", + links: [{type: "jsresource", value: "lib_jquery_1.0.js"}] + }] +}]; + +requestLongerTimeout(2); + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + for (let {selector, attributes} of TEST_DATA) { + info("Testing attributes on node " + selector); + yield selectNode(selector, inspector); + let {editor} = yield getContainerForSelector(selector, inspector); + + for (let {attributeName, links} of attributes) { + info("Testing attribute " + attributeName); + let linkEls = editor.attrElements.get(attributeName) + .querySelectorAll(".link"); + + is(linkEls.length, links.length, "The right number of links were found"); + + for (let i = 0; i < links.length; i++) { + is(linkEls[i].dataset.type, links[i].type, + `Link ${i} has the right type`); + is(linkEls[i].textContent, links[i].value, + `Link ${i} has the right value`); + } + } + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_links_02.js b/devtools/client/inspector/markup/test/browser_markup_links_02.js new file mode 100644 index 000000000..83893281c --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_links_02.js @@ -0,0 +1,38 @@ +/* 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"; + +// Tests that attributes are linkified correctly when attributes are updated +// and created. + +const TEST_URL = URL_ROOT + "doc_markup_links.html"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + info("Adding a contextmenu attribute to the body node"); + yield addNewAttributes("body", "contextmenu=\"menu1\"", inspector); + + info("Checking for links in the new attribute"); + let {editor} = yield getContainerForSelector("body", inspector); + let linkEls = editor.attrElements.get("contextmenu") + .querySelectorAll(".link"); + is(linkEls.length, 1, "There is one link in the contextmenu attribute"); + is(linkEls[0].dataset.type, "idref", "The link has the right type"); + is(linkEls[0].textContent, "menu1", "The link has the right value"); + + info("Editing the contextmenu attribute on the body node"); + let nodeMutated = inspector.once("markupmutation"); + let attr = editor.attrElements.get("contextmenu").querySelector(".editable"); + setEditableFieldValue(attr, "contextmenu=\"menu2\"", inspector); + yield nodeMutated; + + info("Checking for links in the updated attribute"); + ({editor} = yield getContainerForSelector("body", inspector)); + linkEls = editor.attrElements.get("contextmenu").querySelectorAll(".link"); + is(linkEls.length, 1, "There is one link in the contextmenu attribute"); + is(linkEls[0].dataset.type, "idref", "The link has the right type"); + is(linkEls[0].textContent, "menu2", "The link has the right value"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_links_03.js b/devtools/client/inspector/markup/test/browser_markup_links_03.js new file mode 100644 index 000000000..a54ccb498 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_links_03.js @@ -0,0 +1,38 @@ +/* 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"; + +// Tests that links appear correctly in attributes created in content. + +const TEST_URL = URL_ROOT + "doc_markup_links.html"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + info("Adding a contextmenu attribute to the body node via the content"); + let onMutated = inspector.once("markupmutation"); + yield testActor.setAttribute("body", "contextmenu", "menu1"); + yield onMutated; + + info("Checking for links in the new attribute"); + let {editor} = yield getContainerForSelector("body", inspector); + let linkEls = editor.attrElements.get("contextmenu") + .querySelectorAll(".link"); + is(linkEls.length, 1, "There is one link in the contextmenu attribute"); + is(linkEls[0].dataset.type, "idref", "The link has the right type"); + is(linkEls[0].textContent, "menu1", "The link has the right value"); + + info("Editing the contextmenu attribute on the body node"); + onMutated = inspector.once("markupmutation"); + yield testActor.setAttribute("body", "contextmenu", "menu2"); + yield onMutated; + + info("Checking for links in the updated attribute"); + ({editor} = yield getContainerForSelector("body", inspector)); + linkEls = editor.attrElements.get("contextmenu").querySelectorAll(".link"); + is(linkEls.length, 1, "There is one link in the contextmenu attribute"); + is(linkEls[0].dataset.type, "idref", "The link has the right type"); + is(linkEls[0].textContent, "menu2", "The link has the right value"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_links_04.js b/devtools/client/inspector/markup/test/browser_markup_links_04.js new file mode 100644 index 000000000..f21afd8d2 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_links_04.js @@ -0,0 +1,116 @@ +/* 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"; + +// Tests that the contextual menu shows the right items when clicking on a link +// in an attribute. + +const TEST_URL = URL_ROOT + "doc_markup_links.html"; + +const TOOLBOX_L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties"); + +// The test case array contains objects with the following properties: +// - selector: css selector for the node to select in the inspector +// - attributeName: name of the attribute to test +// - popupNodeSelector: css selector for the element inside the attribute +// element to use as the contextual menu anchor +// - isLinkFollowItemVisible: is the follow-link item expected to be displayed +// - isLinkCopyItemVisible: is the copy-link item expected to be displayed +// - linkFollowItemLabel: the expected label of the follow-link item +// - linkCopyItemLabel: the expected label of the copy-link item +const TEST_DATA = [{ + selector: "link", + attributeName: "href", + popupNodeSelector: ".link", + isLinkFollowItemVisible: true, + isLinkCopyItemVisible: true, + linkFollowItemLabel: TOOLBOX_L10N.getStr( + "toolbox.viewCssSourceInStyleEditor.label"), + linkCopyItemLabel: INSPECTOR_L10N.getStr( + "inspector.menu.copyUrlToClipboard.label") +}, { + selector: "link[rel=icon]", + attributeName: "href", + popupNodeSelector: ".link", + isLinkFollowItemVisible: true, + isLinkCopyItemVisible: true, + linkFollowItemLabel: INSPECTOR_L10N.getStr( + "inspector.menu.openUrlInNewTab.label"), + linkCopyItemLabel: INSPECTOR_L10N.getStr( + "inspector.menu.copyUrlToClipboard.label") +}, { + selector: "link", + attributeName: "rel", + popupNodeSelector: ".attr-value", + isLinkFollowItemVisible: false, + isLinkCopyItemVisible: false +}, { + selector: "output", + attributeName: "for", + popupNodeSelector: ".link", + isLinkFollowItemVisible: true, + isLinkCopyItemVisible: false, + linkFollowItemLabel: INSPECTOR_L10N.getFormatStr( + "inspector.menu.selectElement.label", "name") +}, { + selector: "script", + attributeName: "src", + popupNodeSelector: ".link", + isLinkFollowItemVisible: true, + isLinkCopyItemVisible: true, + linkFollowItemLabel: TOOLBOX_L10N.getStr( + "toolbox.viewJsSourceInDebugger.label"), + linkCopyItemLabel: INSPECTOR_L10N.getStr( + "inspector.menu.copyUrlToClipboard.label") +}, { + selector: "p[for]", + attributeName: "for", + popupNodeSelector: ".attr-value", + isLinkFollowItemVisible: false, + isLinkCopyItemVisible: false +}]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + for (let test of TEST_DATA) { + info("Selecting test node " + test.selector); + yield selectNode(test.selector, inspector); + + info("Finding the popupNode to anchor the context-menu to"); + let {editor} = yield getContainerForSelector(test.selector, inspector); + let popupNode = editor.attrElements.get(test.attributeName) + .querySelector(test.popupNodeSelector); + ok(popupNode, "Found the popupNode in attribute " + test.attributeName); + + info("Simulating a context click on the popupNode"); + let allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: popupNode, + }); + + let linkFollow = allMenuItems.find(i => i.id === "node-menu-link-follow"); + let linkCopy = allMenuItems.find(i => i.id === "node-menu-link-copy"); + + // The contextual menu setup is async, because it needs to know if the + // inspector has the resolveRelativeURL method first. So call actorHasMethod + // here too to make sure the first call resolves first and the menu is + // properly setup. + yield inspector.target.actorHasMethod("inspector", "resolveRelativeURL"); + + is(linkFollow.visible, test.isLinkFollowItemVisible, + "The follow-link item display is correct"); + is(linkCopy.visible, test.isLinkCopyItemVisible, + "The copy-link item display is correct"); + + if (test.isLinkFollowItemVisible) { + is(linkFollow.label, test.linkFollowItemLabel, + "the follow-link label is correct"); + } + if (test.isLinkCopyItemVisible) { + is(linkCopy.label, test.linkCopyItemLabel, + "the copy-link label is correct"); + } + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_links_05.js b/devtools/client/inspector/markup/test/browser_markup_links_05.js new file mode 100644 index 000000000..feaf257a8 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_links_05.js @@ -0,0 +1,69 @@ +/* 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"; + +// Tests that the contextual menu items shown when clicking on links in +// attributes actually do the right things. + +const TEST_URL = URL_ROOT + "doc_markup_links.html"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + info("Select a node with a URI attribute"); + yield selectNode("video", inspector); + + info("Set the popupNode to the node that contains the uri"); + let {editor} = yield getContainerForSelector("video", inspector); + openContextMenuAndGetAllItems(inspector, { + target: editor.attrElements.get("poster").querySelector(".link"), + }); + + info("Follow the link and wait for the new tab to open"); + let onTabOpened = once(gBrowser.tabContainer, "TabOpen"); + inspector.onFollowLink(); + let {target: tab} = yield onTabOpened; + yield BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + ok(true, "A new tab opened"); + is(tab.linkedBrowser.currentURI.spec, URL_ROOT + "doc_markup_tooltip.png", + "The URL for the new tab is correct"); + gBrowser.removeTab(tab); + + info("Select a node with a IDREF attribute"); + yield selectNode("label", inspector); + + info("Set the popupNode to the node that contains the ref"); + ({editor} = yield getContainerForSelector("label", inspector)); + openContextMenuAndGetAllItems(inspector, { + target: editor.attrElements.get("for").querySelector(".link"), + }); + + info("Follow the link and wait for the new node to be selected"); + let onSelection = inspector.selection.once("new-node-front"); + inspector.onFollowLink(); + yield onSelection; + + ok(true, "A new node was selected"); + is(inspector.selection.nodeFront.id, "name", "The right node was selected"); + + info("Select a node with an invalid IDREF attribute"); + yield selectNode("output", inspector); + + info("Set the popupNode to the node that contains the ref"); + ({editor} = yield getContainerForSelector("output", inspector)); + openContextMenuAndGetAllItems(inspector, { + target: editor.attrElements.get("for").querySelectorAll(".link")[2], + }); + + info("Try to follow the link and check that no new node were selected"); + let onFailed = inspector.once("idref-attribute-link-failed"); + inspector.onFollowLink(); + yield onFailed; + + ok(true, "The node selection failed"); + is(inspector.selection.nodeFront.tagName.toLowerCase(), "output", + "The <output> node is still selected"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_links_06.js b/devtools/client/inspector/markup/test/browser_markup_links_06.js new file mode 100644 index 000000000..452fa9eca --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_links_06.js @@ -0,0 +1,53 @@ +/* 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"; + +// Tests that the contextual menu items shown when clicking on linked attributes +// for <script> and <link> tags actually open the right tools. + +const TEST_URL = URL_ROOT + "doc_markup_links.html"; + +add_task(function* () { + let {toolbox, inspector} = yield openInspectorForURL(TEST_URL); + + info("Select a node with a cssresource attribute"); + yield selectNode("link", inspector); + + info("Set the popupNode to the node that contains the uri"); + let {editor} = yield getContainerForSelector("link", inspector); + openContextMenuAndGetAllItems(inspector, { + target: editor.attrElements.get("href").querySelector(".link"), + }); + + info("Follow the link and wait for the style-editor to open"); + let onStyleEditorReady = toolbox.once("styleeditor-ready"); + inspector.onFollowLink(); + yield onStyleEditorReady; + + // No real need to test that the editor opened on the right file here as this + // is already tested in /framework/test/browser_toolbox_view_source_* + ok(true, "The style-editor was open"); + + info("Switch back to the inspector"); + yield toolbox.selectTool("inspector"); + + info("Select a node with a jsresource attribute"); + yield selectNode("script", inspector); + + info("Set the popupNode to the node that contains the uri"); + ({editor} = yield getContainerForSelector("script", inspector)); + openContextMenuAndGetAllItems(inspector, { + target: editor.attrElements.get("src").querySelector(".link"), + }); + + info("Follow the link and wait for the debugger to open"); + let onDebuggerReady = toolbox.once("jsdebugger-ready"); + inspector.onFollowLink(); + yield onDebuggerReady; + + // No real need to test that the debugger opened on the right file here as + // this is already tested in /framework/test/browser_toolbox_view_source_* + ok(true, "The debugger was open"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_links_07.js b/devtools/client/inspector/markup/test/browser_markup_links_07.js new file mode 100644 index 000000000..793c1ee90 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_links_07.js @@ -0,0 +1,109 @@ +/* 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"; + +// Tests that a middle-click or meta/ctrl-click on links in attributes actually +// do follows the link. + +const TEST_URL = URL_ROOT + "doc_markup_links.html"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + info("Select a node with a URI attribute"); + yield selectNode("video", inspector); + + info("Find the link element from the markup-view"); + let {editor} = yield getContainerForSelector("video", inspector); + let linkEl = editor.attrElements.get("poster").querySelector(".link"); + + info("Follow the link with middle-click and wait for the new tab to open"); + yield followLinkWaitForTab(linkEl, false, + URL_ROOT + "doc_markup_tooltip.png"); + + info("Follow the link with meta/ctrl-click and wait for the new tab to open"); + yield followLinkWaitForTab(linkEl, true, + URL_ROOT + "doc_markup_tooltip.png"); + + info("Select a node with a IDREF attribute"); + yield selectNode("label", inspector); + + info("Find the link element from the markup-view that contains the ref"); + ({editor} = yield getContainerForSelector("label", inspector)); + linkEl = editor.attrElements.get("for").querySelector(".link"); + + info("Follow link with middle-click, wait for new node to be selected."); + yield followLinkWaitForNewNode(linkEl, false, inspector); + + // We have to re-select the label as the link switched the currently selected + // node. + yield selectNode("label", inspector); + + info("Follow link with ctrl/meta-click, wait for new node to be selected."); + yield followLinkWaitForNewNode(linkEl, true, inspector); + + info("Select a node with an invalid IDREF attribute"); + yield selectNode("output", inspector); + + info("Find the link element from the markup-view that contains the ref"); + ({editor} = yield getContainerForSelector("output", inspector)); + linkEl = editor.attrElements.get("for").querySelectorAll(".link")[2]; + + info("Try to follow link wiith middle-click, check no new node selected"); + yield followLinkNoNewNode(linkEl, false, inspector); + + info("Try to follow link wiith meta/ctrl-click, check no new node selected"); + yield followLinkNoNewNode(linkEl, true, inspector); +}); + +function performMouseDown(linkEl, metactrl) { + let evt = linkEl.ownerDocument.createEvent("MouseEvents"); + + let button = -1; + + if (metactrl) { + info("Performing Meta/Ctrl+Left Click"); + button = 0; + } else { + info("Performing Middle Click"); + button = 1; + } + + evt.initMouseEvent("mousedown", true, true, + linkEl.ownerDocument.defaultView, 1, 0, 0, 0, 0, metactrl, + false, false, metactrl, button, null); + + linkEl.dispatchEvent(evt); +} + +function* followLinkWaitForTab(linkEl, isMetaClick, expectedTabURI) { + let onTabOpened = once(gBrowser.tabContainer, "TabOpen"); + performMouseDown(linkEl, isMetaClick); + let {target} = yield onTabOpened; + yield BrowserTestUtils.browserLoaded(target.linkedBrowser); + ok(true, "A new tab opened"); + is(target.linkedBrowser.currentURI.spec, expectedTabURI, + "The URL for the new tab is correct"); + gBrowser.removeTab(target); +} + +function* followLinkWaitForNewNode(linkEl, isMetaClick, inspector) { + let onSelection = inspector.selection.once("new-node-front"); + performMouseDown(linkEl, isMetaClick); + yield onSelection; + + ok(true, "A new node was selected"); + is(inspector.selection.nodeFront.id, "name", "The right node was selected"); +} + +function* followLinkNoNewNode(linkEl, isMetaClick, inspector) { + let onFailed = inspector.once("idref-attribute-link-failed"); + performMouseDown(linkEl, isMetaClick); + yield onFailed; + + ok(true, "The node selection failed"); + is(inspector.selection.nodeFront.tagName.toLowerCase(), "output", + "The <output> node is still selected"); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_load_01.js b/devtools/client/inspector/markup/test/browser_markup_load_01.js new file mode 100644 index 000000000..9c8f4ed2c --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_load_01.js @@ -0,0 +1,71 @@ +/* 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"; + +// Tests that selecting an element with the 'Inspect Element' context +// menu during a page reload doesn't cause the markup view to become empty. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1036324 + +const server = createTestHTTPServer(); + +// Register a slow image handler so we can simulate a long time between +// a reload and the load event firing. +server.registerContentType("gif", "image/gif"); +server.registerPathHandler("/slow.gif", function (metadata, response) { + info("Image has been requested"); + response.processAsync(); + setTimeout(() => { + info("Image is responding"); + response.finish(); + }, 500); +}); + +// Test page load events. +const TEST_URL = "data:text/html," + + "<!DOCTYPE html>" + + "<head><meta charset='utf-8' /></head>" + + "<body>" + + "<p>Slow script</p>" + + "<img src='http://localhost:" + server.identity.primaryPort + "/slow.gif' /></script>" + + "</body>" + + "</html>"; + +add_task(function* () { + let {inspector, testActor, tab} = yield openInspectorForURL(TEST_URL); + let domContentLoaded = waitForLinkedBrowserEvent(tab, "DOMContentLoaded"); + let pageLoaded = waitForLinkedBrowserEvent(tab, "load"); + + ok(inspector.markup, "There is a markup view"); + + // Select an element while the tab is in the middle of a slow reload. + testActor.eval("location.reload()"); + yield domContentLoaded; + yield chooseWithInspectElementContextMenu("img", testActor); + yield pageLoaded; + + yield inspector.once("markuploaded"); + yield waitForMultipleChildrenUpdates(inspector); + + ok(inspector.markup, "There is a markup view"); + is(inspector.markup._elt.children.length, 1, "The markup view is rendering"); +}); + +function* chooseWithInspectElementContextMenu(selector, testActor) { + yield BrowserTestUtils.synthesizeMouseAtCenter(selector, { + type: "contextmenu", + button: 2 + }, gBrowser.selectedBrowser); + + yield EventUtils.synthesizeKey("Q", {}); +} + +function waitForLinkedBrowserEvent(tab, event) { + let def = defer(); + tab.linkedBrowser.addEventListener(event, function cb() { + tab.linkedBrowser.removeEventListener(event, cb, true); + def.resolve(); + }, true); + return def.promise; +} diff --git a/devtools/client/inspector/markup/test/browser_markup_mutation_01.js b/devtools/client/inspector/markup/test/browser_markup_mutation_01.js new file mode 100644 index 000000000..1e4cfb9b0 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_mutation_01.js @@ -0,0 +1,340 @@ +/* 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"; + +// Tests that various mutations to the dom update the markup view correctly. + +const TEST_URL = URL_ROOT + "doc_markup_mutation.html"; + +// Mutation tests. Each entry in the array has the following properties: +// - desc: for logging only +// - numMutations: how many mutations are expected to come happen due to the +// test case. Defaults to 1 if not set. +// - test: a function supposed to mutate the DOM +// - check: a function supposed to test that the mutation was handled +const TEST_DATA = [ + { + desc: "Adding an attribute", + test: function* (testActor) { + yield testActor.setAttribute("#node1", "newattr", "newattrval"); + }, + check: function* (inspector) { + let {editor} = yield getContainerForSelector("#node1", inspector); + ok([...editor.attrList.querySelectorAll(".attreditor")].some(attr => { + return attr.textContent.trim() === "newattr=\"newattrval\"" + && attr.dataset.value === "newattrval" + && attr.dataset.attr === "newattr"; + }), "newattr attribute found"); + } + }, + { + desc: "Removing an attribute", + test: function* (testActor) { + yield testActor.removeAttribute("#node1", "newattr"); + }, + check: function* (inspector) { + let {editor} = yield getContainerForSelector("#node1", inspector); + ok(![...editor.attrList.querySelectorAll(".attreditor")].some(attr => { + return attr.textContent.trim() === "newattr=\"newattrval\""; + }), "newattr attribute removed"); + } + }, + { + desc: "Re-adding an attribute", + test: function* (testActor) { + yield testActor.setAttribute("#node1", "newattr", "newattrval"); + }, + check: function* (inspector) { + let {editor} = yield getContainerForSelector("#node1", inspector); + ok([...editor.attrList.querySelectorAll(".attreditor")].some(attr => { + return attr.textContent.trim() === "newattr=\"newattrval\"" + && attr.dataset.value === "newattrval" + && attr.dataset.attr === "newattr"; + }), "newattr attribute found"); + } + }, + { + desc: "Changing an attribute", + test: function* (testActor) { + yield testActor.setAttribute("#node1", "newattr", "newattrchanged"); + }, + check: function* (inspector) { + let {editor} = yield getContainerForSelector("#node1", inspector); + ok([...editor.attrList.querySelectorAll(".attreditor")].some(attr => { + return attr.textContent.trim() === "newattr=\"newattrchanged\"" + && attr.dataset.value === "newattrchanged" + && attr.dataset.attr === "newattr"; + }), "newattr attribute found"); + } + }, + { + desc: "Adding another attribute does not rerender unchanged attributes", + test: function* (testActor, inspector) { + let {editor} = yield getContainerForSelector("#node1", inspector); + + // This test checks the impact on the markup-view nodes after setting attributes on + // content nodes. + info("Expect attribute-container for 'new-attr' from the previous test"); + let attributeContainer = editor.attrList.querySelector("[data-attr=newattr]"); + ok(attributeContainer, "attribute-container for 'newattr' found"); + + info("Set a flag on the attribute-container to check after the mutation"); + attributeContainer.beforeMutationFlag = true; + + info("Add the attribute 'otherattr' on the content node to trigger the mutation"); + yield testActor.setAttribute("#node1", "otherattr", "othervalue"); + }, + check: function* (inspector) { + let {editor} = yield getContainerForSelector("#node1", inspector); + + info("Check the attribute-container for the new attribute mutation was created"); + let otherAttrContainer = editor.attrList.querySelector("[data-attr=otherattr]"); + ok(otherAttrContainer, "attribute-container for 'otherattr' found"); + + info("Check the attribute-container for 'new-attr' is the same node as earlier."); + let newAttrContainer = editor.attrList.querySelector("[data-attr=newattr]"); + ok(newAttrContainer, "attribute-container for 'newattr' found"); + ok(newAttrContainer.beforeMutationFlag, "attribute-container same as earlier"); + } + }, + { + desc: "Adding ::after element", + numMutations: 2, + test: function* (testActor) { + yield testActor.eval(` + let node1 = content.document.querySelector("#node1"); + node1.classList.add("pseudo"); + `); + }, + check: function* (inspector) { + let {children} = yield getContainerForSelector("#node1", inspector); + is(children.childNodes.length, 2, + "Node1 now has 2 children (text child and ::after"); + } + }, + { + desc: "Removing ::after element", + numMutations: 2, + test: function* (testActor) { + yield testActor.eval(` + let node1 = content.document.querySelector("#node1"); + node1.classList.remove("pseudo"); + `); + }, + check: function* (inspector) { + let container = yield getContainerForSelector("#node1", inspector); + ok(container.inlineTextChild, "Has single text child."); + } + }, + { + desc: "Updating the text-content", + test: function* (testActor) { + yield testActor.setProperty("#node1", "textContent", "newtext"); + }, + check: function* (inspector) { + let container = yield getContainerForSelector("#node1", inspector); + ok(container.inlineTextChild, "Has single text child."); + ok(!container.canExpand, "Can't expand container with inlineTextChild."); + ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild."); + is(container.editor.elt.querySelector(".text").textContent.trim(), + "newtext", "Single text child editor updated."); + } + }, + { + desc: "Adding a second text child", + test: function* (testActor) { + yield testActor.eval(` + let node1 = content.document.querySelector("#node1"); + let newText = node1.ownerDocument.createTextNode("more"); + node1.appendChild(newText); + `); + }, + check: function* (inspector) { + let container = yield getContainerForSelector("#node1", inspector); + ok(!container.inlineTextChild, "Does not have single text child."); + ok(container.canExpand, "Can expand container with child nodes."); + ok(container.editor.elt.querySelector(".text") == null, + "Single text child editor removed."); + }, + }, + { + desc: "Go from 2 to 1 text child", + test: function* (testActor) { + yield testActor.setProperty("#node1", "textContent", "newtext"); + }, + check: function* (inspector) { + let container = yield getContainerForSelector("#node1", inspector); + ok(container.inlineTextChild, "Has single text child."); + ok(!container.canExpand, "Can't expand container with inlineTextChild."); + ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild."); + ok(container.editor.elt.querySelector(".text").textContent.trim(), + "newtext", "Single text child editor updated."); + }, + }, + { + desc: "Removing an only text child", + test: function* (testActor) { + yield testActor.setProperty("#node1", "innerHTML", ""); + }, + check: function* (inspector) { + let container = yield getContainerForSelector("#node1", inspector); + ok(!container.inlineTextChild, "Does not have single text child."); + ok(!container.canExpand, "Can't expand empty container."); + ok(container.editor.elt.querySelector(".text") == null, + "Single text child editor removed."); + }, + }, + { + desc: "Go from 0 to 1 text child", + test: function* (testActor) { + yield testActor.setProperty("#node1", "textContent", "newtext"); + }, + check: function* (inspector) { + let container = yield getContainerForSelector("#node1", inspector); + ok(container.inlineTextChild, "Has single text child."); + ok(!container.canExpand, "Can't expand container with inlineTextChild."); + ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild."); + ok(container.editor.elt.querySelector(".text").textContent.trim(), + "newtext", "Single text child editor updated."); + }, + }, + + { + desc: "Updating the innerHTML", + test: function* (testActor) { + yield testActor.setProperty("#node2", "innerHTML", + "<div><span>foo</span></div>"); + }, + check: function* (inspector) { + let container = yield getContainerForSelector("#node2", inspector); + + let openTags = container.children.querySelectorAll(".open .tag"); + is(openTags.length, 2, "There are 2 tags in node2"); + is(openTags[0].textContent.trim(), "div", "The first tag is a div"); + is(openTags[1].textContent.trim(), "span", "The second tag is a span"); + + is(container.children.querySelector(".text").textContent.trim(), "foo", + "The span's textcontent is correct"); + } + }, + { + desc: "Removing child nodes", + test: function* (testActor) { + yield testActor.eval(` + let node4 = content.document.querySelector("#node4"); + while (node4.firstChild) { + node4.removeChild(node4.firstChild); + } + `); + }, + check: function* (inspector) { + let {children} = yield getContainerForSelector("#node4", inspector); + is(children.innerHTML, "", "Children have been removed"); + } + }, + { + desc: "Appending a child to a different parent", + test: function* (testActor) { + yield testActor.eval(` + let node17 = content.document.querySelector("#node17"); + let node2 = content.document.querySelector("#node2"); + node2.appendChild(node17); + `); + }, + check: function* (inspector) { + let {children} = yield getContainerForSelector("#node16", inspector); + is(children.innerHTML, "", + "Node17 has been removed from its node16 parent"); + + let container = yield getContainerForSelector("#node2", inspector); + let openTags = container.children.querySelectorAll(".open .tag"); + is(openTags.length, 3, "There are now 3 tags in node2"); + is(openTags[2].textContent.trim(), "p", "The third tag is node17"); + } + }, + { + desc: "Swapping a parent and child element, putting them in the same tree", + // body + // node1 + // node18 + // node19 + // node20 + // node21 + // will become: + // body + // node1 + // node20 + // node21 + // node18 + // node19 + test: function* (testActor) { + yield testActor.eval(` + let node18 = content.document.querySelector("#node18"); + let node20 = content.document.querySelector("#node20"); + let node1 = content.document.querySelector("#node1"); + node1.appendChild(node20); + node20.appendChild(node18); + `); + }, + check: function* (inspector) { + yield inspector.markup.expandAll(); + + let {children} = yield getContainerForSelector("#node1", inspector); + is(children.childNodes.length, 2, + "Node1 now has 2 children (textnode and node20)"); + + let node20 = children.childNodes[1]; + let node20Children = node20.container.children; + is(node20Children.childNodes.length, 2, + "Node20 has 2 children (21 and 18)"); + + let node21 = node20Children.childNodes[0]; + is(node21.container.editor.elt.querySelector(".text").textContent.trim(), + "line21", "Node21 has a single text child"); + + let node18 = node20Children.childNodes[1]; + is(node18.querySelector(".open .attreditor .attr-value") + .textContent.trim(), + "node18", "Node20's second child is indeed node18"); + } + } +]; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + info("Expanding all markup-view nodes"); + yield inspector.markup.expandAll(); + + for (let {desc, test, check, numMutations} of TEST_DATA) { + info("Starting test: " + desc); + + numMutations = numMutations || 1; + + info("Executing the test markup mutation"); + + // If a test expects more than one mutation it may come through in a single + // event or possibly in multiples. + let def = defer(); + let seenMutations = 0; + inspector.on("markupmutation", function onmutation(e, mutations) { + seenMutations += mutations.length; + info("Receieved " + seenMutations + + " mutations, expecting at least " + numMutations); + if (seenMutations >= numMutations) { + inspector.off("markupmutation", onmutation); + def.resolve(); + } + }); + yield test(testActor, inspector); + yield def.promise; + + info("Expanding all markup-view nodes to make sure new nodes are imported"); + yield inspector.markup.expandAll(); + + info("Checking the markup-view content"); + yield check(inspector); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_mutation_02.js b/devtools/client/inspector/markup/test/browser_markup_mutation_02.js new file mode 100644 index 000000000..eb69b4201 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_mutation_02.js @@ -0,0 +1,159 @@ +/* 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 markup-containers in the markup-view do flash when their +// corresponding DOM nodes mutate + +// Have to use the same timer functions used by the inspector. +const {clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {}); + +const TEST_URL = URL_ROOT + "doc_markup_flashing.html"; + +// The test data contains a list of mutations to test. +// Each item is an object: +// - desc: a description of the test step, for better logging +// - mutate: a generator function that should make changes to the content DOM +// - attribute: if set, the test will expect the corresponding attribute to +// flash instead of the whole node +// - flashedNode: [optional] the css selector of the node that is expected to +// flash in the markup-view as a result of the mutation. +// If missing, the rootNode (".list") will be expected to flash +const TEST_DATA = [{ + desc: "Adding a new node should flash the new node", + mutate: function* (testActor) { + yield testActor.eval(` + let newLi = content.document.createElement("LI"); + newLi.textContent = "new list item"; + content.document.querySelector(".list").appendChild(newLi); + `); + }, + flashedNode: ".list li:nth-child(3)" +}, { + desc: "Removing a node should flash its parent", + mutate: function* (testActor) { + yield testActor.eval(` + let root = content.document.querySelector(".list"); + root.removeChild(root.lastElementChild); + `); + } +}, { + desc: "Re-appending an existing node should only flash this node", + mutate: function* (testActor) { + yield testActor.eval(` + let root = content.document.querySelector(".list"); + root.appendChild(root.firstElementChild); + `); + }, + flashedNode: ".list .item:last-child" +}, { + desc: "Adding an attribute should flash the attribute", + attribute: "test-name", + mutate: function* (testActor) { + yield testActor.setAttribute(".list", "test-name", "value-" + Date.now()); + } +}, { + desc: "Adding an attribute with css reserved characters should flash the " + + "attribute", + attribute: "one:two", + mutate: function* (testActor) { + yield testActor.setAttribute(".list", "one:two", "value-" + Date.now()); + } +}, { + desc: "Editing an attribute should flash the attribute", + attribute: "class", + mutate: function* (testActor) { + yield testActor.setAttribute(".list", "class", "list value-" + Date.now()); + } +}, { + desc: "Multiple changes to an attribute should flash the attribute", + attribute: "class", + mutate: function* (testActor) { + yield testActor.eval(` + let root = content.document.querySelector(".list"); + root.removeAttribute("class"); + root.setAttribute("class", "list value-" + Date.now()); + root.setAttribute("class", "list value-" + Date.now()); + root.removeAttribute("class"); + root.setAttribute("class", "list value-" + Date.now()); + root.setAttribute("class", "list value-" + Date.now()); + `); + } +}, { + desc: "Removing an attribute should flash the node", + mutate: function* (testActor) { + yield testActor.eval(` + let root = content.document.querySelector(".list"); + root.removeAttribute("class"); + `); + } +}]; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + // Make sure mutated nodes flash for a very long time so we can more easily + // assert they do + inspector.markup.CONTAINER_FLASHING_DURATION = 1000 * 60 * 60; + + info("Getting the <ul.list> root node to test mutations on"); + let rootNodeFront = yield getNodeFront(".list", inspector); + + info("Selecting the last element of the root node before starting"); + yield selectNode(".list .item:nth-child(2)", inspector); + + for (let {mutate, flashedNode, desc, attribute} of TEST_DATA) { + info("Starting test: " + desc); + + info("Mutating the DOM and listening for markupmutation event"); + let onMutation = inspector.once("markupmutation"); + yield mutate(testActor); + let mutations = yield onMutation; + + info("Wait for the breadcrumbs widget to update if it needs to"); + if (inspector.breadcrumbs._hasInterestingMutations(mutations)) { + yield inspector.once("breadcrumbs-updated"); + } + + info("Asserting that the correct markup-container is flashing"); + let flashingNodeFront = rootNodeFront; + if (flashedNode) { + flashingNodeFront = yield getNodeFront(flashedNode, inspector); + } + + if (attribute) { + yield assertAttributeFlashing(flashingNodeFront, attribute, inspector); + } else { + yield assertNodeFlashing(flashingNodeFront, inspector); + } + } +}); + +function* assertNodeFlashing(nodeFront, inspector) { + let container = getContainerForNodeFront(nodeFront, inspector); + ok(container, "Markup container for node found"); + ok(container.tagState.classList.contains("theme-bg-contrast"), + "Markup container for node is flashing"); + + // Clear the mutation flashing timeout now that we checked the node was + // flashing. + clearTimeout(container._flashMutationTimer); + container._flashMutationTimer = null; + container.tagState.classList.remove("theme-bg-contrast"); +} + +function* assertAttributeFlashing(nodeFront, attribute, inspector) { + let container = getContainerForNodeFront(nodeFront, inspector); + ok(container, "Markup container for node found"); + ok(container.editor.attrElements.get(attribute), + "Attribute exists on editor"); + + let attributeElement = container.editor.getAttributeElement(attribute); + + ok(attributeElement.classList.contains("theme-bg-contrast"), + "Element for " + attribute + " attribute is flashing"); + + attributeElement.classList.remove("theme-bg-contrast"); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_navigation.js b/devtools/client/inspector/markup/test/browser_markup_navigation.js new file mode 100644 index 000000000..5bfd9719f --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_navigation.js @@ -0,0 +1,147 @@ +/* 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 the markup-view nodes can be navigated to with the keyboard + +const TEST_URL = URL_ROOT + "doc_markup_navigation.html"; +const TEST_DATA = [ + ["pageup", "*doctype*"], + ["down", "html"], + ["down", "head"], + ["down", "body"], + ["down", "node0"], + ["right", "node0"], + ["down", "node1"], + ["down", "node2"], + ["down", "node3"], + ["down", "*comment*"], + ["down", "node4"], + ["right", "node4"], + ["down", "*text*"], + ["down", "node5"], + ["down", "*text*"], + ["down", "node6"], + ["down", "*text*"], + ["down", "*comment*"], + ["down", "node7"], + ["right", "node7"], + ["down", "*text*"], + ["down", "node8"], + ["left", "node7"], + ["left", "node7"], + ["right", "node7"], + ["right", "*text*"], + ["down", "node8"], + ["down", "*text*"], + ["down", "node9"], + ["down", "*text*"], + ["down", "node10"], + ["down", "*text*"], + ["down", "node11"], + ["down", "*text*"], + ["down", "node12"], + ["right", "node12"], + ["down", "*text*"], + ["down", "node13"], + ["down", "node14"], + ["down", "node15"], + ["down", "node15"], + ["down", "node15"], + ["up", "node14"], + ["up", "node13"], + ["up", "*text*"], + ["up", "node12"], + ["left", "node12"], + ["down", "node14"], + ["home", "*doctype*"], + ["pagedown", "*text*"], + ["down", "node5"], + ["down", "*text*"], + ["down", "node6"], + ["down", "*text*"], + ["down", "*comment*"], + ["down", "node7"], + ["left", "node7"], + ["down", "*text*"], + ["down", "node9"], + ["down", "*text*"], + ["down", "node10"], + ["pageup", "*text*"], + ["pageup", "*doctype*"], + ["down", "html"], + ["left", "html"], + ["down", "head"] +]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + info("Making sure the markup-view frame is focused"); + inspector.markup._frame.focus(); + + info("Starting to iterate through the test data"); + for (let [key, className] of TEST_DATA) { + info("Testing step: " + key + " to navigate to " + className); + pressKey(key); + + info("Making sure markup-view children get updated"); + yield waitForChildrenUpdated(inspector); + + info("Checking the right node is selected"); + checkSelectedNode(key, className, inspector); + } + + // In theory, we should wait for the inspector-updated event at each iteration + // of the previous loop where we expect the current node to change (because + // changing the current node ends up refreshing the rule-view, breadcrumbs, + // ...), but this would make this test a *lot* slower. Instead, having a final + // catch-all event works too. + yield inspector.once("inspector-updated"); +}); + +function pressKey(key) { + switch (key) { + case "right": + EventUtils.synthesizeKey("VK_RIGHT", {}); + break; + case "down": + EventUtils.synthesizeKey("VK_DOWN", {}); + break; + case "left": + EventUtils.synthesizeKey("VK_LEFT", {}); + break; + case "up": + EventUtils.synthesizeKey("VK_UP", {}); + break; + case "pageup": + EventUtils.synthesizeKey("VK_PAGE_UP", {}); + break; + case "pagedown": + EventUtils.synthesizeKey("VK_PAGE_DOWN", {}); + break; + case "home": + EventUtils.synthesizeKey("VK_HOME", {}); + break; + } +} + +function checkSelectedNode(key, className, inspector) { + let node = inspector.selection.nodeFront; + + if (className == "*comment*") { + is(node.nodeType, Node.COMMENT_NODE, + "Found a comment after pressing " + key); + } else if (className == "*text*") { + is(node.nodeType, Node.TEXT_NODE, + "Found text after pressing " + key); + } else if (className == "*doctype*") { + is(node.nodeType, Node.DOCUMENT_TYPE_NODE, + "Found the doctype after pressing " + key); + } else { + is(node.className, className, + "Found node: " + className + " after pressing " + key); + } +} diff --git a/devtools/client/inspector/markup/test/browser_markup_node_names.js b/devtools/client/inspector/markup/test/browser_markup_node_names.js new file mode 100644 index 000000000..a8afad5e9 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_node_names.js @@ -0,0 +1,28 @@ +/* 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 element node name in the markupview +const TEST_URL = URL_ROOT + "doc_markup_html_mixed_case.html"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + // Get and open the svg element to show its children + let svgNodeFront = yield getNodeFront("svg", inspector); + yield inspector.markup.expandNode(svgNodeFront); + yield waitForMultipleChildrenUpdates(inspector); + + let clipPathContainer = yield getContainerForSelector("clipPath", inspector); + info("Checking the clipPath element"); + ok(clipPathContainer.editor.tag.textContent === "clipPath", + "clipPath node name is not lowercased"); + + let divContainer = yield getContainerForSelector("div", inspector); + + info("Checking the div element"); + ok(divContainer.editor.tag.textContent === "div", + "div node name is lowercased"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_node_names_namespaced.js b/devtools/client/inspector/markup/test/browser_markup_node_names_namespaced.js new file mode 100644 index 000000000..261176f94 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_node_names_namespaced.js @@ -0,0 +1,43 @@ +/* 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 namespaced element node names in the markupview. + +const XHTML = ` + <!DOCTYPE html> + <html xmlns="http://www.w3.org/1999/xhtml" + xmlns:svg="http://www.w3.org/2000/svg"> + <body> + <svg:svg width="100" height="100"> + <svg:clipPath id="clip"> + <svg:rect id="rectangle" x="0" y="0" width="10" height="5"></svg:rect> + </svg:clipPath> + <svg:circle cx="0" cy="0" r="5"></svg:circle> + </svg:svg> + </body> + </html> +`; + +const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML); + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URI); + + // Get and open the svg element to show its children. + let svgNodeFront = yield getNodeFront("svg", inspector); + yield inspector.markup.expandNode(svgNodeFront); + yield waitForMultipleChildrenUpdates(inspector); + + let clipPathContainer = yield getContainerForSelector("clipPath", inspector); + info("Checking the clipPath element"); + ok(clipPathContainer.editor.tag.textContent === "svg:clipPath", + "svg:clipPath node is correctly displayed"); + + let circlePathContainer = yield getContainerForSelector("circle", inspector); + info("Checking the circle element"); + ok(circlePathContainer.editor.tag.textContent === "svg:circle", + "svg:circle node is correctly displayed"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_node_not_displayed_01.js b/devtools/client/inspector/markup/test/browser_markup_node_not_displayed_01.js new file mode 100644 index 000000000..ea4ecdfd0 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_node_not_displayed_01.js @@ -0,0 +1,35 @@ +/* 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"; + +// Tests that nodes that are not displayed appear differently in the markup-view +// when these nodes are imported in the view. + +// Note that nodes inside a display:none parent are obviously not displayed too +// but the markup-view uses css inheritance to mark those as hidden instead of +// having to visit each and every child of a hidden node. So there's no sense +// testing children nodes. + +const TEST_URL = URL_ROOT + "doc_markup_not_displayed.html"; +const TEST_DATA = [ + {selector: "#normal-div", isDisplayed: true}, + {selector: "head", isDisplayed: false}, + {selector: "#display-none", isDisplayed: false}, + {selector: "#hidden-true", isDisplayed: false}, + {selector: "#visibility-hidden", isDisplayed: true}, + {selector: "#hidden-via-hide-shortcut", isDisplayed: false}, +]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + for (let {selector, isDisplayed} of TEST_DATA) { + info("Getting node " + selector); + let nodeFront = yield getNodeFront(selector, inspector); + let container = getContainerForNodeFront(nodeFront, inspector); + is(!container.elt.classList.contains("not-displayed"), isDisplayed, + `The container for ${selector} is marked as displayed ${isDisplayed}`); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_node_not_displayed_02.js b/devtools/client/inspector/markup/test/browser_markup_node_not_displayed_02.js new file mode 100644 index 000000000..b0423d2e6 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_node_not_displayed_02.js @@ -0,0 +1,150 @@ +/* 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"; + +// Tests that nodes are marked as displayed and not-displayed dynamically, when +// their display changes + +const TEST_URL = URL_ROOT + "doc_markup_not_displayed.html"; +const TEST_DATA = [ + { + desc: "Hiding a node by creating a new stylesheet", + selector: "#normal-div", + before: true, + changeStyle: function* (testActor) { + yield testActor.eval(` + let div = content.document.createElement("div"); + div.id = "new-style"; + div.innerHTML = "<style>#normal-div {display:none;}</style>"; + content.document.body.appendChild(div); + `); + }, + after: false + }, + { + desc: "Showing a node by deleting an existing stylesheet", + selector: "#normal-div", + before: false, + changeStyle: function* (testActor) { + yield testActor.eval(` + content.document.getElementById("new-style").remove(); + `); + }, + after: true + }, + { + desc: "Hiding a node by changing its style property", + selector: "#display-none", + before: false, + changeStyle: function* (testActor) { + yield testActor.eval(` + let node = content.document.querySelector("#display-none"); + node.style.display = "block"; + `); + }, + after: true + }, + { + desc: "Showing a node by removing its hidden attribute", + selector: "#hidden-true", + before: false, + changeStyle: function* (testActor) { + yield testActor.eval(` + content.document.querySelector("#hidden-true") + .removeAttribute("hidden"); + `); + }, + after: true + }, + { + desc: "Hiding a node by adding a hidden attribute", + selector: "#hidden-true", + before: true, + changeStyle: function* (testActor) { + yield testActor.setAttribute("#hidden-true", "hidden", "true"); + }, + after: false + }, + { + desc: "Showing a node by changin a stylesheet's rule", + selector: "#hidden-via-stylesheet", + before: false, + changeStyle: function* (testActor) { + yield testActor.eval(` + content.document.styleSheets[0] + .cssRules[0].style + .setProperty("display", "inline"); + `); + }, + after: true + }, + { + desc: "Hiding a node by adding a new rule to a stylesheet", + selector: "#hidden-via-stylesheet", + before: true, + changeStyle: function* (testActor) { + yield testActor.eval(` + content.document.styleSheets[0].insertRule( + "#hidden-via-stylesheet {display: none;}", 1); + `); + }, + after: false + }, + { + desc: "Hiding a node by adding a class that matches an existing rule", + selector: "#normal-div", + before: true, + changeStyle: function* (testActor) { + yield testActor.eval(` + content.document.styleSheets[0].insertRule( + ".a-new-class {display: none;}", 2); + content.document.querySelector("#normal-div") + .classList.add("a-new-class"); + `); + }, + after: false + } +]; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + for (let data of TEST_DATA) { + info("Running test case: " + data.desc); + yield runTestData(inspector, testActor, data); + } +}); + +function* runTestData(inspector, testActor, + {selector, before, changeStyle, after}) { + info("Getting the " + selector + " test node"); + let nodeFront = yield getNodeFront(selector, inspector); + let container = getContainerForNodeFront(nodeFront, inspector); + is(!container.elt.classList.contains("not-displayed"), before, + "The container is marked as " + (before ? "shown" : "hidden")); + + info("Listening for the display-change event"); + let onDisplayChanged = defer(); + inspector.markup.walker.once("display-change", onDisplayChanged.resolve); + + info("Making style changes"); + yield changeStyle(testActor); + let nodes = yield onDisplayChanged.promise; + + info("Verifying that the list of changed nodes include our container"); + + ok(nodes.length, "The display-change event was received with a nodes"); + let foundContainer = false; + for (let node of nodes) { + if (getContainerForNodeFront(node, inspector) === container) { + foundContainer = true; + break; + } + } + ok(foundContainer, "Container is part of the list of changed nodes"); + + is(!container.elt.classList.contains("not-displayed"), after, + "The container is marked as " + (after ? "shown" : "hidden")); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_pagesize_01.js b/devtools/client/inspector/markup/test/browser_markup_pagesize_01.js new file mode 100644 index 000000000..a9ba9fc05 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_pagesize_01.js @@ -0,0 +1,86 @@ +/* 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"; + +// Tests that the markup view loads only as many nodes as specified by the +// devtools.markup.pagesize preference. + +Services.prefs.setIntPref("devtools.markup.pagesize", 5); + +const TEST_URL = URL_ROOT + "doc_markup_pagesize_01.html"; +const TEST_DATA = [{ + desc: "Select the last item", + selector: "#z", + expected: "*more*vwxyz" +}, { + desc: "Select the first item", + selector: "#a", + expected: "abcde*more*" +}, { + desc: "Select the last item", + selector: "#z", + expected: "*more*vwxyz" +}, { + desc: "Select an already-visible item", + selector: "#v", + // Because "v" was already visible, we shouldn't have loaded + // a different page. + expected: "*more*vwxyz" +}, { + desc: "Verify childrenDirty reloads the page", + selector: "#w", + forceReload: true, + // But now that we don't already have a loaded page, selecting + // w should center around w. + expected: "*more*uvwxy*more*" +}]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + info("Start iterating through the test data"); + for (let step of TEST_DATA) { + info("Start test: " + step.desc); + + if (step.forceReload) { + yield forceReload(inspector); + } + info("Selecting the node that corresponds to " + step.selector); + yield selectNode(step.selector, inspector); + + info("Checking that the right nodes are shwon"); + yield assertChildren(step.expected, inspector); + } + + info("Checking that clicking the more button loads everything"); + yield clickShowMoreNodes(inspector); + yield inspector.markup._waitForChildren(); + yield assertChildren("abcdefghijklmnopqrstuvwxyz", inspector); +}); + +function* assertChildren(expected, inspector) { + let container = yield getContainerForSelector("body", inspector); + let found = ""; + for (let child of container.children.children) { + if (child.classList.contains("more-nodes")) { + found += "*more*"; + } else { + found += child.container.node.getAttribute("id"); + } + } + is(found, expected, "Got the expected children."); +} + +function* forceReload(inspector) { + let container = yield getContainerForSelector("body", inspector); + container.childrenDirty = true; +} + +function* clickShowMoreNodes(inspector) { + let container = yield getContainerForSelector("body", inspector); + let button = container.elt.querySelector("button"); + let win = button.ownerDocument.defaultView; + EventUtils.sendMouseEvent({type: "click"}, button, win); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_pagesize_02.js b/devtools/client/inspector/markup/test/browser_markup_pagesize_02.js new file mode 100644 index 000000000..549a36b0d --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_pagesize_02.js @@ -0,0 +1,47 @@ +/* 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"; + +// Tests that the markup view loads only as many nodes as specified +// by the devtools.markup.pagesize preference and that pressing the "show all +// nodes" actually shows the nodes + +const TEST_URL = URL_ROOT + "doc_markup_pagesize_02.html"; + +// Make sure nodes are hidden when there are more than 5 in a row +Services.prefs.setIntPref("devtools.markup.pagesize", 5); + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + info("Selecting the UL node"); + yield clickContainer("ul", inspector); + info("Reloading the page with the UL node selected will expand its children"); + yield reloadPage(inspector, testActor); + yield inspector.markup._waitForChildren(); + + info("Click on the 'show all nodes' button in the UL's list of children"); + yield showAllNodes(inspector); + + yield assertAllNodesAreVisible(inspector, testActor); +}); + +function* showAllNodes(inspector) { + let container = yield getContainerForSelector("ul", inspector); + let button = container.elt.querySelector("button"); + ok(button, "All nodes button is here"); + let win = button.ownerDocument.defaultView; + + EventUtils.sendMouseEvent({type: "click"}, button, win); + yield inspector.markup._waitForChildren(); +} + +function* assertAllNodesAreVisible(inspector, testActor) { + let container = yield getContainerForSelector("ul", inspector); + ok(!container.elt.querySelector("button"), + "All nodes button isn't here anymore"); + let numItems = yield testActor.getNumberOfElementMatches("ul > *"); + is(container.children.childNodes.length, numItems); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_remove_xul_attributes.js b/devtools/client/inspector/markup/test/browser_markup_remove_xul_attributes.js new file mode 100644 index 000000000..b7065c683 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_remove_xul_attributes.js @@ -0,0 +1,28 @@ +/* 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 confirms that XUL attributes don't show up as empty +// attributes after being deleted + +const TEST_URL = URL_ROOT + "doc_markup_xul.xul"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + let panelFront = yield getNodeFront("#test", inspector); + ok(panelFront.hasAttribute("id"), + "panelFront has id attribute in the beginning"); + + info("Removing panel's id attribute"); + let onMutation = inspector.once("markupmutation"); + yield testActor.removeAttribute("#test", "id"); + + info("Waiting for markupmutation"); + yield onMutation; + + is(panelFront.hasAttribute("id"), false, + "panelFront doesn't have id attribute anymore"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_search_01.js b/devtools/client/inspector/markup/test/browser_markup_search_01.js new file mode 100644 index 000000000..68f0c04db --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_search_01.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 searching for nodes using the selector-search input expands and +// selects the right nodes in the markup-view, even when those nodes are deeply +// nested (and therefore not attached yet when the markup-view is initialized). + +const TEST_URL = URL_ROOT + "doc_markup_search.html"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + let container = yield getContainerForSelector("em", inspector); + ok(!container, "The <em> tag isn't present yet in the markup-view"); + + // Searching for the innermost element first makes sure that the inspector + // back-end is able to attach the resulting node to the tree it knows at the + // moment. When the inspector is started, the <body> is the default selected + // node, and only the parents up to the ROOT are known, and its direct + // children. + info("searching for the innermost child: <em>"); + yield searchFor("em", inspector); + + container = yield getContainerForSelector("em", inspector); + ok(container, "The <em> tag is now imported in the markup-view"); + + let nodeFront = yield getNodeFront("em", inspector); + is(inspector.selection.nodeFront, nodeFront, + "The <em> tag is the currently selected node"); + + info("searching for other nodes too"); + for (let node of ["span", "li", "ul"]) { + yield searchFor(node, inspector); + + nodeFront = yield getNodeFront(node, inspector); + is(inspector.selection.nodeFront, nodeFront, + "The <" + node + "> tag is the currently selected node"); + } +}); + +function* searchFor(selector, inspector) { + let onNewNodeFront = inspector.selection.once("new-node-front"); + + searchUsingSelectorSearch(selector, inspector); + + yield onNewNodeFront; + yield inspector.once("inspector-updated"); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_01.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_01.js new file mode 100644 index 000000000..b1b4f7115 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_01.js @@ -0,0 +1,68 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_attributes_test_runner.js */ +"use strict"; + +// Test editing various markup-containers' attribute fields + +loadHelperScript("helper_attributes_test_runner.js"); + +const TEST_URL = URL_ROOT + "doc_markup_edit.html"; +var TEST_DATA = [{ + desc: "Change an attribute", + node: "#node1", + originalAttributes: { + id: "node1", + class: "node1" + }, + name: "class", + value: 'class="changednode1"', + expectedAttributes: { + id: "node1", + class: "changednode1" + } +}, { + desc: "Try changing an attribute to a quote (\") - this should result " + + "in it being set to an empty string", + node: "#node22", + originalAttributes: { + id: "node22", + class: "unchanged" + }, + name: "class", + value: 'class="""', + expectedAttributes: { + id: "node22", + class: "" + } +}, { + desc: "Remove an attribute", + node: "#node4", + originalAttributes: { + id: "node4", + class: "node4" + }, + name: "class", + value: "", + expectedAttributes: { + id: "node4" + } +}, { + desc: "Try add attributes by adding to an existing attribute's entry", + node: "#node24", + originalAttributes: { + id: "node24" + }, + name: "id", + value: 'id="node24" class="""', + expectedAttributes: { + id: "node24", + class: "" + } +}]; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + yield runEditAttributesTests(TEST_DATA, inspector, testActor); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_02.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_02.js new file mode 100644 index 000000000..1e32d783a --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_02.js @@ -0,0 +1,44 @@ +/* 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"; + +// Tests that an existing attribute can be modified + +const TEST_URL = `data:text/html, + <div id='test-div'>Test modifying my ID attribute</div>`; + +add_task(function* () { + info("Opening the inspector on the test page"); + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + info("Selecting the test node"); + yield focusNode("#test-div", inspector); + + info("Verify attributes, only ID should be there for now"); + yield assertAttributes("#test-div", { + id: "test-div" + }, testActor); + + info("Focus the ID attribute and change its content"); + let {editor} = yield getContainerForSelector("#test-div", inspector); + let attr = editor.attrElements.get("id").querySelector(".editable"); + let mutated = inspector.once("markupmutation"); + setEditableFieldValue(attr, + attr.textContent + ' class="newclass" style="color:green"', inspector); + yield mutated; + + info("Verify attributes, should have ID, class and style"); + yield assertAttributes("#test-div", { + id: "test-div", + class: "newclass", + style: "color:green" + }, testActor); + + info("Trying to undo the change"); + yield undoChange(inspector); + yield assertAttributes("#test-div", { + id: "test-div" + }, testActor); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_03.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_03.js new file mode 100644 index 000000000..cdbdc72b6 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_03.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"; + +// Tests that a node's tagname can be edited in the markup-view + +const TEST_URL = `data:text/html;charset=utf-8, + <div id='retag-me'><div id='retag-me-2'></div></div>`; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + yield inspector.markup.expandAll(); + + info("Selecting the test node"); + yield focusNode("#retag-me", inspector); + + info("Getting the markup-container for the test node"); + let container = yield getContainerForSelector("#retag-me", inspector); + ok(container.expanded, "The container is expanded"); + + let parentInfo = yield testActor.getNodeInfo("#retag-me"); + is(parentInfo.tagName.toLowerCase(), "div", + "We've got #retag-me element, it's a DIV"); + is(parentInfo.numChildren, 1, "#retag-me has one child"); + let childInfo = yield testActor.getNodeInfo("#retag-me > *"); + is(childInfo.attributes[0].value, "retag-me-2", + "#retag-me's only child is #retag-me-2"); + + info("Changing #retag-me's tagname in the markup-view"); + let mutated = inspector.once("markupmutation"); + let tagEditor = container.editor.tag; + setEditableFieldValue(tagEditor, "p", inspector); + yield mutated; + + info("Checking that the markup-container exists and is correct"); + container = yield getContainerForSelector("#retag-me", inspector); + ok(container.expanded, "The container is still expanded"); + ok(container.selected, "The container is still selected"); + + info("Checking that the tagname change was done"); + parentInfo = yield testActor.getNodeInfo("#retag-me"); + is(parentInfo.tagName.toLowerCase(), "p", + "The #retag-me element is now a P"); + is(parentInfo.numChildren, 1, "#retag-me still has one child"); + childInfo = yield testActor.getNodeInfo("#retag-me > *"); + is(childInfo.attributes[0].value, "retag-me-2", + "#retag-me's only child is #retag-me-2"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_04-backspace.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_04-backspace.js new file mode 100644 index 000000000..dbe718f45 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_04-backspace.js @@ -0,0 +1,59 @@ +/* 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"; + +// Tests that a node can be deleted from the markup-view with the backspace key. +// Also checks that after deletion the correct element is highlighted. +// The previous sibling is preferred, but the parent is a fallback. + +const HTML = `<style type="text/css"> + #pseudo::before { content: 'before'; } + #pseudo::after { content: 'after'; } + </style> + <div id="parent"> + <div id="first"></div> + <div id="second"></div> + <div id="third"></div> + </div> + <div id="only-child"> + <div id="fourth"></div> + </div> + <div id="pseudo"> + <div id="fifth"></div> + </div>`; +const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML); + +// List of all the test cases. Each item is an object with the following props: +// - selector: the css selector of the node that should be selected +// - focusedSelector: the css selector of the node we expect to be selected as +// a result of the deletion +// - pseudo: (optional) if the focused node is actually supposed to be a pseudo element +// of the specified selector. +// Note that after each test case, undo is called. +const TEST_DATA = [{ + selector: "#first", + focusedSelector: "#second" +}, { + selector: "#second", + focusedSelector: "#first" +}, { + selector: "#third", + focusedSelector: "#second" +}, { + selector: "#fourth", + focusedSelector: "#only-child" +}, { + selector: "#fifth", + focusedSelector: "#pseudo", + pseudo: "before" +}]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + for (let data of TEST_DATA) { + yield checkDeleteAndSelection(inspector, "back_space", data); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_04-delete.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_04-delete.js new file mode 100644 index 000000000..1446eba30 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_04-delete.js @@ -0,0 +1,59 @@ +/* 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"; + +// Tests that a node can be deleted from the markup-view with the delete key. +// Also checks that after deletion the correct element is highlighted. +// The next sibling is preferred, but the parent is a fallback. + +const HTML = `<style type="text/css"> + #pseudo::before { content: 'before'; } + #pseudo::after { content: 'after'; } + </style> + <div id="parent"> + <div id="first"></div> + <div id="second"></div> + <div id="third"></div> + </div> + <div id="only-child"> + <div id="fourth"></div> + </div> + <div id="pseudo"> + <div id="fifth"></div> + </div>`; +const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML); + +// List of all the test cases. Each item is an object with the following props: +// - selector: the css selector of the node that should be selected +// - focusedSelector: the css selector of the node we expect to be selected as +// a result of the deletion +// - pseudo: (optional) if the focused node is actually supposed to be a pseudo element +// of the specified selector. +// Note that after each test case, undo is called. +const TEST_DATA = [{ + selector: "#first", + focusedSelector: "#second" +}, { + selector: "#second", + focusedSelector: "#third" +}, { + selector: "#third", + focusedSelector: "#second" +}, { + selector: "#fourth", + focusedSelector: "#only-child" +}, { + selector: "#fifth", + focusedSelector: "#pseudo", + pseudo: "after" +}]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + for (let data of TEST_DATA) { + yield checkDeleteAndSelection(inspector, "delete", data); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_05.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_05.js new file mode 100644 index 000000000..54a1dab44 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_05.js @@ -0,0 +1,77 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_attributes_test_runner.js */ +"use strict"; + +// Tests that adding various types of attributes to nodes in the markup-view +// works as expected. Also checks that the changes are properly undoable and +// redoable. For each step in the test, we: +// - Create a new DIV +// - Make the change, check that the change was made as we expect +// - Undo the change, check that the node is back in its original state +// - Redo the change, check that the node change was made again correctly. + +loadHelperScript("helper_attributes_test_runner.js"); + +var TEST_URL = "data:text/html,<div>markup-view attributes addition test</div>"; +var TEST_DATA = [{ + desc: "Add an attribute value without closing \"", + text: 'style="display: block;', + expectedAttributes: { + style: "display: block;" + } +}, { + desc: "Add an attribute value without closing '", + text: "style='display: inline;", + expectedAttributes: { + style: "display: inline;" + } +}, { + desc: "Add an attribute wrapped with with double quotes double quote in it", + text: 'style="display: "inline', + expectedAttributes: { + style: "display: ", + inline: "" + } +}, { + desc: "Add an attribute wrapped with single quotes with single quote in it", + text: "style='display: 'inline", + expectedAttributes: { + style: "display: ", + inline: "" + } +}, { + desc: "Add an attribute with no value", + text: "disabled", + expectedAttributes: { + disabled: "" + } +}, { + desc: "Add multiple attributes with no value", + text: "disabled autofocus", + expectedAttributes: { + disabled: "", + autofocus: "" + } +}, { + desc: "Add multiple attributes with no value, and some with value", + text: "disabled name='name' data-test='test' autofocus", + expectedAttributes: { + disabled: "", + autofocus: "", + name: "name", + "data-test": "test" + } +}, { + desc: "Add attribute with xmlns", + text: "xmlns:edi='http://ecommerce.example.org/schema'", + expectedAttributes: { + "xmlns:edi": "http://ecommerce.example.org/schema" + } +}]; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + yield runAddAttributesTests(TEST_DATA, "div", inspector, testActor); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_06.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_06.js new file mode 100644 index 000000000..8202bd0a2 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_06.js @@ -0,0 +1,85 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_attributes_test_runner.js */ +"use strict"; + +// Tests that adding various types of attributes to nodes in the markup-view +// works as expected. Also checks that the changes are properly undoable and +// redoable. For each step in the test, we: +// - Create a new DIV +// - Make the change, check that the change was made as we expect +// - Undo the change, check that the node is back in its original state +// - Redo the change, check that the node change was made again correctly. + +loadHelperScript("helper_attributes_test_runner.js"); + +var TEST_URL = "data:text/html,<div>markup-view attributes addition test</div>"; +var TEST_DATA = [{ + desc: "Mixed single and double quotes", + text: "name=\"hi\" maxlength='not a number'", + expectedAttributes: { + maxlength: "not a number", + name: "hi" + } +}, { + desc: "Invalid attribute name", + text: "x='y' <why-would-you-do-this>=\"???\"", + expectedAttributes: { + x: "y" + } +}, { + desc: "Double quote wrapped in single quotes", + text: "x='h\"i'", + expectedAttributes: { + x: "h\"i" + } +}, { + desc: "Single quote wrapped in double quotes", + text: "x=\"h'i\"", + expectedAttributes: { + x: "h'i" + } +}, { + desc: "No quote wrapping", + text: "a=b x=y data-test=Some spaced data", + expectedAttributes: { + a: "b", + x: "y", + "data-test": "Some", + spaced: "", + data: "" + } +}, { + desc: "Duplicate Attributes", + text: "a=b a='c' a=\"d\"", + expectedAttributes: { + a: "b" + } +}, { + desc: "Inline styles", + text: "style=\"font-family: 'Lucida Grande', sans-serif; font-size: 75%;\"", + expectedAttributes: { + style: "font-family: 'Lucida Grande', sans-serif; font-size: 75%;" + } +}, { + desc: "Object attribute names", + text: "toString=\"true\" hasOwnProperty=\"false\"", + expectedAttributes: { + tostring: "true", + hasownproperty: "false" + } +}, { + desc: "Add event handlers", + text: "onclick=\"javascript: throw new Error('wont fire');\" " + + "onload=\"alert('here');\"", + expectedAttributes: { + onclick: "javascript: throw new Error('wont fire');", + onload: "alert('here');" + } +}]; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + yield runAddAttributesTests(TEST_DATA, "div", inspector, testActor); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_07.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_07.js new file mode 100644 index 000000000..fffdc99cc --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_07.js @@ -0,0 +1,135 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* import-globals-from helper_attributes_test_runner.js */ +"use strict"; + +// One more test testing various add-attributes configurations +// Some of the test data below asserts that long attributes get collapsed + +loadHelperScript("helper_attributes_test_runner.js"); + +/*eslint-disable */ +const LONG_ATTRIBUTE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const LONG_ATTRIBUTE_COLLAPSED = "ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEF\u2026UVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const DATA_URL_INLINE_STYLE='color: red; background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC");'; +const DATA_URL_INLINE_STYLE_COLLAPSED='color: red; background: url("data:image/png;base64,iVBORw0KG\u2026NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC");'; +const DATA_URL_ATTRIBUTE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC"; +const DATA_URL_ATTRIBUTE_COLLAPSED = "data:image/png;base64,iVBORw0K\u20269/AFGGFyjOXZtQAAAAAElFTkSuQmCC"; +/*eslint-enable */ + +var TEST_URL = "data:text/html,<div>markup-view attributes addition test</div>"; +var TEST_DATA = [{ + desc: "Add an attribute value containing < > ü \" & '", + text: 'src="somefile.html?param1=<a>¶m2=ü¶m3=\'"\'"', + expectedAttributes: { + src: "somefile.html?param1=<a>¶m2=\xfc¶m3='\"'" + } +}, { + desc: "Add an attribute by clicking the empty space after a node", + text: 'class="newclass" style="color:green"', + expectedAttributes: { + class: "newclass", + style: "color:green" + } +}, { + desc: "Try add an attribute containing a quote (\") attribute by " + + "clicking the empty space after a node - this should result " + + "in it being set to an empty string", + text: 'class="newclass" style="""', + expectedAttributes: { + class: "newclass", + style: "" + } +}, { + desc: "Try to add long data URL to make sure it is collapsed in attribute " + + "editor.", + text: `style='${DATA_URL_INLINE_STYLE}'`, + expectedAttributes: { + "style": DATA_URL_INLINE_STYLE + }, + validate: (container, inspector) => { + let editor = container.editor; + let visibleAttrText = editor.attrElements.get("style") + .querySelector(".attr-value") + .textContent; + is(visibleAttrText, DATA_URL_INLINE_STYLE_COLLAPSED); + } +}, { + desc: "Try to add long attribute to make sure it is collapsed in attribute " + + "editor.", + text: `data-long="${LONG_ATTRIBUTE}"`, + expectedAttributes: { + "data-long": LONG_ATTRIBUTE + }, + validate: (container, inspector) => { + let editor = container.editor; + let visibleAttrText = editor.attrElements.get("data-long") + .querySelector(".attr-value") + .textContent; + is(visibleAttrText, LONG_ATTRIBUTE_COLLAPSED); + } +}, { + desc: "Try to add long data URL to make sure it is collapsed in attribute " + + "editor.", + text: `src="${DATA_URL_ATTRIBUTE}"`, + expectedAttributes: { + "src": DATA_URL_ATTRIBUTE + }, + validate: (container, inspector) => { + let editor = container.editor; + let visibleAttrText = editor.attrElements.get("src") + .querySelector(".attr-value").textContent; + is(visibleAttrText, DATA_URL_ATTRIBUTE_COLLAPSED); + } +}, { + desc: "Try to add long attribute with collapseAttributes == false" + + "to make sure it isn't collapsed in attribute editor.", + text: `data-long="${LONG_ATTRIBUTE}"`, + expectedAttributes: { + "data-long": LONG_ATTRIBUTE + }, + setUp: function (inspector) { + Services.prefs.setBoolPref("devtools.markup.collapseAttributes", false); + }, + validate: (container, inspector) => { + let editor = container.editor; + let visibleAttrText = editor.attrElements + .get("data-long") + .querySelector(".attr-value") + .textContent; + is(visibleAttrText, LONG_ATTRIBUTE); + }, + tearDown: function (inspector) { + Services.prefs.clearUserPref("devtools.markup.collapseAttributes"); + } +}, { + desc: "Try to collapse attributes with collapseAttributeLength == 5", + text: `data-long="${LONG_ATTRIBUTE}"`, + expectedAttributes: { + "data-long": LONG_ATTRIBUTE + }, + setUp: function (inspector) { + Services.prefs.setIntPref("devtools.markup.collapseAttributeLength", 2); + }, + validate: (container, inspector) => { + let firstChar = LONG_ATTRIBUTE[0]; + let lastChar = LONG_ATTRIBUTE[LONG_ATTRIBUTE.length - 1]; + let collapsed = firstChar + "\u2026" + lastChar; + let editor = container.editor; + let visibleAttrText = editor.attrElements + .get("data-long") + .querySelector(".attr-value") + .textContent; + is(visibleAttrText, collapsed); + }, + tearDown: function (inspector) { + Services.prefs.clearUserPref("devtools.markup.collapseAttributeLength"); + } +}]; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + yield runAddAttributesTests(TEST_DATA, "div", inspector, testActor); +}); + diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_08.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_08.js new file mode 100644 index 000000000..238e59c52 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_08.js @@ -0,0 +1,132 @@ +/* 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 editing various markup-containers' attribute fields, in particular +// attributes with long values and quotes + +const TEST_URL = URL_ROOT + "doc_markup_edit.html"; +/*eslint-disable */ +const LONG_ATTRIBUTE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const LONG_ATTRIBUTE_COLLAPSED = "ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEF\u2026UVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +/*eslint-enable */ + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + yield inspector.markup.expandAll(); + yield testCollapsedLongAttribute(inspector, testActor); + yield testModifyInlineStyleWithQuotes(inspector, testActor); + yield testEditingAttributeWithMixedQuotes(inspector, testActor); +}); + +function* testCollapsedLongAttribute(inspector, testActor) { + info("Try to modify the collapsed long attribute, making sure it expands."); + + info("Adding test attributes to the node"); + let onMutated = inspector.once("markupmutation"); + yield testActor.setAttribute("#node24", "class", ""); + yield testActor.setAttribute("#node24", "data-long", LONG_ATTRIBUTE); + yield onMutated; + + yield assertAttributes("#node24", { + id: "node24", + "class": "", + "data-long": LONG_ATTRIBUTE + }, testActor); + + let {editor} = yield focusNode("#node24", inspector); + let attr = editor.attrElements.get("data-long").querySelector(".editable"); + + // Check to make sure it has expanded after focus + attr.focus(); + EventUtils.sendKey("return", inspector.panelWin); + let input = inplaceEditor(attr).input; + is(input.value, `data-long="${LONG_ATTRIBUTE}"`); + EventUtils.sendKey("escape", inspector.panelWin); + + setEditableFieldValue(attr, input.value + ' data-short="ABC"', inspector); + yield inspector.once("markupmutation"); + + let visibleAttrText = editor.attrElements.get("data-long") + .querySelector(".attr-value").textContent; + is(visibleAttrText, LONG_ATTRIBUTE_COLLAPSED); + + yield assertAttributes("#node24", { + id: "node24", + class: "", + "data-long": LONG_ATTRIBUTE, + "data-short": "ABC" + }, testActor); +} + +function* testModifyInlineStyleWithQuotes(inspector, testActor) { + info("Modify inline style containing \""); + + yield assertAttributes("#node26", { + id: "node26", + style: 'background-image: url("moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F");' + }, testActor); + + let onMutated = inspector.once("markupmutation"); + let {editor} = yield focusNode("#node26", inspector); + let attr = editor.attrElements.get("style").querySelector(".editable"); + + attr.focus(); + EventUtils.sendKey("return", inspector.panelWin); + + let input = inplaceEditor(attr).input; + let value = input.value; + + is(value, + "style='background-image: url(\"moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F\");'", + "Value contains actual double quotes" + ); + + value = value.replace(/mozilla\.org/, "mozilla.com"); + input.value = value; + + EventUtils.sendKey("return", inspector.panelWin); + + yield onMutated; + + yield assertAttributes("#node26", { + id: "node26", + style: 'background-image: url("moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.com%2F");' + }, testActor); +} + +function* testEditingAttributeWithMixedQuotes(inspector, testActor) { + info("Modify class containing \" and \'"); + + yield assertAttributes("#node27", { + "id": "node27", + "class": 'Double " and single \'' + }, testActor); + + let onMutated = inspector.once("markupmutation"); + let {editor} = yield focusNode("#node27", inspector); + let attr = editor.attrElements.get("class").querySelector(".editable"); + + attr.focus(); + EventUtils.sendKey("return", inspector.panelWin); + + let input = inplaceEditor(attr).input; + let value = input.value; + + is(value, "class=\"Double " and single '\"", "Value contains ""); + + value = value.replace(/Double/, """).replace(/single/, "'"); + input.value = value; + + EventUtils.sendKey("return", inspector.panelWin); + + yield onMutated; + + yield assertAttributes("#node27", { + id: "node27", + class: '" " and \' \'' + }, testActor); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_09.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_09.js new file mode 100644 index 000000000..8680ab9f5 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_09.js @@ -0,0 +1,71 @@ +/* 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 editing a mixed-case attribute preserves the case + +const TEST_URL = URL_ROOT + "doc_markup_svg_attributes.html"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + yield inspector.markup.expandAll(); + yield selectNode("svg", inspector); + + yield testWellformedMixedCase(inspector, testActor); + yield testMalformedMixedCase(inspector, testActor); +}); + +function* testWellformedMixedCase(inspector, testActor) { + info("Modifying a mixed-case attribute, " + + "expecting the attribute's case to be preserved"); + + info("Listening to markup mutations"); + let onMutated = inspector.once("markupmutation"); + + info("Focusing the viewBox attribute editor"); + let {editor} = yield focusNode("svg", inspector); + let attr = editor.attrElements.get("viewBox").querySelector(".editable"); + attr.focus(); + EventUtils.sendKey("return", inspector.panelWin); + + info("Editing the attribute value and waiting for the mutation event"); + let input = inplaceEditor(attr).input; + input.value = "viewBox=\"0 0 1 1\""; + EventUtils.sendKey("return", inspector.panelWin); + yield onMutated; + + yield assertAttributes("svg", { + "viewBox": "0 0 1 1", + "width": "200", + "height": "200" + }, testActor); +} + +function* testMalformedMixedCase(inspector, testActor) { + info("Modifying a malformed, mixed-case attribute, " + + "expecting the attribute's case to be preserved"); + + info("Listening to markup mutations"); + let onMutated = inspector.once("markupmutation"); + + info("Focusing the viewBox attribute editor"); + let {editor} = yield focusNode("svg", inspector); + let attr = editor.attrElements.get("viewBox").querySelector(".editable"); + attr.focus(); + EventUtils.sendKey("return", inspector.panelWin); + + info("Editing the attribute value and waiting for the mutation event"); + let input = inplaceEditor(attr).input; + input.value = "viewBox=\"<>\""; + EventUtils.sendKey("return", inspector.panelWin); + yield onMutated; + + yield assertAttributes("svg", { + "viewBox": "<>", + "width": "200", + "height": "200" + }, testActor); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_10.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_10.js new file mode 100644 index 000000000..8d27c5468 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_10.js @@ -0,0 +1,34 @@ +/* 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"; + +// Tests that invalid tagname updates are handled correctly + +const TEST_URL = "data:text/html;charset=utf-8,<div></div>"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + yield inspector.markup.expandAll(); + + info("Updating the DIV tagname to an invalid value"); + let container = yield focusNode("div", inspector); + let onCancelReselect = inspector.markup.once("canceledreselectonremoved"); + let tagEditor = container.editor.tag; + setEditableFieldValue(tagEditor, "<<<", inspector); + yield onCancelReselect; + ok(true, "The markup-view emitted the canceledreselectonremoved event"); + is(inspector.selection.nodeFront, container.node, + "The test DIV is still selected"); + + info("Updating the DIV tagname to a valid value this time"); + let onReselect = inspector.markup.once("reselectedonremoved"); + setEditableFieldValue(tagEditor, "span", inspector); + yield onReselect; + ok(true, "The markup-view emitted the reselectedonremoved event"); + + let spanFront = yield getNodeFront("span", inspector); + is(inspector.selection.nodeFront, spanFront, + "The selected node is now the SPAN"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_11.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_11.js new file mode 100644 index 000000000..906c4aced --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_11.js @@ -0,0 +1,38 @@ +/* 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"; + +// Bug 1090874 - Tests that a node is not recreated when it's tagname editor +// is blurred and no changes were done. + +const TEST_URL = "data:text/html;charset=utf-8,<div></div>"; + +add_task(function* () { + let isEditTagNameCalled = false; + + let {inspector} = yield openInspectorForURL(TEST_URL); + + // Overriding the editTagName walkerActor method here to check that it isn't + // called when blurring the tagname field. + inspector.walker.editTagName = function () { + isEditTagNameCalled = true; + }; + + let container = yield focusNode("div", inspector); + let tagEditor = container.editor.tag; + + info("Blurring the tagname field"); + tagEditor.blur(); + is(isEditTagNameCalled, false, "The editTagName method wasn't called"); + + info("Updating the tagname to uppercase"); + yield focusNode("div", inspector); + setEditableFieldValue(tagEditor, "DIV", inspector); + is(isEditTagNameCalled, false, "The editTagName method wasn't called"); + + info("Updating the tagname to a different value"); + setEditableFieldValue(tagEditor, "SPAN", inspector); + is(isEditTagNameCalled, true, "The editTagName method was called"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_12.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_12.js new file mode 100644 index 000000000..4fcf3dd66 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_12.js @@ -0,0 +1,98 @@ +/* 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"; + +// Tests that focus position is correct when tabbing through and editing +// attributes. + +const TEST_URL = "data:text/html;charset=utf8," + + "<div id='attr' a='1' b='2' c='3'></div>" + + "<div id='delattr' tobeinvalid='1' last='2'></div>"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + yield testAttributeEditing(inspector); + yield testAttributeDeletion(inspector); +}); + +function* testAttributeEditing(inspector) { + info("Testing focus position after attribute editing"); + + info("Setting the first non-id attribute in edit mode"); + // focuses id + yield activateFirstAttribute("#attr", inspector); + // focuses the first attr after id + collapseSelectionAndTab(inspector); + + let attrs = yield getAttributesFromEditor("#attr", inspector); + + info("Editing this attribute, keeping the same name, " + + "and tabbing to the next"); + yield editAttributeAndTab(attrs[1] + '="99"', inspector); + checkFocusedAttribute(attrs[2], true); + + info("Editing the new focused attribute, keeping the name, " + + "and tabbing to the previous"); + yield editAttributeAndTab(attrs[2] + '="99"', inspector, true); + checkFocusedAttribute(attrs[1], true); + + info("Editing attribute name, changes attribute order"); + yield editAttributeAndTab("d='4'", inspector); + checkFocusedAttribute("id", true); + + // Escape of the currently focused field for the next test + EventUtils.sendKey("escape", inspector.panelWin); +} + +function* testAttributeDeletion(inspector) { + info("Testing focus position after attribute deletion"); + + info("Setting the first non-id attribute in edit mode"); + // focuses id + yield activateFirstAttribute("#delattr", inspector); + // focuses the first attr after id + collapseSelectionAndTab(inspector); + + let attrs = yield getAttributesFromEditor("#delattr", inspector); + + info("Entering an invalid attribute to delete the attribute"); + yield editAttributeAndTab('"', inspector); + checkFocusedAttribute(attrs[2], true); + + info("Deleting the last attribute"); + yield editAttributeAndTab(" ", inspector); + + // Check we're on the newattr element + let focusedAttr = Services.focus.focusedElement; + ok(focusedAttr.classList.contains("styleinspector-propertyeditor"), + "in newattr"); + is(focusedAttr.tagName, "textarea", "newattr is active"); +} + +function* editAttributeAndTab(newValue, inspector, goPrevious) { + let onEditMutation = inspector.markup.once("refocusedonedit"); + inspector.markup.doc.activeElement.value = newValue; + if (goPrevious) { + EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, + inspector.panelWin); + } else { + EventUtils.sendKey("tab", inspector.panelWin); + } + yield onEditMutation; +} + +/** + * Given a markup container, focus and turn in edit mode its first attribute + * field. + */ +function* activateFirstAttribute(container, inspector) { + let {editor} = yield focusNode(container, inspector); + editor.tag.focus(); + + // Go to "id" attribute and trigger edit mode. + EventUtils.sendKey("tab", inspector.panelWin); + EventUtils.sendKey("return", inspector.panelWin); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_13-other.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_13-other.js new file mode 100644 index 000000000..188e12cbc --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_13-other.js @@ -0,0 +1,38 @@ +/* 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"; + +// Tests that doesn't fit into any specific category. + +const TEST_URL = `data:text/html;charset=utf8, + <div a b id='order' c class></div>`; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + yield testOriginalAttributesOrder(inspector); + yield testOrderAfterAttributeChange(inspector, testActor); +}); + +function* testOriginalAttributesOrder(inspector) { + info("Testing order of attributes on initial node render"); + + let attributes = yield getAttributesFromEditor("#order", inspector); + ok(isEqual(attributes, ["id", "class", "a", "b", "c"]), "ordered correctly"); +} + +function* testOrderAfterAttributeChange(inspector, testActor) { + info("Testing order of attributes after attribute is change by setAttribute"); + + yield testActor.setAttribute("#order", "a", "changed"); + + let attributes = yield getAttributesFromEditor("#order", inspector); + ok(isEqual(attributes, ["id", "class", "a", "b", "c"]), + "order isn't changed"); +} + +function isEqual(a, b) { + return a.toString() === b.toString(); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_tag_edit_long-classname.js b/devtools/client/inspector/markup/test/browser_markup_tag_edit_long-classname.js new file mode 100644 index 000000000..7615ed691 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_long-classname.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 editing long classnames shows the whole class attribute without scrollbars. + +const classname = "this-long-class-attribute-should-be-displayed " + + "without-overflow-when-switching-to-edit-mode " + + "AAAAAAAAAAAA-BBBBBBBBBBBBB-CCCCCCCCCCCCC-DDDDDDDDDDDDDD-EEEEEEEEEEEEE"; +const TEST_URL = `data:text/html;charset=utf8, <div class="${classname}"></div>`; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + yield selectNode("div", inspector); + yield clickContainer("div", inspector); + + let container = yield focusNode("div", inspector); + ok(container && container.editor, "The markup-container was found"); + + info("Listening for the markupmutation event"); + let nodeMutated = inspector.once("markupmutation"); + let attr = container.editor.attrElements.get("class").querySelector(".editable"); + + attr.focus(); + EventUtils.sendKey("return", inspector.panelWin); + let input = inplaceEditor(attr).input; + ok(input, "Found editable field for class attribute"); + + is(input.scrollHeight, input.clientHeight, "input should not have vertical scrollbars"); + is(input.scrollWidth, input.clientWidth, "input should not have horizontal scrollbars"); + input.value = "class=\"other value\""; + + info("Commit the new class value"); + EventUtils.sendKey("return", inspector.panelWin); + + info("Wait for the markup-mutation event"); + yield nodeMutated; +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_textcontent_display.js b/devtools/client/inspector/markup/test/browser_markup_textcontent_display.js new file mode 100644 index 000000000..7513e4e18 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_textcontent_display.js @@ -0,0 +1,89 @@ +/* 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 the rendering of text nodes in the markup view. + +const LONG_VALUE = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do " + + "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam."; +const SCHEMA = "data:text/html;charset=UTF-8,"; +const TEST_URL = `${SCHEMA}<!DOCTYPE html> + <html> + <body> + <div id="shorttext">Short text</div> + <div id="longtext">${LONG_VALUE}</div> + <div id="shortcomment"><!--Short comment--></div> + <div id="longcomment"><!--${LONG_VALUE}--></div> + <div id="shorttext-and-node">Short text<span>Other element</span></div> + <div id="longtext-and-node">${LONG_VALUE}<span>Other element</span></div> + </body> + </html>`; + +const TEST_DATA = [{ + desc: "Test node containing a short text, short text nodes can be inlined.", + selector: "#shorttext", + inline: true, + value: "Short text", +}, { + desc: "Test node containing a long text, long text nodes are not inlined.", + selector: "#longtext", + inline: false, + value: LONG_VALUE, +}, { + desc: "Test node containing a short comment, comments are not inlined.", + selector: "#shortcomment", + inline: false, + value: "Short comment", +}, { + desc: "Test node containing a long comment, comments are not inlined.", + selector: "#longcomment", + inline: false, + value: LONG_VALUE, +}, { + desc: "Test node containing a short text and a span.", + selector: "#shorttext-and-node", + inline: false, + value: "Short text", +}, { + desc: "Test node containing a long text and a span.", + selector: "#longtext-and-node", + inline: false, + value: LONG_VALUE, +}]; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + for (let data of TEST_DATA) { + yield checkNode(inspector, testActor, data); + } +}); + +function* checkNode(inspector, testActor, {desc, selector, inline, value}) { + info(desc); + + let container = yield getContainerForSelector(selector, inspector); + let nodeValue = yield getFirstChildNodeValue(selector, testActor); + is(nodeValue, value, "The test node's text content is correct"); + + is(!!container.inlineTextChild, inline, "Container inlineTextChild is as expected"); + is(!container.canExpand, inline, "Container canExpand property is as expected"); + + let textContainer; + if (inline) { + textContainer = container.elt.querySelector("pre"); + ok(!!textContainer, "Text container is already rendered for inline text elements"); + } else { + textContainer = container.elt.querySelector("pre"); + ok(!textContainer, "Text container is not rendered for collapsed text nodes"); + yield inspector.markup.expandNode(container.node); + yield waitForMultipleChildrenUpdates(inspector); + + textContainer = container.elt.querySelector("pre"); + ok(!!textContainer, "Text container is rendered after expanding the container"); + } + + is(textContainer.textContent, value, "The complete text node is rendered."); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js b/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js new file mode 100644 index 000000000..f27b56647 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js @@ -0,0 +1,84 @@ +/* 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 editing a node's text content + +const TEST_URL = URL_ROOT + "doc_markup_edit.html"; +const {DEFAULT_VALUE_SUMMARY_LENGTH} = require("devtools/server/actors/inspector"); + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + info("Expanding all nodes"); + yield inspector.markup.expandAll(); + yield waitForMultipleChildrenUpdates(inspector); + + yield editContainer(inspector, testActor, { + selector: ".node6", + newValue: "New text", + oldValue: "line6" + }); + + yield editContainer(inspector, testActor, { + selector: "#node17", + newValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " + + "DONEC POSUERE PLACERAT MAGNA ET IMPERDIET.", + oldValue: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "Donec posuere placerat magna et imperdiet." + }); + + yield editContainer(inspector, testActor, { + selector: "#node17", + newValue: "New value", + oldValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " + + "DONEC POSUERE PLACERAT MAGNA ET IMPERDIET." + }); + + yield editContainer(inspector, testActor, { + selector: "#node17", + newValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " + + "DONEC POSUERE PLACERAT MAGNA ET IMPERDIET.", + oldValue: "New value" + }); +}); + +function* editContainer(inspector, testActor, + {selector, newValue, oldValue}) { + let nodeValue = yield getFirstChildNodeValue(selector, testActor); + is(nodeValue, oldValue, "The test node's text content is correct"); + + info("Changing the text content"); + let onMutated = inspector.once("markupmutation"); + let container = yield focusNode(selector, inspector); + + let isOldValueInline = oldValue.length <= DEFAULT_VALUE_SUMMARY_LENGTH; + is(!!container.inlineTextChild, isOldValueInline, "inlineTextChild is as expected"); + is(!container.canExpand, isOldValueInline, "canExpand property is as expected"); + + let field = container.elt.querySelector("pre"); + is(field.textContent, oldValue, + "The text node has the correct original value after selecting"); + setEditableFieldValue(field, newValue, inspector); + + info("Listening to the markupmutation event"); + yield onMutated; + + nodeValue = yield getFirstChildNodeValue(selector, testActor); + is(nodeValue, newValue, "The test node's text content has changed"); + + let isNewValueInline = newValue.length <= DEFAULT_VALUE_SUMMARY_LENGTH; + is(!!container.inlineTextChild, isNewValueInline, "inlineTextChild is as expected"); + is(!container.canExpand, isNewValueInline, "canExpand property is as expected"); + + if (isOldValueInline != isNewValueInline) { + is(container.expanded, !isNewValueInline, + "Container was automatically expanded/collapsed"); + } + + info("Selecting the <body> to reset the selection"); + let bodyContainer = yield getContainerForSelector("body", inspector); + inspector.markup.markNodeAsSelected(bodyContainer.node); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_02.js b/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_02.js new file mode 100644 index 000000000..04825f2d4 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_02.js @@ -0,0 +1,116 @@ +/* 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 using UP/DOWN next to a number when editing a text node does not +// increment or decrement but simply navigates inside the editable field. + +const TEST_URL = URL_ROOT + "doc_markup_edit.html"; +const SELECTOR = ".node6"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + info("Expanding all nodes"); + yield inspector.markup.expandAll(); + yield waitForMultipleChildrenUpdates(inspector); + + let nodeValue = yield getFirstChildNodeValue(SELECTOR, testActor); + let expectedValue = "line6"; + is(nodeValue, expectedValue, "The test node's text content is correct"); + + info("Open editable field for .node6"); + let container = yield focusNode(SELECTOR, inspector); + let field = container.elt.querySelector("pre"); + field.focus(); + EventUtils.sendKey("return", inspector.panelWin); + let editor = inplaceEditor(field); + + info("Initially, all the input content should be selected"); + checkSelectionPositions(editor, 0, expectedValue.length); + + info("Navigate using 'RIGHT': move the caret to the end"); + yield sendKey("VK_RIGHT", {}, editor, inspector.panelWin); + is(editor.input.value, expectedValue, "Value should not have changed"); + checkSelectionPositions(editor, expectedValue.length, expectedValue.length); + + info("Navigate using 'DOWN': no effect, already at the end"); + yield sendKey("VK_DOWN", {}, editor, inspector.panelWin); + is(editor.input.value, expectedValue, "Value should not have changed"); + checkSelectionPositions(editor, expectedValue.length, expectedValue.length); + + info("Navigate using 'UP': move to the start"); + yield sendKey("VK_UP", {}, editor, inspector.panelWin); + is(editor.input.value, expectedValue, "Value should not have changed"); + checkSelectionPositions(editor, 0, 0); + + info("Navigate using 'DOWN': move to the end"); + yield sendKey("VK_DOWN", {}, editor, inspector.panelWin); + is(editor.input.value, expectedValue, "Value should not have changed"); + checkSelectionPositions(editor, expectedValue.length, expectedValue.length); + + info("Type 'b' in the editable field"); + yield sendKey("b", {}, editor, inspector.panelWin); + expectedValue += "b"; + is(editor.input.value, expectedValue, "Value should be updated"); + + info("Type 'a' in the editable field"); + yield sendKey("a", {}, editor, inspector.panelWin); + expectedValue += "a"; + is(editor.input.value, expectedValue, "Value should be updated"); + + info("Create a new line using shift+RETURN"); + yield sendKey("VK_RETURN", {shiftKey: true}, editor, inspector.panelWin); + expectedValue += "\n"; + is(editor.input.value, expectedValue, "Value should have a new line"); + checkSelectionPositions(editor, expectedValue.length, expectedValue.length); + + info("Type '1' in the editable field"); + yield sendKey("1", {}, editor, inspector.panelWin); + expectedValue += "1"; + is(editor.input.value, expectedValue, "Value should be updated"); + checkSelectionPositions(editor, expectedValue.length, expectedValue.length); + + info("Navigate using 'UP': move back to the first line"); + yield sendKey("VK_UP", {}, editor, inspector.panelWin); + is(editor.input.value, expectedValue, "Value should not have changed"); + info("Caret should be back on the first line"); + checkSelectionPositions(editor, 1, 1); + + info("Commit the new value with RETURN, wait for the markupmutation event"); + let onMutated = inspector.once("markupmutation"); + yield sendKey("VK_RETURN", {}, editor, inspector.panelWin); + yield onMutated; + + nodeValue = yield getFirstChildNodeValue(SELECTOR, testActor); + is(nodeValue, expectedValue, "The test node's text content is correct"); +}); + +/** + * Check that the editor selection is at the expected positions. + */ +function checkSelectionPositions(editor, expectedStart, expectedEnd) { + is(editor.input.selectionStart, expectedStart, + "Selection should start at " + expectedStart); + is(editor.input.selectionEnd, expectedEnd, + "Selection should end at " + expectedEnd); +} + +/** + * Send a key and expect to receive a keypress event on the editor's input. + */ +function sendKey(key, options, editor, win) { + return new Promise(resolve => { + info("Adding event listener for down|left|right|back_space|return keys"); + editor.input.addEventListener("keypress", function onKeypress() { + if (editor.input) { + editor.input.removeEventListener("keypress", onKeypress); + } + executeSoon(resolve); + }); + + EventUtils.synthesizeKey(key, options, win); + }); +} diff --git a/devtools/client/inspector/markup/test/browser_markup_toggle_01.js b/devtools/client/inspector/markup/test/browser_markup_toggle_01.js new file mode 100644 index 000000000..a481f685e --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_toggle_01.js @@ -0,0 +1,58 @@ +/* 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 toggling (expand/collapse) elements by clicking on twisties + +const TEST_URL = URL_ROOT + "doc_markup_toggle.html"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + info("Getting the container for the html element"); + let container = yield getContainerForSelector("html", inspector); + ok(container.mustExpand, "HTML element mustExpand"); + ok(container.canExpand, "HTML element canExpand"); + is(container.expander.style.visibility, "hidden", "HTML twisty is hidden"); + + info("Getting the container for the UL parent element"); + container = yield getContainerForSelector("ul", inspector); + ok(!container.mustExpand, "UL element !mustExpand"); + ok(container.canExpand, "UL element canExpand"); + is(container.expander.style.visibility, "visible", "HTML twisty is visible"); + + info("Clicking on the UL parent expander, and waiting for children"); + let onChildren = waitForChildrenUpdated(inspector); + let onUpdated = inspector.once("inspector-updated"); + EventUtils.synthesizeMouseAtCenter(container.expander, {}, + inspector.markup.doc.defaultView); + yield onChildren; + yield onUpdated; + + info("Checking that child LI elements have been created"); + let numLi = yield testActor.getNumberOfElementMatches("li"); + for (let i = 0; i < numLi; i++) { + let liContainer = yield getContainerForSelector( + `li:nth-child(${i + 1})`, inspector); + ok(liContainer, "A container for the child LI element was created"); + } + ok(container.expanded, "Parent UL container is expanded"); + + info("Clicking again on the UL expander"); + // No need to wait, this is a local, synchronous operation where nodes are + // only hidden from the view, not destroyed + EventUtils.synthesizeMouseAtCenter(container.expander, {}, + inspector.markup.doc.defaultView); + + info("Checking that child LI elements have been hidden"); + numLi = yield testActor.getNumberOfElementMatches("li"); + for (let i = 0; i < numLi; i++) { + let liContainer = yield getContainerForSelector( + `li:nth-child(${i + 1})`, inspector); + is(liContainer.elt.getClientRects().length, 0, + "The container for the child LI element was hidden"); + } + ok(!container.expanded, "Parent UL container is collapsed"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_toggle_02.js b/devtools/client/inspector/markup/test/browser_markup_toggle_02.js new file mode 100644 index 000000000..481f0bf58 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_toggle_02.js @@ -0,0 +1,49 @@ +/* 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 toggling (expand/collapse) elements by dbl-clicking on tag lines + +const TEST_URL = URL_ROOT + "doc_markup_toggle.html"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + info("Getting the container for the UL parent element"); + let container = yield getContainerForSelector("ul", inspector); + + info("Dbl-clicking on the UL parent expander, and waiting for children"); + let onChildren = waitForChildrenUpdated(inspector); + let onUpdated = inspector.once("inspector-updated"); + EventUtils.synthesizeMouseAtCenter(container.tagLine, {clickCount: 2}, + inspector.markup.doc.defaultView); + yield onChildren; + yield onUpdated; + + info("Checking that child LI elements have been created"); + let numLi = yield testActor.getNumberOfElementMatches("li"); + for (let i = 0; i < numLi; i++) { + let liContainer = yield getContainerForSelector( + "li:nth-child(" + (i + 1) + ")", inspector); + ok(liContainer, "A container for the child LI element was created"); + } + ok(container.expanded, "Parent UL container is expanded"); + + info("Dbl-clicking again on the UL expander"); + // No need to wait, this is a local, synchronous operation where nodes are + // only hidden from the view, not destroyed + EventUtils.synthesizeMouseAtCenter(container.tagLine, {clickCount: 2}, + inspector.markup.doc.defaultView); + + info("Checking that child LI elements have been hidden"); + numLi = yield testActor.getNumberOfElementMatches("li"); + for (let i = 0; i < numLi; i++) { + let liContainer = yield getContainerForSelector( + "li:nth-child(" + (i + 1) + ")", inspector); + is(liContainer.elt.getClientRects().length, 0, + "The container for the child LI element was hidden"); + } + ok(!container.expanded, "Parent UL container is collapsed"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_toggle_03.js b/devtools/client/inspector/markup/test/browser_markup_toggle_03.js new file mode 100644 index 000000000..fb3529c8e --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_toggle_03.js @@ -0,0 +1,35 @@ +/* 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 toggling (expand/collapse) elements by alt-clicking on twisties, which +// should expand all the descendants + +const TEST_URL = URL_ROOT + "doc_markup_toggle.html"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + info("Getting the container for the UL parent element"); + let container = yield getContainerForSelector("ul", inspector); + + info("Alt-clicking on the UL parent expander, and waiting for children"); + let onUpdated = inspector.once("inspector-updated"); + EventUtils.synthesizeMouseAtCenter(container.expander, {altKey: true}, + inspector.markup.doc.defaultView); + yield onUpdated; + yield waitForMultipleChildrenUpdates(inspector); + + info("Checking that all nodes exist and are expanded"); + let nodeList = yield inspector.walker.querySelectorAll( + inspector.walker.rootNode, "ul, li, span, em"); + let nodeFronts = yield nodeList.items(); + for (let nodeFront of nodeFronts) { + let nodeContainer = getContainerForNodeFront(nodeFront, inspector); + ok(nodeContainer, "Container for node " + nodeFront.tagName + " exists"); + ok(nodeContainer.expanded, + "Container for node " + nodeFront.tagName + " is expanded"); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_update-on-navigtion.js b/devtools/client/inspector/markup/test/browser_markup_update-on-navigtion.js new file mode 100644 index 000000000..241cea672 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_update-on-navigtion.js @@ -0,0 +1,44 @@ +/* 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 markup view handles page navigation correctly. + +const SCHEMA = "data:text/html;charset=UTF-8,"; +const URL_1 = SCHEMA + "<div id='one' style='color:red;'>ONE</div>"; +const URL_2 = SCHEMA + "<div id='two' style='color:green;'>TWO</div>"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(URL_1); + + assertMarkupViewIsLoaded(); + yield selectNode("#one", inspector); + + let willNavigate = inspector.target.once("will-navigate"); + yield testActor.eval(`content.location = "${URL_2}"`); + + info("Waiting for will-navigate"); + yield willNavigate; + + info("Navigation to page 2 has started, the inspector should be empty"); + assertMarkupViewIsEmpty(); + + info("Waiting for new-root"); + yield inspector.once("new-root"); + + info("Navigation to page 2 was done, the inspector should be back up"); + assertMarkupViewIsLoaded(); + + yield selectNode("#two", inspector); + + function assertMarkupViewIsLoaded() { + let markupViewBox = inspector.panelDoc.getElementById("markup-box"); + is(markupViewBox.childNodes.length, 1, "The markup-view is loaded"); + } + + function assertMarkupViewIsEmpty() { + let markupViewBox = inspector.panelDoc.getElementById("markup-box"); + is(markupViewBox.childNodes.length, 0, "The markup-view is unloaded"); + } +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_void_elements_html.js b/devtools/client/inspector/markup/test/browser_markup_void_elements_html.js new file mode 100644 index 000000000..60330a144 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_void_elements_html.js @@ -0,0 +1,44 @@ +/* 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 void element display in the markupview. +const TEST_URL = URL_ROOT + "doc_markup_void_elements.html"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + let {win} = inspector.markup; + + info("check non-void element closing tag is displayed"); + let {editor} = yield getContainerForSelector("h1", inspector); + ok(!editor.elt.classList.contains("void-element"), + "h1 element does not have void-element class"); + ok(!editor.elt.querySelector(".close").style.display !== "none", + "h1 element tag is not hidden"); + + info("check void element closing tag is hidden in HTML document"); + let container = yield getContainerForSelector("img", inspector); + ok(container.editor.elt.classList.contains("void-element"), + "img element has the expected class"); + let closeElement = container.editor.elt.querySelector(".close"); + let computedStyle = win.getComputedStyle(closeElement, null); + ok(computedStyle.display === "none", "img closing tag is hidden"); + + info("check void element with pseudo element"); + let hrNodeFront = yield getNodeFront("hr.before", inspector); + container = getContainerForNodeFront(hrNodeFront, inspector); + ok(container.editor.elt.classList.contains("void-element"), + "hr element has the expected class"); + closeElement = container.editor.elt.querySelector(".close"); + computedStyle = win.getComputedStyle(closeElement, null); + ok(computedStyle.display === "none", "hr closing tag is hidden"); + + info("check expanded void element closing tag is not hidden"); + yield inspector.markup.expandNode(hrNodeFront); + yield waitForMultipleChildrenUpdates(inspector); + ok(container.expanded, "hr container is expanded"); + computedStyle = win.getComputedStyle(closeElement, null); + ok(computedStyle.display === "none", "hr closing tag is not hidden anymore"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_void_elements_xhtml.js b/devtools/client/inspector/markup/test/browser_markup_void_elements_xhtml.js new file mode 100644 index 000000000..0cccf54d4 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_void_elements_xhtml.js @@ -0,0 +1,28 @@ +/* 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 void element display in the markupview. +const TEST_URL = URL_ROOT + "doc_markup_void_elements.xhtml"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + let {win} = inspector.markup; + + info("check non-void element closing tag is displayed"); + let {editor} = yield getContainerForSelector("h1", inspector); + ok(!editor.elt.classList.contains("void-element"), + "h1 element does not have void-element class"); + ok(!editor.elt.querySelector(".close").style.display !== "none", + "h1 element tag is not hidden"); + + info("check void element closing tag is not hidden in XHTML document"); + let container = yield getContainerForSelector("br", inspector); + ok(!container.editor.elt.classList.contains("void-element"), + "br element does not have void-element class"); + let closeElement = container.editor.elt.querySelector(".close"); + let computedStyle = win.getComputedStyle(closeElement, null); + ok(computedStyle.display !== "none", "br closing tag is not hidden"); +}); diff --git a/devtools/client/inspector/markup/test/browser_markup_whitespace.js b/devtools/client/inspector/markup/test/browser_markup_whitespace.js new file mode 100644 index 000000000..63a0d0467 --- /dev/null +++ b/devtools/client/inspector/markup/test/browser_markup_whitespace.js @@ -0,0 +1,66 @@ +/* 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 whitespace text nodes do show up in the markup-view when needed. + +const TEST_URL = URL_ROOT + "doc_markup_whitespace.html"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + let {markup} = inspector; + + yield markup.expandAll(); + + info("Verify the number of child nodes and child elements in body"); + + // Body has 5 element children, but there are 6 text nodes in there too, they come from + // the HTML file formatting (spaces and carriage returns). + let {numNodes, numChildren} = yield testActor.getNodeInfo("body"); + is(numNodes, 11, "The body node has 11 child nodes (includes text nodes)"); + is(numChildren, 5, "The body node has 5 child elements (only element nodes)"); + + // In body, there are only block-level elements, so whitespace text nodes do not have + // layout, so they should be skipped in the markup-view. + info("Check that the body's whitespace text node children aren't shown"); + let bodyContainer = markup.getContainer(inspector.selection.nodeFront); + let childContainers = bodyContainer.getChildContainers(); + is(childContainers.length, 5, + "Only the element nodes are shown in the markup view"); + + // div#inline has 3 element children, but there are 4 text nodes in there too, like in + // body, they come from spaces and carriage returns in the HTML file. + info("Verify the number of child nodes and child elements in div#inline"); + ({numNodes, numChildren} = yield testActor.getNodeInfo("#inline")); + is(numNodes, 7, "The div#inline node has 7 child nodes (includes text nodes)"); + is(numChildren, 3, "The div#inline node has 3 child elements (only element nodes)"); + + // Within the inline formatting context in div#inline, the whitespace text nodes between + // the images have layout, so they should appear in the markup-view. + info("Check that the div#inline's whitespace text node children are shown"); + yield selectNode("#inline", inspector); + let divContainer = markup.getContainer(inspector.selection.nodeFront); + childContainers = divContainer.getChildContainers(); + is(childContainers.length, 5, + "Both the element nodes and some text nodes are shown in the markup view"); + + // div#pre has 2 element children, but there are 3 text nodes in there too, like in + // div#inline, they come from spaces and carriage returns in the HTML file. + info("Verify the number of child nodes and child elements in div#pre"); + ({numNodes, numChildren} = yield testActor.getNodeInfo("#pre")); + is(numNodes, 5, "The div#pre node has 5 child nodes (includes text nodes)"); + is(numChildren, 2, "The div#pre node has 2 child elements (only element nodes)"); + + // Within the inline formatting context in div#pre, the whitespace text nodes between + // the images have layout, so they should appear in the markup-view, but since + // white-space is set to pre, then the whitespace text nodes before and after the first + // and last image should also appear. + info("Check that the div#pre's whitespace text node children are shown"); + yield selectNode("#pre", inspector); + divContainer = markup.getContainer(inspector.selection.nodeFront); + childContainers = divContainer.getChildContainers(); + is(childContainers.length, 5, + "Both the element nodes and all text nodes are shown in the markup view"); +}); diff --git a/devtools/client/inspector/markup/test/doc_markup_anonymous.html b/devtools/client/inspector/markup/test/doc_markup_anonymous.html new file mode 100644 index 000000000..0ede3ca5f --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_anonymous.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Anonymous content test</title> + <style type="text/css"> + #pseudo::before { + content: "before"; + } + #pseudo::after { + content: "after"; + } + #shadow::before { + content: "Testing ::before on a shadow host"; + } + </style> +</head> +<body> + <div id="pseudo"><span>middle</span></div> + + <div id="shadow">light dom</div> + + <div id="native"><video controls></video></div> + + <script> + "use strict"; + var host = document.querySelector("#shadow"); + if (host.createShadowRoot) { + var root = host.createShadowRoot(); + root.innerHTML = "<h3>Shadow DOM</h3><select multiple></select>"; + } + </script> +</body> +</html>
\ No newline at end of file diff --git a/devtools/client/inspector/markup/test/doc_markup_dragdrop.html b/devtools/client/inspector/markup/test/doc_markup_dragdrop.html new file mode 100644 index 000000000..f45c26065 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_dragdrop.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=858038 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 858038</title> + <style> + #test::before { + content: 'This should not be draggable'; + } + </style> +</head> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=858038">Mozilla Bug 858038</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <input id="anonymousParent" /><span id="before">Before<!-- Force not-inline --></span> + <pre id="test"><span id="firstChild">First</span><span id="middleChild">Middle</span><span id="lastChild">Last</span></pre> <span id="after">After</span> +</body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_01.html b/devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_01.html new file mode 100644 index 000000000..35f3b5f31 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_01.html @@ -0,0 +1,87 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=858038 +https://bugzilla.mozilla.org/show_bug.cgi?id=1226898 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 858038 and 1226898 - Autoscroll</title> +</head> +<body> + <div id="first"></div> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=858038">Mozilla Bug 858038</a> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1226898">Mozilla Bug 1226898</a> + <p id="display">Test</p> + <div id="content" style="display: none"> + + </div> + + <!-- Make sure the markup-view has enough nodes shown by default that it has a scrollbar --> + + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> +</body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_02.html b/devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_02.html new file mode 100644 index 000000000..9e4d92cf3 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_02.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=858038 +https://bugzilla.mozilla.org/show_bug.cgi?id=1226898 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 858038 and 1226898 - Autoscroll</title> +</head> +<body> + <div id="first"></div> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=858038">Mozilla Bug 858038</a> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1226898">Mozilla Bug 1226898</a> + <p id="display">Test</p> + <div id="content" style="display: none"> + + </div> + + <!-- Make sure the markup-view has enough nodes shown by default that it has a scrollbar --> + + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> +</body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_edit.html b/devtools/client/inspector/markup/test/doc_markup_edit.html new file mode 100644 index 000000000..ddefd1d87 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_edit.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> + +<html class="html"> + + <body class="body"> + <div class="node0"> + <div id="node1" class="node1">line1</div> + <div id="node2" class="node2">line2</div> + <p class="node3">line3</p> + <!-- A comment --> + <p id="node4" class="node4">line4 + <span class="node5">line5</span> + <span class="node6">line6</span> + <!-- A comment --> + <a class="node7">line7<span class="node8">line8</span></a> + <span class="node9">line9</span> + <span class="node10">line10</span> + <span class="node11">line11</span> + <a class="node12">line12<span class="node13">line13</span></a> + </p> + <p id="node14">line14</p> + <p class="node15">line15</p> + </div> + <div id="node16"> + <p id="node17">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec posuere placerat magna et imperdiet.</p> + </div> + <div id="node18"> + <div id="node19"> + <div id="node20"> + <div id="node21"> + line21 + </div> + </div> + </div> + </div> + <div id="node22" class="unchanged"></div> + <div id="node23"></div> + <div id="node24"></div> + <div id="retag-me"> + <div id="retag-me-2"></div> + </div> + <div id="node25"></div> + <div id="node26" style='background-image: url("moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F");'></div> + <div id="node27" class="Double " and single '"></div> + <img id="node-data-url" /> + <div id="node-data-url-style"></div> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_events-overflow.html b/devtools/client/inspector/markup/test/doc_markup_events-overflow.html new file mode 100644 index 000000000..d604245fe --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_events-overflow.html @@ -0,0 +1,19 @@ +<html> +<head> + <meta charset="UTF-8"> + <title>doc_markup_events-overflow.html</title> +</head> +<body> + <h1>doc_markup_events-overflow.html</h1> + <span id="events">Inspect me!</span> + <script> + "use strict"; + var el = document.getElementById("events"); + for (var i = 50; i > 0; i--) { + el.addEventListener("click", function onClick() { + alert("click"); + }); + } + </script> +</body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_events1.html b/devtools/client/inspector/markup/test/doc_markup_events1.html new file mode 100644 index 000000000..0955289e2 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_events1.html @@ -0,0 +1,113 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + #container { + border: 1px solid #000; + width: 200px; + height: 85px; + } + + #container > div { + border: 1px solid #000; + display: inline-block; + margin: 2px; + } + + #output, + #noevents, + #DOM0, + #handleevent, + #output, + #noevents { + cursor: auto; + } + + #output { + min-height: 1.5em; + } + </style> + <script type="application/javascript;version=1.8"> + function init() { + let container = document.getElementById("container"); + let multiple = document.getElementById("multiple"); + + container.addEventListener("mouseover", mouseoverHandler, true); + multiple.addEventListener("click", clickHandler, false); + multiple.addEventListener("mouseup", mouseupHandler, false); + + let he = new handleEventClick(); + let handleevent = document.getElementById("handleevent"); + handleevent.addEventListener("click", he); + } + + function mouseoverHandler(event) { + if (event.target.id !== "container") { + let output = document.getElementById("output"); + output.textContent = event.target.textContent; + } + } + + function clickHandler(event) { + let output = document.getElementById("output"); + output.textContent = "click"; + } + + function mouseupHandler(event) { + let output = document.getElementById("output"); + output.textContent = "mouseup"; + } + + function handleEventClick(hehe) { + + } + + handleEventClick.prototype = { + handleEvent: function(blah) { + alert("handleEvent"); + } + }; + + function noeventsClickHandler(event) { + alert("noevents has an event listener"); + } + + function addNoeventsClickHandler() { + let noevents = document.getElementById("noevents"); + noevents.addEventListener("click", noeventsClickHandler); + } + + function removeNoeventsClickHandler() { + let noevents = document.getElementById("noevents"); + noevents.removeEventListener("click", noeventsClickHandler); + } + </script> + </head> + <body onload="init();"> + <h1>Events test 1</h1> + <div id="container"> + <div>1</div> + <div>2</div> + <div>3</div> + <div>4</div> + <div>5</div> + <div>6</div> + <div>7</div> + <div>8</div> + <div>9</div> + <div>10</div> + <div>11</div> + <div>12</div> + <div>13</div> + <div>14</div> + <div>15</div> + <div>16</div> + <div id="multiple">multiple</div> + </div> + <div id="output"></div> + <div id="noevents">noevents</div> + <div id="DOM0" onclick="alert('DOM0')">DOM0 event here</div> + <div id="handleevent">handleEvent</div> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_events2.html b/devtools/client/inspector/markup/test/doc_markup_events2.html new file mode 100644 index 000000000..ddc17537d --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_events2.html @@ -0,0 +1,111 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + #fatarrow, + #bound, + #boundhe, + #comment-inline, + #comment-streaming, + #anon-object-method, + #object-method { + border: 1px solid #000; + width: 200px; + min-height: 1em; + cursor: pointer; + } + </style> + <script type="application/javascript;version=1.8"> + function init() { + let fatarrow = document.getElementById("fatarrow"); + + let he = new handleEventClick(); + let anonObjectMethod = document.getElementById("anon-object-method"); + anonObjectMethod.addEventListener("click", he.anonObjectMethod); + + let objectMethod = document.getElementById("object-method"); + objectMethod.addEventListener("click", he.objectMethod); + + let bhe = new boundHandleEventClick(); + let boundheNode = document.getElementById("boundhe"); + bhe.handleEvent = bhe.handleEvent.bind(bhe); + boundheNode.addEventListener("click", bhe); + + let boundNode = document.getElementById("bound"); + boundClickHandler = boundClickHandler.bind(this); + boundNode.addEventListener("click", boundClickHandler); + + fatarrow.addEventListener("click", () => { + alert("Fat arrow without params!"); + }); + + fatarrow.addEventListener("click", event => { + alert("Fat arrow with 1 param!"); + }); + + fatarrow.addEventListener("click", (event, foo, bar) => { + alert("Fat arrow with 3 params!"); + }); + + fatarrow.addEventListener("click", b => b); + + let inlineCommentNode = document.getElementById("comment-inline"); + inlineCommentNode + .addEventListener("click", functionProceededByInlineComment); + + let streamingCommentNode = document.getElementById("comment-streaming"); + streamingCommentNode + .addEventListener("click", functionProceededByStreamingComment); + } + + function boundClickHandler(event) { + alert("Bound event"); + } + + function handleEventClick(hehe) { + + } + + handleEventClick.prototype = { + anonObjectMethod: function() { + alert("obj.anonObjectMethod"); + }, + + objectMethod: function kay() { + alert("obj.objectMethod"); + }, + }; + + function boundHandleEventClick() { + + } + + boundHandleEventClick.prototype = { + handleEvent: function() { + alert("boundHandleEvent"); + } + }; + + // A function proceeded with an inline comment + function functionProceededByInlineComment() { + alert("comment-inline"); + } + + /* A function proceeded with a streaming comment */ + function functionProceededByStreamingComment() { + alert("comment-streaming"); + } + </script> + </head> + <body onload="init();"> + <h1>Events test 2</h1> + <div id="fatarrow">Fat arrows</div> + <div id="boundhe">Bound handleEvent</div> + <div id="bound">Bound event</div> + <div id="comment-inline">Event proceeded by an inline comment</div> + <div id="comment-streaming">Event proceeded by a streaming comment</div> + <div id="anon-object-method">Anonymous object method</div> + <div id="object-method">Object method</div> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_events3.html b/devtools/client/inspector/markup/test/doc_markup_events3.html new file mode 100644 index 000000000..af4decc40 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_events3.html @@ -0,0 +1,115 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + #es6-method, + #generator, + #anon-generator, + #named-function-expression, + #anon-function-expression, + #returned-function, + #constructed-function, + #constructed-function-with-body-string, + #multiple-assignment { + border: 1px solid #000; + width: 200px; + min-height: 1em; + cursor: pointer; + } + </style> + <script type="application/javascript;version=1.8"> + let namedFunctionExpression = + function foo() { + alert("namedFunctionExpression"); + } + + let anonFunctionExpression = function() { + alert("anonFunctionExpression"); + }; + + let returnedFunction = (function() { + return function bar() { + alert("returnedFunction"); + } + })(); + + let constructedFunc = new Function(); + + let constructedFuncWithBodyString = + new Function('a', 'b', 'c', 'alert("constructedFuncWithBodyString");'); + + let multipleAssignment = foo = bar = function multi() { + alert("multipleAssignment"); + } + + function init() { + let he = new handleEventClick(); + let es6Method = document.getElementById("es6-method"); + es6Method.addEventListener("click", he.es6Method); + + let generatorNode = document.getElementById("generator"); + generatorNode.addEventListener("click", generator); + + let anonGenerator = document.getElementById("anon-generator"); + anonGenerator.addEventListener("click", function* () { + alert("anonGenerator"); + }); + + let namedFunctionExpressionNode = + document.getElementById("named-function-expression"); + namedFunctionExpressionNode.addEventListener("click", + namedFunctionExpression); + + let anonFunctionExpressionNode = + document.getElementById("anon-function-expression"); + anonFunctionExpressionNode.addEventListener("click", + anonFunctionExpression); + + let returnedFunctionNode = document.getElementById("returned-function"); + returnedFunctionNode.addEventListener("click", returnedFunction); + + let constructedFunctionNode = + document.getElementById("constructed-function"); + constructedFunctionNode.addEventListener("click", constructedFunc); + + let constructedFunctionWithBodyStringNode = + document.getElementById("constructed-function-with-body-string"); + constructedFunctionWithBodyStringNode + .addEventListener("click", constructedFuncWithBodyString); + + let multipleAssignmentNode = + document.getElementById("multiple-assignment"); + multipleAssignmentNode.addEventListener("click", multipleAssignment); + } + + function handleEventClick(hehe) { + + } + + handleEventClick.prototype = { + es6Method() { + alert("obj.es6Method"); + } + }; + + function* generator() { + alert("generator"); + } + </script> + </head> + <body onload="init();"> + <h1>Events test 3</h1> + <div id="es6-method">ES6 method</div> + <div id="generator">Generator</div> + <div id="anon-generator">Anonymous Generator</div> + <div id="named-function-expression">Named Function Expression</div> + <div id="anon-function-expression">Anonymous Function Expression</div> + <div id="returned-function">Returned Function</div> + <div id="constructed-function">Constructed Function</div> + <div id="constructed-function-with-body-string"> + Constructed Function with body string + </div> + <div id="multiple-assignment">Multiple Assignment</div> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_events_form.html b/devtools/client/inspector/markup/test/doc_markup_events_form.html new file mode 100644 index 000000000..b4ddff4aa --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_events_form.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + </style> + </head> + <body> + <div id="container"> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_events_jquery.html b/devtools/client/inspector/markup/test/doc_markup_events_jquery.html new file mode 100644 index 000000000..5f8caff27 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_events_jquery.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + + <style> + input { + margin: 5px 3px 10px 10px; + } + + div { + width: 100px; + height: 100px; + border: 1px solid #000; + } + </style> + + <script type="application/javascript;version=1.8"> + let jq = document.location.search.substr(1); + + let script = document.createElement("script"); + script.setAttribute("type", "text/javascript"); + script.setAttribute("src", jq); + + document.head.appendChild(script); + + window.addEventListener("load", () => { + var handler1 = function liveDivDblClick() { alert(1); }; + var handler2 = function liveDivDragStart() { alert(2); }; + var handler3 = function liveDivDragLeave() { alert(3); }; + var handler4 = function liveDivDragEnd() { alert(4); }; + var handler5 = function liveDivDrop() { alert(5); }; + var handler6 = function liveDivDragOver() { alert(6); }; + var handler7 = function divClick1() { alert(7); }; + var handler8 = function divClick2() { alert(8); }; + var handler9 = function divKeyDown() { alert(9); }; + var handler10 = function divDragOut() { alert(10); }; + + if ($("#livediv").live) { + $("#livediv").live( "dblclick", handler1); + $("#livediv").live( "dragstart", handler2); + } + + if ($("#livediv").delegate) { + $(document).delegate( "#livediv", "dragleave", handler3); + $(document).delegate( "#livediv", "dragend", handler4); + } + + if ($("#livediv").on) { + $(document).on( "drop", "#livediv", handler5); + $(document).on( "dragover", "#livediv", handler6); + $(document).on( "dragout", "#livediv:xxxxx", handler10); + } + + var div = $("div")[0]; + $(div).click(handler7); + $(div).click(handler8); + $(div).keydown(handler9); + }); + </script> + </head> + <body> + <div id="testdiv"></div> + <br> + <div id="livediv"></div> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_flashing.html b/devtools/client/inspector/markup/test/doc_markup_flashing.html new file mode 100644 index 000000000..3bb8cf1d2 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_flashing.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>mutation flashing test</title> +</head> +<body> + <div id="root"> + <ul class="list"> + <li class="item">item</li> + <li class="item">item</li> + </ul> + </div> +</body> +</html>
\ No newline at end of file diff --git a/devtools/client/inspector/markup/test/doc_markup_html_mixed_case.html b/devtools/client/inspector/markup/test/doc_markup_html_mixed_case.html new file mode 100644 index 000000000..ab26005e1 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_html_mixed_case.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> + <body> + <svg viewBox="0 0 2 2" width=200 height=200> + <clipPath> + <rect x=0 y=0 width=1 height=1 /> + </clipPath> + <circle cx=1 cy=1 r=1 fill=lime /> + </svg> + <DIV></DIV> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_image_and_canvas.html b/devtools/client/inspector/markup/test/doc_markup_image_and_canvas.html new file mode 100644 index 000000000..0b8a8bb80 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_image_and_canvas.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html class="html"> + <head class="head"> + <meta charset=utf-8 /> + <title>Image and Canvas markup-view test</title> + </head> + <body> + <div></div> + <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAdYElEQVRogVWYZ3QV5pWuz6y5694pKzNzZ26SyWQyccpkJo5jx8GOHXcbgzHYgOkgJECIYoQAIQkhoYoaoIKEQEK9HUnnqJxedXpvOk3l6OiogkB0XOLkzpo/z/2BV9a6P971re/ffvbe6/32twVB3QFC+oMEdcmMa/cT0uxnwpBCwnaEZc8J7npPMDO2n8Dop3iHNhIc2URUtoVp1XYS2iSiol3E5UeZ12YRNxSz6G1idXaU5Xk9E5MqEvNO4vMu4gk7iYSVpYSRlYSelVk1t+Mqlue1LM1pWE5ouJPQcG9WxaNZNU/ier6aNfF1wspKUM29yTGeLNq4k9CyEJdxa0HC4vwwgoDmWeBBXTJBXTIRXQqTxgPELanM2Y8QUe4krNhBWLadSfVO5saSWLaksGw5yLLpEM62j/F17SQ6cpQ5UzHLgSZuTw1yZ17L6oqd+TkLc3M25uYsLM2ZuTVnYGVOy524gltxOUuzapa+hVlNqFidkbASEbHg6SZubWfJPcgXcT1P5wysTCm5HVdxO6FkcVbK8oIMQXjsAOGxZCL6Z5mfNB5gynSQKUMKE/r9hBW7mBnbz5L1MCvOYzzwZ/A0coZH/gxuW45gbHwfbe372Dt2sWAu5PFkK6tTvdyeFnN3QcPctJK5mIaFuI7lhI47CQ2rCRX3EjJW43LuJLTcndPxYE7Dw1kJtyO9TBirMfZnMXw9FU3nGR5MDPD1kpqliJg7cQWri3rmZ+QsJlQIosYDhPT7CWn3EdYlEf0WJqzZS1C5i1lLKnc86Tzwn+Zh4AxfTeTyf+OFfBHMYslwGHvLR0grX2O44i08A4e467vC41gHD2N93J8Rczcu4e6snNWE4tk5K+VefJT7iVHuz0pYjal4mNDxJKHg3kQ/cVstNtFpBmu30pj3FvXn3sQnO8+TmX4ezo6yGpdzb2GM5cQYs1MqBCHtXsbVuwmodhHU7GFyLJmY6SAzpoPEjAdYdBznrv80D8fP8iScw59mCvnvuVIe+M8wpUjC2voR0srf0XX+VwxVvoVPdJhV7yW+jnfwzVwff1gU8/XiCE/nh3g0K+Z+TMj9mJBHs4M8mRvlaVzJFzNK7keExM3VeIcz0bXtRVSzlo7S16n8/Gf0XfmIeXcNf1yRc2d6hDszWh4uu0hM6hFMGJKZNKYwbT5IwnGEW9507oUyeRTN5slELg9CWTyOnufrqQL+OFPMfycq+GaqmAXTcdx9W9DUv81Yw7vorr6HqvY9LC1biclPcNtSyC17Kfd81dwPNHA/dIN7oSZWI83cn2zl6Ww3f1gU86c5GV9MiJg31WHvO4mi/jNGqtcjrnobYflr1Gf9B/U5L+KVneWb5WFWJgZZmlDwZMXP0owdwe1AOndDp3gYzebJVC5Pp/P4IpbPV7ELfD1TxNPJfL6YKuQP08V8Eyvhy2gRK85MIrJkHN2bMTV/yHjfZ0wO7SEo3IG/ZxvBvt2M9+/D07OH8FAaE9J0YqpsYrrzzBgKmDEXMeeoYNlTx21nPXFtOY7uEwxXfUpf4bsIC9+iO+8VmjKf50bWC9RkPI+u7QD3ox3cnxnh9qSCe3M2bsWsCJ7O5PFVooA/LZTwx/kSvojlcz+cw11/Jnd8Z7njzWbFk8WqJ4dVTw5L1jNMqtIIDCUREO1mSpJEXJbEzGgS0cGdBPs+I9K7g1D3Nvxdn+Fu34qvdzfjAwfwDR7E2ZeCqXs/hu5kjN0HMLYmoW7YzmDJOjpy3qAr5036z79N26k1XD74U+qOP09t+ouM1O5i3tXI10sq7ie0LExoWJwyIPhDoogvYvk8iGRzJ5DJHd8ZbnvOMG89ybT+CBOawwTlKbjFe3EM7MErSiYqP0pMk05ce4JF/XFmZSnMjO5lQrQDb9sGXE3r8bVuxNvyMYGOLQR7dxAS7iHQv4/AwAG8gwcxd+9B1bSZ0cvv0p3/Mo0n/pO2U2sQ5a2j9cQbFG/5MWfXfpfinT+jIf0N2gs2ccvdzJ9uabgbk3JrSsXqnAnBnUAmi650pg1pjCuScIl3Yuz+BG3bBlTNH6Fs2oD65kZ0bVuwCfcQlR9nwZjDsuU8C4ZMFvWfMzm6j1D/dvzdm3E0rcPRtA5/2yYC7Z/8GSLQvQ1vzw68vbvx9e/F0bcHc8dmdNffo6/wBerS/pXGY88zkv8xA2c3UrrleY68+vec3/RTrqW/R/uFzcwYr/J4ZoT7szLufWu/AlPPZnTtH6Ns/hBp43uIqt+kp/wVhFW/Z7T+QzQ3t+AWHWBae5o5Uw4LljwWrfksWPJYMGUzrzvB+MBObG0bsTStx3DtA6w31hPo2MJ451Y8LZvwdmzB27UVZ8dmHJ1bcXZvw927A3ffZ7h6N6C6+hqd2c/Tdvq3DJ/fRFf6enLX/Zyk5/+G8xv/k9asjfSU7CSivsTdSB8P5xQ8Xh5jZVaNoLvsVTpL19BT/jsGLr/JwOW3GbzyDpqbW/ANpzJvPseKq5i7nlKWrIXMGfNYMF9g0VLEoukccdVxHJ2b0TWuRX9tHWON67Hc3ISneye+rp14O3fg796Bt2cH7q5t2Ns+wdb+Cc6uLbh7P8E3sBFX90eMNa5HUfkRPZlrKdz0Cw6++B32/fKvqNy3hv6i7Ygu7WXSeIXVqQEezct5tKTndlyNoLf8Tfoq3mLw8vtI6jcgb9yEvm07gdFjLFrzeTR+mRVnKdOaLIKSDKY1OdxxlbPqqeKW9QLR0cOYbm5EffVDjDc3Y23fhqNzN57e/bi69+HuScLTm4Svbx/e3t24u7bhaP8UZ+eneHo+wdmzDo9wIz7hLhyt++jN/oBT7/4Te3/5Fxx74x+4fvL3DJR9ysDlLSSc1TydH+L+nJQ7CS0rCQOCgUsfILryISO1G5E1bEbXsgvXwGFi2mxu2UtZtpVw113BirOcOWMB86ZCVpzl3HaUMTeWi1e4D8219agbPsLevQfvYCo+0RE8omM4+9OwdR3A1p2EszcJr3AfPuEuvD3b8PdsIdC/FVffx7iFn+Lr38t4fxr6ut00HH6Zwi0/omr/v9N+7jW6S9+h4+K7xB3lfLE4xOqslNszelbnHQgGK9YxWr0JWd1mVNe2Ye05yLQmhxVn2bMs2y+ybCtlWn0en/gkvuEMprV5TGvzCEoyMLVtR167FnXjJ/jFaUQVmUQUZwkpsghIz2DpOYyxIxlzxx6cvfvwC/fi79tBULiNkGg740Pb8Yl34RXuIzhwmPDgCaxN+xkpW8dA8Rt0Fb1MZ+nLXM//NaGxHB7O9nF3RsbtmJG7CS+Ckas7kTfvR30zBU37IbySLG7763g61cLDyA2eTLZi78+g5vRrFCT/gq6SjwiMZjKlycc1cBRd0xZGrnyA5sZWxkczmNYVMKkvIaorIawpwiLMQN+Vhr4tCWvXPrzCffj7dhEU7iAs3sO4eA+h0RR8/Ul4e/YzLUknLs3A27kLzdUPGCx7icHy33A99z/wyT9nNXqT+zMj3IsbuBO3I1D3ZyPtPcXYUC4uXRlhey1LU518uSLlm1U1y+EeJC0nObPnBfa9+48UpLyAtH4fPvEZxofSsXbuxtmbhLM/jZD0HFO6SsZVldiHi3FKL+JXlSFpPkTf5c3Im7Zj696Lo2snnp5dTAynMC1NIyRKxte3j/GBZKZHj5CQHWV6aD+Rvm0Ya99EXPBLbp5+DkXtem45qngSE7I6KebxvA6BS3+Vcdt1pv3tTI23MR1sZ3VxlG8em/jqgYFEsA9x82kObvwJ7/xCQMZnP8HYcYw5XQkB8edMKdLxDBwgLDuDazCToKKCwfpj1Odv51TSGmovfIq84wRm0SlGr+9AXLMBW/deIsNp+Pr2MTFyhLA4lcDAAcZFKUREB5kaSmF6KIkZ0R58TeuQFf+G5s//lcHi3xNT5fAo0sKDSSGPZyUIJpxdxP19zIWETHrbiAU6eHJbxX89sfD0loqovZmumlRO7vwVJz/7KS0X1uEZPMmMOocJaTqhkTSiss+JyrMZrt1NXdZ6Sk+8T27aW+z44F9Y/+pfk536G7qufMbojd0om3dh7UkhIErDN3gQ/+BhxsVHCAwdwSc6hE+YQqB/P5PiZOKjKUwKd6C/8hbN6c/Rnv0ibuERVgPXeRzr52F8CMFiYJhZ3yAJ/wDz4X7uJST81xMLf3pgYnlyEK+2Bml7JoP1aZj6zjKlLWbOWMCCMZcVWx7zY2eJqU/RX7GB0sO/Zucb36Hy9Hoy9r7CKz8V8G/fEfDRa/+Tyqw30fQewz2agV2YhnvwKFFFJh7RMXzD6fhHTuId/hxnfyouYTLBwRQmRw4wM5KCrWkDfXkv0XHuZYztB1jxNvBlQszjxAiChEvElK2HeZ+I+3EVX67o+eaegYcLSmb83QT0DUxYbjBtbSA6Vsm0oYQlewl3nBdYsmSzZDnHtDaToZrNXDn9Ojvf/nuyU15l59of8fPvCnj9lwLOH/s9yr7TeFT5uGXnCCrPManJJ6o6z7g0h4D8HOOKXMYVufhkZ/BJ0hkfPsr4UCqRoUO4OrejrF6LuPw99C37WbBX82VCxOPEEIJZp5gpWx/zPjF3p+Xcm1XwaEnL09tjPF7UPhucwoPMutuIWa8SM1UQtxSTMOWSMGYxqc4gMHqMsDwHVUsa9ec/ISv5d5xLe4czh96grngX6sF8Io6rRGxXmLBUseC9yoKzhrCmiLDuIuGxMsJjZUQM5USMpUR0+YSVZwlJTzI+dBhPbxKW1u2oGj5F25LEtOEij2NCHsYHEMy6RcRdgyyGRrk7o+LRkp6v7jl4esfK4pQcn7EVnfgS8p5CrKNlhMYuMWmsJGEv5+74ZRZthbhEaXiGzmAWZuJXViHryEHdV4isJx+nthab6hJOXSUTrgaWwm0sh9tIOK8RNVwmarpCxFxD1FJLxFpL2FJN1FxJ1FDM5Nh5IvJT+ESHcfcdwNi2B21LEhFNIQ+mOng0I0RgkdbhUjcy5e7j9rSCp3esPL5jxWHqoK7qBAf3vs3xlLWkJ39AZupaGor3YhoqZtbVyIKnjthYHvGxfILK8/ikBfiVVbiV1ZhHKhkbqcAgKcesrMRjqCbqamTGd5O4t5mY8zqTtgaCxjqClnrCtusEbQ34zTUEzJeZsFQxY6tgxpBPUJKOR3QES3cyutZ9hFR5PJhs5VG8G4F3rBGLsppJfw8L0xLik6PcWTYh7C7jxz/6H/zz9/6CH37vL3nuB/+L//jx3/DRmz/jUn4ybt0NVqJ9rASuEjcWElTmE9JWELM0MmVrIWxqxme4jt/UiM/cQMBylbCjkZi3hYXxThaDPSwGe5gPdBHzthFxNBO03yBov07IXk/EWkPUXEbMXMSMMY+gJB2H8BDOgaN4JZnMOi7z1XwfAr2kFKu2hrmJQW7NK1iaU3P3roP+/su89OIP+NEP/44ffP9v+bu/+Qv++i8FPP+zf+JCVgpOXSfLkRHmnHXEzaVMGcqYNtcQdzQRd3Uw6+lhbryfCVcLEVczEed1JtxNxLwtxP0dzHo7iHvambQ3E7U3E3G0EHG2Eva0EHXfJOqsZ8JeRcx2kVlTAWHFKdyDabhERwlITzHnqOCPCz0I1EMFeK0N3JodZnlOTiIuZ3JSTnNzIW+8+Ut+/vN/4SfP/ZDvffd/8w/f+St+++K/U5yXjlbajlPbzIztKtPGMqJj5UyZa5mxNzPjbCXu7mTG20XM18GUp5VJVxMTzhtM2q8zYbtB1NxE2HidgK6OkOkaEUcLk54Oor42JjwtRF0NTDmqmTSVEjNdIKw4hWvwEM7+VHwjJ5izFPGHRCuCMUkREed1bsXELM4MMxESYTC0UFl5ik2fvMWPf/x9nvvxD/n5z37CmpdeYN+uT7l5rRyjug+T4joR4xX8qgu4ZBfwa8qZsFwn5mghYm7Cq69j2tPKlLuZCWcjE/YGIpYGwsYGxvUNBLX1BDTVhAz1TNqbmXC1EHY2M25rZNxSTdhcybgmn7AmG9/Icay9+7D0JOEcOEhMn8vTyXoE5pEiwuZ6liL9rMyMMhMeQiW9SnnJMVKSNvL273/Db196nt+teZFPN3zA2ZMHab9RhlrajF5ag01aiEOSi1NSgFdZTshQz4TlBiHTNXy6WqLWa0Rt9USsV4mY6wibrhIx1BPWXyM61kBUX8uksZ4Jy3XC5mt4xmpxaKtwaC7i0RThkmbhlWZgF6ZgbN+OqX0ntr4kosoM7geqEFjFxUQM9dwO93MvLmUuNIxe1kBF4VGSdnzA5g1v8e7rv+GDN9ewb8cGcs+kcKMum6Gei8j7CzGIc3HJ8vCqSvEoynDIKnArL+PT1RIYq8OprMClvohXU45XU45fXYlffYmgppaI9ioxQx3TY7VE9LX4NNXYVVVY5Bexygqxy/NwSs7iHD6OqWs3uptbMLZvx967l7DkGLccFxA4RaXMmJt4MCHmXnSEOe8gfkMnzdVZ7N38Fq/9+ie8+IsfsuZX/8bH773E8ZR11JSlIu7OQ9F/HllXOsquE8g70pG2ZqDszsE0VIJDVoFTUY55pACrpACXvAiXvAi3rBSPrJyA4gphZTWT2ktEVOX4FeW45eXY5WVYFaXYFUXYFbm4ZJk4xUfRtW5Ddf1jjK1bsXbvxic+yKz+DAK7uJSEo5VHk6MsBQZIeIXcisrQiGo5vv9D3lvzHK/88ru88JO/5YXn/ooPXvk+pw69w7WKJNpqD9JxeTeNhZu4nPUBV/M3IWo8jm2kFI+iAvNIATZpMU5FCV5VKT51GT5VBT5VFUFNLVHdVYKKMvyyYpySQhySYhyKctyaCtzqUjyqAryybByDR1E3f4a8YQP65k8xt2/D1beXCdlRBDPeDhbDQpajAyxEBliKDjEXHMCuuoqwKYuTyW+x/cOf8eGr/4cNr3+fjW/8Mx+/9k9sf/+HHN3+K5LX/wtJa7/LiW0/p+7cBmQ3T2AduoBjtACHtBC3ogSPshSPugKf9jI+TTVedTVuZTUuxSWc0ou4FSX4VBUE9JcJGqoJj1UzrinHryzGJz2HuecI2pu70d/cjq5pM/obH+Pu3cGs5giCh0sKHt9SsjonYWGin4WJfpYmBog6b2KWVtJ86QAlp9ZzOvlVsg6+TvbBNzm171VOJ/2OC0feoezE+1SdfJemC5tRNB/HMXwBt7QQ23Ae1qHzeJSluBUlOBUXcSkrcKku4VZfwaupw6utwaWqxKOtxK+7RMRQw4S5hgnjFSL6CsKaEkLyPKy9R9E070J/czvG5q0YbmzE1b2VKWkKgq/v6vjDPS2PFuUsRoXMjnexEO4l7u8gbG7ENFKKuOkkHVcOIqw7grjxJH21R+m6nEpfzWFGrh9H3nQMU28WflkxAdVFnKN5mMXZWIZycEgLcMqK/gzgUV3Gq67Gq63Bq615VhVtFaGxS0SMV5gwXmHCUPHsS6rOJyjNxNyVgvb6VsZubMbU/Anm5o24OrcQGdqL4NHCCF+syHmyJOH2VB+J8TYS/hamnU2EjM9szyEpxTiYj22kGLe0AvtwKebBQmziQhyiPOyiXJxDebhH8rAP52IaOINZnI1DkodtJBeHtACPshSftgq/7go+TTUu1SUcikq8mkt4NZUEtBUEtRWEtKWE1AUE5Nn4JRk4+1MxtG5H2/gxumvP9kfGxg9xtW4i0r8LwYPZAb64LeHr21Lux/tZDLWR8DYx7Wggaqpm1tXIhLGaoLaScU0FfsVFnCMF2MR5uIYu4B7JwyY6i0l4CmNfBmN9GRgHMrANZeOS5eFRXMCtKMSjLMGruohHXYFHXYVbVYlb9cxSfaoK/KqL+FSFBBR5+GRn8AwdxTV4AHP7NgzNH6O/thZ9/Xuor7yJ6vIb2BrXEe7ZjuDx3CBf3hriyyUxd6c6SfgaiTuvErPXErPVELPVMGW6TERfQUBVglt6AYf4HHZRDo6hbBxDZ7EOnsbcfxJz/0msg6ewDWfilOTgluUS0pcQ0BTjVhTikBZgkxbiUpbh01YxPnaFce0V/OpKfMpSvIo8vJJMXENHsAuTsHZvx9iyAUPzWgzX3kNT8zry8jXIyn6Lqe49Au2bETyZFfJkXsjj2R5uhZqIOaqZNFcxZakkZq0iMlZKVFfyrKyKPHyyXHzSc7hHsnCIT2EXZWAXZWAbPIllMB2r6CSO4dO4ZefwKs4T1BUT0BTjURXgkhfgVBbjUZcR0FcRNFwmqLvMuKYCn6IIjyQH99Dn2Pr3Y+78DEPbBvQ33kV77Q00Nb9DWfky0osvoSx/BWvDWsbbtyJ4ONPF49keHs50sBxsZMZ+mQlzGZOmZ5oyXGRirIigOo+AModxxTO5Rk5iEqZhEx3HLv4cm+g4xt4jjPWmYRlMxyXNYlyTj0t+Do8ij4CmmHF9KaGxCgJjlfj1FQS05QS1lQTUZXhk+ThHzmAfTMPcvRt960Z0N95H0/B7lNVrkFa+iPTir5GXr0F35S1czRsJ9+xCcH+ylccznTyYamPRV8+0pYKooZSorpiw9gJRfSFhbR5BVTYBxVnGZafxSzLwjqTjHvocu/gYdvExbKKjWAePYRk8hlV0Aos4A+vQKbyK8/jVBYT0F4mYKoiYKgjqy/GpS/AoS/DIi3FK87ENn8UuSsc+mIq1dy/mjs0YW9ahqHmNweL/pPf8TxkqeQH1lTcxNqzF1bKVUO9eBKvRVh5MtXF/spVFXz0xaxUTxotE9YWENPmENOcJqnMYV2bil58mIMvAJ0nHPXwch+gIloHDmPtTMQkPYew7xFhvKoa+wxiFxxkTHieoLSSkKyakv0hIf5FxfSk+TRFuRSFO2QXc8gIcklxsw5k4hk7gFKdh69uHsW0z2utrUda+wXDFbxkpexlt3TvYmjfh6tzOuHA/U5JjCFZCLaxGW1mNtrIcuMas4wpT5nIiY0UEtRcIqnMJKLMIKM4QUJxhXH6KgCwd7+hx3MNHcQ0dwSlOwypK/RbmMKb+I5j6P8c8cAK37BxuWS5ueT5ueT5OeR4O2XlsknNYR3Oe3SU5OEazcA2fwj18HFtfMvqWrSivrkVy5R1GL72Bsu49rK1b8PTtJTBwgNDIEaZVpxEs+2+yEmrhbqSNlVAzi54G4vbLTJrKiIwVEVDm4JWdwSM5iWvkBJ6Rz/+/wJ3iVByiQ1hFqZj7UzH3H8YsPPYMoP8kqs4jaLqPoRdmYBw8g3k4C8tI9p9ll+bglOXgkmbhGj2NY/A4xq5k9Dd3or62GVndRygbNmJs3Y6nP4XA0BECI58TVpxhSn8OwYKvmeXxFu5EOrgbbWcl1MK851krTZrKCChzcEtOYR08gqH3ILqu/Wg7k9B37kXXsQdN2w40bTtQt+5E1bILRctulC37UbUfRNWWymhzCrKWw6i70zEMZGIdPod99FkFbJJszMNncEjP4pJl4hjOwNSdhq41hbHWZMztB7B1peLqO4p/OJ2wPPPZukWdQ1Sfx5SlCMGcu5kFXzMr4Q7uT/Vwf6Kb5UATs/aaZxCGYsZV53AOp2PoPYi6fS+Kll0omrchb9rCSMNGRq5tYKThE0avbWH0+jZkTXuRtySjbE1F232Csb4MTKIsbCO5OKX5uOUFuBT5uBTnMY+cwiY9jVOWiXXoFKa+4xi7jmHvO4l3MJOINI+o4gJT6iIm9UVEdAUE9QWExgqJmIsQxJxNxN1N3Brv4GFMyMNYH7eCN5l11jJtrWLKXE5YX4hPnol9+Dgm4SH03cmoO3ajaH0GIW/+FOXN7ahbd6Pt3I+hJ+3bR+00Lsl5XJJ8XJICXJKCPwN4FIV4VPk4FWdxKTNxK7LwyHPwSXIJSi8QlRcxpSojrqsiPnaJ2NglpgwVhPQl+PRFeHUFuLTnEUzaGpl23GDJ38bDmJBHM/3fAlxl2lpFSFdMQJ2HT56FS5KOXXwMc38q+p4kNJ270HXsQt+1E2PPfqz9qdjFx/BKMgko8gipCwmoSvApinBJCrAPfzsbSfJwywvwqi8QNFzAqz+HX5NLSFdEzFTBnLWaJUs9C6Y65k1XmbPUM2e5SsxSTdR0iYCxFLe2AKs6l/8HXK32/y5m8HIAAAAASUVORK5CYII=" /> + <canvas class="canvas" width="600" height="600"></canvas> + <script type="text/javascript"> + "use strict"; + + let context = document.querySelector(".canvas").getContext("2d"); + context.beginPath(); + context.moveTo(300, 0); + context.lineTo(600, 600); + context.lineTo(0, 600); + context.closePath(); + context.fillStyle = "#ffc821"; + context.fill(); + </script> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_image_and_canvas_2.html b/devtools/client/inspector/markup/test/doc_markup_image_and_canvas_2.html new file mode 100644 index 000000000..adae9ce21 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_image_and_canvas_2.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html class="html"> + <head class="head"> + <meta charset=utf-8 /> + <title>Image and Canvas markup-view test</title> + </head> + <body> + <img class="local" src="chrome://branding/content/about-logo.png" /> + <img class="data" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAADI5JREFUeNrsWwuQFNUVPf1m5z87szv7HWSWj8CigBFMEFZKiQsB1PgJwUAZg1HBpIQsKmokEhNjWUnFVPnDWBT+KolJYbRMoqUVq0yCClpqiX8sCchPWFwVlt2db7+X93pez7zu6Vn2NxsVWh8987p7pu+9555z7+tZjTGGY3kjOMa34w447oBjfKsY7i/UNM3Y8eFSAkD50Plgw03K5P9gvGv7U5ieeR3PszeREiPNX3/0DL4hjslzhm8THh+OITfXk3dhiv4GDtGPVzCaeJmPLYzuu5qJuWfuw2QTlcN1X9pwQU7LhdZ/ZAseD45cOh9hHvDkc/yAF/DNhdb5Mrr3PvBMaAYW8fMSIi2G497IMEK/YutGtAYr6+ej+nxu/NN8Ks3N7AR6HgcLz0Eg1Ljg1UcxZzi5qewIkMYLRweTr2Kzp+nmyXAd5pS3XQDd+N/4h4zgu9FI7brlXf90nMEnuwQxlvv+hosE3TuexmWeysmT4W+WxkMaLzf9Y8ATgjcUn7T9H1gqrpFq8eV1gMn6t16NhngjfoX6q4DUP032Rd4LJgpSLwJ1yzFqBG69eRkah0MVyo0Acfe+yy9AG4nMiYCkeM53KKFXncBLAXqEm+wCqZwaueq7WCmuLTcKSJmj737ol2hurA9eq9VdyiO8yWa3NNyog+SB5CZodSsQq/dfu34tJpYbBaTMzvVddDZu16q5smXf4G8zEvqm4cyaAmJPuTJk3oJWdS4WzcVtfMZbThSQckb/pYfRGgo3zNOqZnEHbJPGK4abaDCQIIsT8V/qTaBqHkLh6LzXH8XZQhbLhYKyyCC/WeHYcNdmvOgfe8skzbWL270/T3wf7tSx/lGCbTu8xlzzmCSWLc5iwmgikcCHi3Mga0Ry913vBFvQwg90l6M4ImWKfsWOp7DSWxmfpPlCFuPFfsNfKrCnPYpQKIRgqBK7D0SxYaNHwkEiJMtl0ReDp3Lc5D3PGoTo/sKngCl7a5chFqvBatKwjBd7WwqIlzB/78NcoUcp5VSgGxm+7b8eqQRGnHMO634epO4S1EZww09/iFg5UmGoESDuznP1xVhTUX1WWHPzjpd25wyH0hRxI3LGM75nxmuNEEUVpAN0XgxmPoKralakbQnWlIMQyVBD/w+3orkq4lvualjKyWwzt4MaxqspQHVhPOWG64bxYuhZXSFGWhipbSDVragOu5Y9eAsmDDUKyBA703vemVhHoueD6e9wAzJK1WfmN0Umk5GGM4kEMZcuIECqgjm0nldAqmbjwtm4VxZH5AvlADP6mx9Eqy9Q0+KqW8Ch+47FaMMYmnNGfY1iPMshoC6qFxme4wQ+0p+ARE6H3+9veWEDWgUhDhUKyFARn4jM5BNxT0XsMg7bfymGK1ov3wtjDfhL4w0HVGUVBEjDaaE+QNdrcNWch1PG4W6xrjBUXECGivg++Cva3JUT4iQUz3V2RsSVaKLwOuDT89A3HdBQoxhNC+fnVm74ual2EG893P6G+PuP4SfiO4cCBWQooL9qCWKNXPbcI37Aa/lnlZxXRt4RFONGwSDCPAHqOuqjWct1QiEMw5mChM5X4K47FyNqcd3aK9AwFH0CGYLoe1ctxk2eWi57rg5JfGp9rzC6ggCdFlAgHBDw5Yxlcg6G8SyHCjMlsgmDD9zhSeHlF+JnAgWDTQUy2NxfdwOao1UVV3pi3+bE97YSbWpLAbn6zefHNQkp1PMpIBwwvslKgIYTKM2nEpNzrGcH3FXTEal0L38kJ4uDQgEZbO4vnI173LXf5NHZaiUxtaCxyZuo/rK6LpUg54yg3zTWRAArvDcRIPZ6BqzrQ1REpmL+DNw32OKIDCb3X1qPVn8wNNMT4w2bvs+q4bAZrqBh2skaL3yyhhIIZ4i6oHkUK0RckcB8GigEyRIH4A6Mgc8fatl0/+BkkQxC9gIT4ljna1rIZW9rEdNbjJcNjsnoYj7LHWCUwpITzEgzRQKZ3XAFHbTzA3hrz8TEUUZxFBhoKpABQt/97p+w0hMZG68I8R6FtlsJT3FELndZntjM+VMnylKYq8GJI3UZaRMpquGSGFVOEfv0YZBMNzz+uvjbfzS6xQERIhlI9FcvQWNdFVb7x1zCb+QNK8vb9NsiifmI5hBgVoOCBC1sb0ab5RomqENxLO3eA1/0NDRU47q2RQNbRCUDIb7lF2CNL3ZGxEV4n08TVvZWYG4pZyV0zUdS45tyCBByOHWiyvZmxFXDCyRo1ge5+Sy0TA+8lWMiP/6O0S32exGV9Jf4fr8azdUR3zL/CZz4MtvzdX5uOYs6NDOmpkuj5Huh+7qUQSYl0ThHzw0YQzcGo6bhzEqoYq5rN3yRiYiG3Vfe2Ybm/qKA9NNZ3nNm4F7/yDkg9AN+U1mHiBcXP8zuDN76jj8hg1QyiWQigalj02BJPhK8I0zxijAjhp5zhlpLUDvS+BCy2HMAvvB4XDgL9/SXC0g/ou/5+6/xLX8w0uJrOIkXfPvyhY0F6gr7M8H0KWFYikcqAXakB+xwD9CdREBLoau7Gz3cAdSIdLFxFtJTCqRChSjnutvhDcREtzjz2Tswtz+yeNRFUeXZXtWux7C1fuoVcbd3J//ipDX3uZZDLGrwweS+UBLL5TDliVBnF8P7H+XI8aRRGsIBJg/Zlslt1+W+D1JWoSyi+kD9jfhs78t7mhZhSl+fLfY1Bdyv3I8V/qpY3B1McgN7ZFT5/vNO0I5DPLLdPBIJA8qc4h2I0QplYfDpJwHT+aj0246r5S8rToG8OjCle8wk4OLvvYGa+Ovr84uo2qBSwJS9G5egoZFLTfiEqWDtbwGfHgKOdPHcS+ai7XDzMPW/FJRLGGcxnBbK4YJC2K+h+T6Bdu5CqHqCWERd3bawb7JI+iJ735+LNaHaprBLLHBm08U3XxShEsdt+f3eTh3v7aC95Dct4RCWL5OZWh/oXBZThxAIxyOXLzBk8aiEWJID8rK3CpPOmeHaGpvCS+7EHv5FujVHUSJPLXvIFeHcNc+9xrB2gws9KZdxuLFax/WLM5gzzSm/lTXF/OdAcapyvjxPqxqHjr2v4ckX2bS2dRBrc5lSdpKjEJ9/9tdwX2WMd53ZQ2IVo3RES+UwVSpCPvYepNx4gmTGDUKIMQ4eduPnD7mx9xOn/KZKOlFbStjONxHTtR+BYAPmnoZ1Zp8wkBRwP/EL3u0F/C2hGl7vpz7vW37T3vP7if8wroKuoh8ribknX9BK5rcF+mo1qKaKyRPJTgTDjbzY8szcuLb3bpH00u35T47j7prRpwDJTxzyG0dHgxPp5bPG8VdkpfPbUg3SgoOo2mwVukb98D5EqpswZTTulCggTk4gpYhv0++wIhCJxr0+Hq1sondis0SE2oxQe3qWXwWyO4DSQg9gJ8Iiw1VFcGqXxet0N9xE4ygIxv/9W6wo9WyROEX/R+eiobYSq2vHTOR631Eiv2lRfh9dvxkumkXh92Qsx8XrAJ+7YGbWuhxOi/U+31NQmzyqNYG8N/3wfo6CRtRHcN01FzkvojohwLu0VVvDa56IS/xcj2b7nN+O+m0jqpE1wMPXZxAN9iCVThtDvH7gmiRGRpU8Lspv1Uhq4wIVdQoyuGSLNYPKUCS8+CzNURbzMmjK3i8u0U793lmuV0ef9nWQ5MGC/DiUqEUSaCtXna9RJEspZS1lrXINK/pcq+SpT50t98QKMq1FRmDfx3vxty102k0PM4ssEnvuz5+G26Ij4yDpz6z9fV8bkyIkqBFkhej0Ib+ZQ34XJK9AfozaiimqIoX3Jp3tiISrcfYpuN2+iFph/02P36PNC9fVcCnp6H9jYouKyfaWufz5Tp9tVxcUniw7IohZv4dZz81/ns67z3AYPrc2n0+Ix2q8k0PWjgBy88XaibnfK9A+5LdDY2Ivhy36fbT8Zv3Lb1U1qLqUxorXEEXIs0mjjrtxoTZWtdvigNs2sgPiujTv6DIZLld6b/V5742JZV3fUsUVFy5gdsNtKWFzUCEVbNepD1MkSMVbsb6SZm7jI3/zODtQKgUMsOw8wDZ63t5xcV1TnaEAxoc6wrqY+Fj+N4DsqOnhOIdicrQSm1MPYCPlIqHn5bbHg8/bj2D3QfZnCX3mpAICDZV8jH5kpbZqTD0W+DxaA74CWzLN2nd14OlL72J38Lf7+TjC7dadZFDoZJQPrtaIKL/G0L6ktptPZVJ8fMqHYPZOKYPMyQGadIJfDvdXwAFiZOTvDBPydf5vk4rWA+RfdhBlaF/yDDBRoMu9pfnSjv/p7DG+HXfAcQcc49v/BBgAcFAO4DmB2GQAAAAASUVORK5CYII=" /> + <img class="remote" src="http://example.com/browser/devtools/client/inspector/markup/test/doc_markup_tooltip.png" /> + <canvas class="canvas" width="600" height="600"></canvas> + <script type="text/javascript"> + "use strict"; + + let context = document.querySelector(".canvas").getContext("2d"); + context.beginPath(); + context.moveTo(300, 0); + context.lineTo(600, 600); + context.lineTo(0, 600); + context.closePath(); + context.fillStyle = "#ffc821"; + context.fill(); + </script> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_links.html b/devtools/client/inspector/markup/test/doc_markup_links.html new file mode 100644 index 000000000..f393319f8 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_links.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>Markup-view links</title> + <link rel="stylesheet" type="text/css" href="style.css"> + <link rel="icon" type="image/png" sizes="196x196" href="/media/img/firefox/favicon-196.223e1bcaf067.png"> + </head> + <body> + <form id="message-form" method="post" action="/post_message"> + <p for="invalid-idref"> + <label for="name">Name</label> + <input id="name" type="text" /> + </p> + <p> + <label for="message">Message</label> + <input id="message" type="text" /> + </p> + <p> + <button>Send message</button> + </p> + <output form="message-form" for="name message invalid">Thank you for your message!</output> + </form> + <a href="/go/somewhere/else" ping="/analytics?page=pageA /analytics?user=test">Click me, I'm a link</a> + <ul> + <li contextmenu="menu1">Item 1</li> + <li contextmenu="menu2">Item 2</li> + <li contextmenu="menu3">Item 3</li> + </ul> + <menu type="context" id="menu1"> + <menuitem label="custom menu 1"></menuitem> + </menu> + <menu type="context" id="menu2"> + <menuitem label="custom menu 2"></menuitem> + </menu> + <menu type="context" id="menu3"> + <menuitem label="custom menu 3"></menuitem> + </menu> + <video controls poster="doc_markup_tooltip.png" src="code-rush.mp4"></video> + <script type="text/javascript" src="lib_jquery_1.0.js"></script> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_mutation.html b/devtools/client/inspector/markup/test/doc_markup_mutation.html new file mode 100644 index 000000000..f021c9fcf --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_mutation.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> + +<html class="html"> + <style type="text/css"> + #node1.pseudo::after { + content: "after"; + } + </style> + + <body class="body"> + <div class="node0"> + <div id="node1" class="node1">line1</div> + <div id="node2" class="node2">line2</div> + <p class="node3">line3</p> + <!-- A comment --> + <p id="node4" class="node4">line4 + <span class="node5">line5</span> + <span class="node6">line6</span> + <!-- A comment --> + <a class="node7">line7<span class="node8">line8</span></a> + <span class="node9">line9</span> + <span class="node10">line10</span> + <span class="node11">line11</span> + <a class="node12">line12<span class="node13">line13</span></a> + </p> + <p id="node14">line14</p> + <p class="node15">line15</p> + </div> + <div id="node16"> + <p id="node17">line17</p> + </div> + <div id="node18"> + <div id="node19"> + <div id="node20"> + <div id="node21"> + line21 + </div> + </div> + </div> + </div> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_navigation.html b/devtools/client/inspector/markup/test/doc_markup_navigation.html new file mode 100644 index 000000000..9633052e1 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_navigation.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> + +<html class="html"> + <head class="head"> + <meta charset=utf-8 /> + </head> + + <body class="body"> + <div class="node0"> + <p class="node1">line1</p> + <p class="node2">line2</p> + <p class="node3">line3</p> + <!-- A comment --> + <p class="node4">line4 + <span class="node5">line5</span> + <span class="node6">line6</span> + <!-- A comment --> + <a class="node7">line7<span class="node8">line8</span></a> + <span class="node9">line9</span> + <span class="node10">line10</span> + <span class="node11">line11</span> + <a class="node12">line12<span class="node13">line13</span></a> + </p> + <p class="node14">line14</p> + <p class="node15">line15</p> + </div> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_not_displayed.html b/devtools/client/inspector/markup/test/doc_markup_not_displayed.html new file mode 100644 index 000000000..20a4b9415 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_not_displayed.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> + <style> + #hidden-via-stylesheet { + display: none; + } + </style> +</head> +<body> + <div id="normal-div"></div> + <div id="display-none" style="display:none;"></div> + <div id="hidden-true" hidden="true"></div> + <div id="hidden-via-hide-shortcut" class="__fx-devtools-hide-shortcut__"></div> + <div id="visibility-hidden" style="visibility:hidden;"></div> + <div id="hidden-via-stylesheet"></div> +</body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_pagesize_01.html b/devtools/client/inspector/markup/test/doc_markup_pagesize_01.html new file mode 100644 index 000000000..8323f0b2e --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_pagesize_01.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> + +<html class="html"> + <body class="body"> + <div id="a"></div> + <div id="b"></div> + <div id="c"></div> + <div id="d"></div> + <div id="e"></div> + <div id="f"></div> + <div id="g"></div> + <div id="h"></div> + <div id="i"></div> + <div id="j"></div> + <div id="k"></div> + <div id="l"></div> + <div id="m"></div> + <div id="n"></div> + <div id="o"></div> + <div id="p"></div> + <div id="q"></div> + <div id="r"></div> + <div id="s"></div> + <div id="t"></div> + <div id="u"></div> + <div id="v"></div> + <div id="w"></div> + <div id="x"></div> + <div id="y"></div> + <div id="z"></div> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_pagesize_02.html b/devtools/client/inspector/markup/test/doc_markup_pagesize_02.html new file mode 100644 index 000000000..db2502c89 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_pagesize_02.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> + +<html class="html"> + <body class="body"> + <ul> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + <li>some content</li> + </ul> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_search.html b/devtools/client/inspector/markup/test/doc_markup_search.html new file mode 100644 index 000000000..08c047bcc --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_search.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head></head> +<body> + <ul> + <li> + <span>this is an <em>important</em> node</span> + </li> + </ul> +</body> +</html>
\ No newline at end of file diff --git a/devtools/client/inspector/markup/test/doc_markup_svg_attributes.html b/devtools/client/inspector/markup/test/doc_markup_svg_attributes.html new file mode 100644 index 000000000..04b699be7 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_svg_attributes.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> + <body> + <svg viewBox="0 0 2 2" width=200 height=200> + <circle cx=1 cy=1 r=1 fill=lime /> + </svg> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_toggle.html b/devtools/client/inspector/markup/test/doc_markup_toggle.html new file mode 100644 index 000000000..521db100c --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_toggle.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Expanding and collapsing markup-view containers</title> +</head> +<body> + <ul> + <li> + <span>list <em>item<!-- force expand --></em></span> + </li> + <li> + <span>list <em>item<!-- force expand --></em></span> + </li> + <li> + <span>list <em>item<!-- force expand --></em></span> + </li> + <li> + <span>list <em>item<!-- force expand --></em></span> + </li> + <li> + <span>list <em>item<!-- force expand --></em></span> + </li> + <li> + <span>list <em>item<!-- force expand --></em></span> + </li> + </ul> +</body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_tooltip.png b/devtools/client/inspector/markup/test/doc_markup_tooltip.png Binary files differnew file mode 100644 index 000000000..699ef7940 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_tooltip.png diff --git a/devtools/client/inspector/markup/test/doc_markup_void_elements.html b/devtools/client/inspector/markup/test/doc_markup_void_elements.html new file mode 100644 index 000000000..72a937980 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_void_elements.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html class="html"> + <head class="head"> + <meta charset=utf-8 /> + <style> + .before:before { + content: "before"; + } + </style> + </head> + <body class="body"> + <h1>Test void elements in HTML document</h1> + <img> + <hr> + <hr class="before"> + <br> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_void_elements.xhtml b/devtools/client/inspector/markup/test/doc_markup_void_elements.xhtml new file mode 100644 index 000000000..331346b24 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_void_elements.xhtml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head class="head"> + <meta charset="utf-8" /> + <style> + .before:before { + content: "before"; + } + </style> + </head> + <body class="body"> + <h1>Test void elements in XHTML document</h1> + <hr class="before" /> + <img /> + <hr /> + <br /> + </body> +</html> diff --git a/devtools/client/inspector/markup/test/doc_markup_whitespace.html b/devtools/client/inspector/markup/test/doc_markup_whitespace.html new file mode 100644 index 000000000..9071c802d --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_whitespace.html @@ -0,0 +1,25 @@ +<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ #pre {
+ white-space: pre;
+ }
+ </style>
+ </head>
+ <body>
+ <div>div 1</div>
+ <div>div 2</div>
+ <div>div 3</div>
+ <div id="inline">
+ <img src="chrome://branding/content/about-logo.png" />
+ <img src="chrome://branding/content/about-logo.png" />
+ <img src="chrome://branding/content/about-logo.png" />
+ </div>
+ <div id="pre">
+ <img src="chrome://branding/content/about-logo.png" />
+ <img src="chrome://branding/content/about-logo.png" />
+ </div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/markup/test/doc_markup_xul.xul b/devtools/client/inspector/markup/test/doc_markup_xul.xul new file mode 100644 index 000000000..34f13dae0 --- /dev/null +++ b/devtools/client/inspector/markup/test/doc_markup_xul.xul @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<xul:window xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Test Bug 984442"> + + <xul:panel id="test"></xul:panel> + +</xul:window> diff --git a/devtools/client/inspector/markup/test/head.js b/devtools/client/inspector/markup/test/head.js new file mode 100644 index 000000000..f7d55a272 --- /dev/null +++ b/devtools/client/inspector/markup/test/head.js @@ -0,0 +1,653 @@ +/* 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/. */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from ../../test/head.js */ +"use strict"; + +// Import the inspector's head.js first (which itself imports shared-head.js). +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js", + this); + +var {getInplaceEditorForSpan: inplaceEditor} = require("devtools/client/shared/inplace-editor"); +var clipboard = require("sdk/clipboard"); +var {ActorRegistryFront} = require("devtools/shared/fronts/actor-registry"); + +// If a test times out we want to see the complete log and not just the last few +// lines. +SimpleTest.requestCompleteLog(); + +// Set the testing flag on DevToolsUtils and reset it when the test ends +flags.testing = true; +registerCleanupFunction(() => { + flags.testing = false; +}); + +// Clear preferences that may be set during the course of tests. +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.inspector.htmlPanelOpen"); + Services.prefs.clearUserPref("devtools.inspector.sidebarOpen"); + Services.prefs.clearUserPref("devtools.markup.pagesize"); + Services.prefs.clearUserPref("dom.webcomponents.enabled"); + Services.prefs.clearUserPref("devtools.inspector.showAllAnonymousContent"); +}); + +/** + * Some tests may need to import one or more of the test helper scripts. + * A test helper script is simply a js file that contains common test code that + * is either not common-enough to be in head.js, or that is located in a + * separate directory. + * The script will be loaded synchronously and in the test's scope. + * @param {String} filePath The file path, relative to the current directory. + * Examples: + * - "helper_attributes_test_runner.js" + * - "../../../commandline/test/helpers.js" + */ +function loadHelperScript(filePath) { + let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); + Services.scriptloader.loadSubScript(testDir + "/" + filePath, this); +} + +/** + * Reload the current page + * @return a promise that resolves when the inspector has emitted the event + * new-root + */ +function reloadPage(inspector, testActor) { + info("Reloading the page"); + let newRoot = inspector.once("new-root"); + testActor.reload(); + return newRoot; +} + +/** + * Get the MarkupContainer object instance that corresponds to the given + * NodeFront + * @param {NodeFront} nodeFront + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {MarkupContainer} + */ +function getContainerForNodeFront(nodeFront, {markup}) { + return markup.getContainer(nodeFront); +} + +/** + * Get the MarkupContainer object instance that corresponds to the given + * selector + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {MarkupContainer} + */ +var getContainerForSelector = Task.async(function* (selector, inspector) { + info("Getting the markup-container for node " + selector); + let nodeFront = yield getNodeFront(selector, inspector); + let container = getContainerForNodeFront(nodeFront, inspector); + info("Found markup-container " + container); + return container; +}); + +/** + * Retrieve the nodeValue for the firstChild of a provided selector on the content page. + * + * @param {String} selector + * @param {TestActorFront} testActor The current TestActorFront instance. + * @return {String} the nodeValue of the first + */ +function* getFirstChildNodeValue(selector, testActor) { + let nodeValue = yield testActor.eval(` + content.document.querySelector("${selector}").firstChild.nodeValue; + `); + return nodeValue; +} + +/** + * Using the markupview's _waitForChildren function, wait for all queued + * children updates to be handled. + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return a promise that resolves when all queued children updates have been + * handled + */ +function waitForChildrenUpdated({markup}) { + info("Waiting for queued children updates to be handled"); + let def = defer(); + markup._waitForChildren().then(() => { + executeSoon(def.resolve); + }); + return def.promise; +} + +/** + * Simulate a click on the markup-container (a line in the markup-view) + * that corresponds to the selector passed. + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {Promise} Resolves when the node has been selected. + */ +var clickContainer = Task.async(function* (selector, inspector) { + info("Clicking on the markup-container for node " + selector); + + let nodeFront = yield getNodeFront(selector, inspector); + let container = getContainerForNodeFront(nodeFront, inspector); + + let updated = container.selected + ? promise.resolve() + : inspector.once("inspector-updated"); + EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousedown"}, + inspector.markup.doc.defaultView); + EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mouseup"}, + inspector.markup.doc.defaultView); + return updated; +}); + +/** + * Focus a given editable element, enter edit mode, set value, and commit + * @param {DOMNode} field The element that gets editable after receiving focus + * and <ENTER> keypress + * @param {String} value The string value to be set into the edited field + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + */ +function setEditableFieldValue(field, value, inspector) { + field.focus(); + EventUtils.sendKey("return", inspector.panelWin); + let input = inplaceEditor(field).input; + ok(input, "Found editable field for setting value: " + value); + input.value = value; + EventUtils.sendKey("return", inspector.panelWin); +} + +/** + * Focus the new-attribute inplace-editor field of a node's markup container + * and enters the given text, then wait for it to be applied and the for the + * node to mutates (when new attribute(s) is(are) created) + * @param {String} selector The selector for the node to edit. + * @param {String} text The new attribute text to be entered (e.g. "id='test'") + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return a promise that resolves when the node has mutated + */ +var addNewAttributes = Task.async(function* (selector, text, inspector) { + info(`Entering text "${text}" in new attribute field for node ${selector}`); + + let container = yield focusNode(selector, inspector); + ok(container, "The container for '" + selector + "' was found"); + + info("Listening for the markupmutation event"); + let nodeMutated = inspector.once("markupmutation"); + setEditableFieldValue(container.editor.newAttr, text, inspector); + yield nodeMutated; +}); + +/** + * Checks that a node has the given attributes. + * + * @param {String} selector The selector for the node to check. + * @param {Object} expected An object containing the attributes to check. + * e.g. {id: "id1", class: "someclass"} + * @param {TestActorFront} testActor The current TestActorFront instance. + * + * Note that node.getAttribute() returns attribute values provided by the HTML + * parser. The parser only provides unescaped entities so & will return &. + */ +var assertAttributes = Task.async(function* (selector, expected, testActor) { + let {attributes: actual} = yield testActor.getNodeInfo(selector); + + is(actual.length, Object.keys(expected).length, + "The node " + selector + " has the expected number of attributes."); + for (let attr in expected) { + let foundAttr = actual.find(({name}) => name === attr); + let foundValue = foundAttr ? foundAttr.value : undefined; + ok(foundAttr, "The node " + selector + " has the attribute " + attr); + is(foundValue, expected[attr], + "The node " + selector + " has the correct " + attr + " attribute value"); + } +}); + +/** + * Undo the last markup-view action and wait for the corresponding mutation to + * occur + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return a promise that resolves when the markup-mutation has been treated or + * rejects if no undo action is possible + */ +function undoChange(inspector) { + let canUndo = inspector.markup.undo.canUndo(); + ok(canUndo, "The last change in the markup-view can be undone"); + if (!canUndo) { + return promise.reject(); + } + + let mutated = inspector.once("markupmutation"); + inspector.markup.undo.undo(); + return mutated; +} + +/** + * Redo the last markup-view action and wait for the corresponding mutation to + * occur + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return a promise that resolves when the markup-mutation has been treated or + * rejects if no redo action is possible + */ +function redoChange(inspector) { + let canRedo = inspector.markup.undo.canRedo(); + ok(canRedo, "The last change in the markup-view can be redone"); + if (!canRedo) { + return promise.reject(); + } + + let mutated = inspector.once("markupmutation"); + inspector.markup.undo.redo(); + return mutated; +} + +/** + * Get the selector-search input box from the inspector panel + * @return {DOMNode} + */ +function getSelectorSearchBox(inspector) { + return inspector.panelWin.document.getElementById("inspector-searchbox"); +} + +/** + * Using the inspector panel's selector search box, search for a given selector. + * The selector input string will be entered in the input field and the <ENTER> + * keypress will be simulated. + * This function won't wait for any events and is not async. It's up to callers + * to subscribe to events and react accordingly. + */ +function searchUsingSelectorSearch(selector, inspector) { + info("Entering \"" + selector + "\" into the selector-search input field"); + let field = getSelectorSearchBox(inspector); + field.focus(); + field.value = selector; + EventUtils.sendKey("return", inspector.panelWin); +} + +/** + * Check to see if the inspector menu items for editing are disabled. + * Things like Edit As HTML, Delete Node, etc. + * @param {NodeFront} nodeFront + * @param {InspectorPanel} inspector + * @param {Boolean} assert Should this function run assertions inline. + * @return A promise that resolves with a boolean indicating whether + * the menu items are disabled once the menu has been checked. + */ +var isEditingMenuDisabled = Task.async( +function* (nodeFront, inspector, assert = true) { + // To ensure clipboard contains something to paste. + clipboard.set("<p>test</p>", "html"); + + yield selectNode(nodeFront, inspector); + let allMenuItems = openContextMenuAndGetAllItems(inspector); + + let deleteMenuItem = allMenuItems.find(i => i.id === "node-menu-delete"); + let editHTMLMenuItem = allMenuItems.find(i => i.id === "node-menu-edithtml"); + let pasteHTMLMenuItem = allMenuItems.find(i => i.id === "node-menu-pasteouterhtml"); + + if (assert) { + ok(deleteMenuItem.disabled, "Delete menu item is disabled"); + ok(editHTMLMenuItem.disabled, "Edit HTML menu item is disabled"); + ok(pasteHTMLMenuItem.disabled, "Paste HTML menu item is disabled"); + } + + return deleteMenuItem.disabled && + editHTMLMenuItem.disabled && + pasteHTMLMenuItem.disabled; +}); + +/** + * Check to see if the inspector menu items for editing are enabled. + * Things like Edit As HTML, Delete Node, etc. + * @param {NodeFront} nodeFront + * @param {InspectorPanel} inspector + * @param {Boolean} assert Should this function run assertions inline. + * @return A promise that resolves with a boolean indicating whether + * the menu items are enabled once the menu has been checked. + */ +var isEditingMenuEnabled = Task.async( +function* (nodeFront, inspector, assert = true) { + // To ensure clipboard contains something to paste. + clipboard.set("<p>test</p>", "html"); + + yield selectNode(nodeFront, inspector); + let allMenuItems = openContextMenuAndGetAllItems(inspector); + + let deleteMenuItem = allMenuItems.find(i => i.id === "node-menu-delete"); + let editHTMLMenuItem = allMenuItems.find(i => i.id === "node-menu-edithtml"); + let pasteHTMLMenuItem = allMenuItems.find(i => i.id === "node-menu-pasteouterhtml"); + + if (assert) { + ok(!deleteMenuItem.disabled, "Delete menu item is enabled"); + ok(!editHTMLMenuItem.disabled, "Edit HTML menu item is enabled"); + ok(!pasteHTMLMenuItem.disabled, "Paste HTML menu item is enabled"); + } + + return !deleteMenuItem.disabled && + !editHTMLMenuItem.disabled && + !pasteHTMLMenuItem.disabled; +}); + +/** + * Wait for all current promises to be resolved. See this as executeSoon that + * can be used with yield. + */ +function promiseNextTick() { + let deferred = defer(); + executeSoon(deferred.resolve); + return deferred.promise; +} + +/** + * Collapses the current text selection in an input field and tabs to the next + * field. + */ +function collapseSelectionAndTab(inspector) { + // collapse selection and move caret to end + EventUtils.sendKey("tab", inspector.panelWin); + // next element + EventUtils.sendKey("tab", inspector.panelWin); +} + +/** + * Collapses the current text selection in an input field and tabs to the + * previous field. + */ +function collapseSelectionAndShiftTab(inspector) { + // collapse selection and move caret to end + EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, + inspector.panelWin); + // previous element + EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, + inspector.panelWin); +} + +/** + * Check that the current focused element is an attribute element in the markup + * view. + * @param {String} attrName The attribute name expected to be found + * @param {Boolean} editMode Whether or not the attribute should be in edit mode + */ +function checkFocusedAttribute(attrName, editMode) { + let focusedAttr = Services.focus.focusedElement; + ok(focusedAttr, "Has a focused element"); + + let dataAttr = focusedAttr.parentNode.dataset.attr; + is(dataAttr, attrName, attrName + " attribute editor is currently focused."); + if (editMode) { + // Using a multiline editor for attributes, the focused element should be a textarea. + is(focusedAttr.tagName, "textarea", attrName + "is in edit mode"); + } else { + is(focusedAttr.tagName, "span", attrName + "is not in edit mode"); + } +} + +/** + * Get attributes for node as how they are represented in editor. + * + * @param {String} selector + * @param {InspectorPanel} inspector + * @return {Promise} + * A promise that resolves with an array of attribute names + * (e.g. ["id", "class", "href"]) + */ +var getAttributesFromEditor = Task.async(function* (selector, inspector) { + let nodeList = (yield getContainerForSelector(selector, inspector)) + .tagLine.querySelectorAll("[data-attr]"); + + return [...nodeList].map(node => node.getAttribute("data-attr")); +}); + +// The expand all operation of the markup-view calls itself recursively and +// there's not one event we can wait for to know when it's done so use this +// helper function to wait until all recursive children updates are done. +function* waitForMultipleChildrenUpdates(inspector) { + // As long as child updates are queued up while we wait for an update already + // wait again + if (inspector.markup._queuedChildUpdates && + inspector.markup._queuedChildUpdates.size) { + yield waitForChildrenUpdated(inspector); + return yield waitForMultipleChildrenUpdates(inspector); + } + return undefined; +} + +/** + * Create an HTTP server that can be used to simulate custom requests within + * a test. It is automatically cleaned up when the test ends, so no need to + * call `destroy`. + * + * See https://developer.mozilla.org/en-US/docs/Httpd.js/HTTP_server_for_unit_tests + * for more information about how to register handlers. + * + * The server can be accessed like: + * + * const server = createTestHTTPServer(); + * let url = "http://localhost: " + server.identity.primaryPort + "/path"; + * + * @returns {HttpServer} + */ +function createTestHTTPServer() { + const {HttpServer} = Cu.import("resource://testing-common/httpd.js", {}); + let server = new HttpServer(); + + registerCleanupFunction(function* cleanup() { + let destroyed = defer(); + server.stop(() => { + destroyed.resolve(); + }); + yield destroyed.promise; + }); + + server.start(-1); + return server; +} + +/** + * Registers new backend tab actor. + * + * @param {DebuggerClient} client RDP client object (toolbox.target.client) + * @param {Object} options Configuration object with the following options: + * + * - moduleUrl {String}: URL of the module that contains actor implementation. + * - prefix {String}: prefix of the actor. + * - actorClass {ActorClassWithSpec}: Constructor object for the actor. + * - frontClass {FrontClassWithSpec}: Constructor object for the front part + * of the registered actor. + * + * @returns {Promise} A promise that is resolved when the actor is registered. + * The resolved value has two properties: + * + * - registrar {ActorActor}: A handle to the registered actor that allows + * unregistration. + * - form {Object}: The JSON actor form provided by the server. + */ +function registerTabActor(client, options) { + let moduleUrl = options.moduleUrl; + + return client.listTabs().then(response => { + let config = { + prefix: options.prefix, + constructor: options.actorClass, + type: { tab: true }, + }; + + // Register the custom actor on the backend. + let registry = ActorRegistryFront(client, response); + return registry.registerActor(moduleUrl, config).then(registrar => { + return client.getTab().then(tabResponse => ({ + registrar: registrar, + form: tabResponse.tab + })); + }); + }); +} + +/** + * A helper for unregistering an existing backend actor. + * + * @param {ActorActor} registrar A handle to the registered actor + * that has been received after registration. + * @param {Front} Corresponding front object. + * + * @returns A promise that is resolved when the unregistration + * has finished. + */ +function unregisterActor(registrar, front) { + return front.detach().then(() => { + return registrar.unregister(); + }); +} + +/** + * Simulate dragging a MarkupContainer by calling its mousedown and mousemove + * handlers. + * @param {InspectorPanel} inspector The current inspector-panel instance. + * @param {String|MarkupContainer} selector The selector to identify the node or + * the MarkupContainer for this node. + * @param {Number} xOffset Optional x offset to drag by. + * @param {Number} yOffset Optional y offset to drag by. + */ +function* simulateNodeDrag(inspector, selector, xOffset = 10, yOffset = 10) { + let container = typeof selector === "string" + ? yield getContainerForSelector(selector, inspector) + : selector; + let rect = container.tagLine.getBoundingClientRect(); + let scrollX = inspector.markup.doc.documentElement.scrollLeft; + let scrollY = inspector.markup.doc.documentElement.scrollTop; + + info("Simulate mouseDown on element " + selector); + container._onMouseDown({ + target: container.tagLine, + button: 0, + pageX: scrollX + rect.x, + pageY: scrollY + rect.y, + stopPropagation: () => {}, + preventDefault: () => {} + }); + + // _onMouseDown selects the node, so make sure to wait for the + // inspector-updated event if the current selection was different. + if (inspector.selection.nodeFront !== container.node) { + yield inspector.once("inspector-updated"); + } + + info("Simulate mouseMove on element " + selector); + container._onMouseMove({ + pageX: scrollX + rect.x + xOffset, + pageY: scrollY + rect.y + yOffset + }); +} + +/** + * Simulate dropping a MarkupContainer by calling its mouseup handler. This is + * meant to be called after simulateNodeDrag has been called. + * @param {InspectorPanel} inspector The current inspector-panel instance. + * @param {String|MarkupContainer} selector The selector to identify the node or + * the MarkupContainer for this node. + */ +function* simulateNodeDrop(inspector, selector) { + info("Simulate mouseUp on element " + selector); + let container = typeof selector === "string" + ? yield getContainerForSelector(selector, inspector) + : selector; + container._onMouseUp(); + inspector.markup._onMouseUp(); +} + +/** + * Simulate drag'n'dropping a MarkupContainer by calling its mousedown, + * mousemove and mouseup handlers. + * @param {InspectorPanel} inspector The current inspector-panel instance. + * @param {String|MarkupContainer} selector The selector to identify the node or + * the MarkupContainer for this node. + * @param {Number} xOffset Optional x offset to drag by. + * @param {Number} yOffset Optional y offset to drag by. + */ +function* simulateNodeDragAndDrop(inspector, selector, xOffset, yOffset) { + yield simulateNodeDrag(inspector, selector, xOffset, yOffset); + yield simulateNodeDrop(inspector, selector); +} + +/** + * Waits until the element has not scrolled for 30 consecutive frames. + */ +function* waitForScrollStop(doc) { + let el = doc.documentElement; + let win = doc.defaultView; + let lastScrollTop = el.scrollTop; + let stopFrameCount = 0; + while (stopFrameCount < 30) { + // Wait for a frame. + yield new Promise(resolve => win.requestAnimationFrame(resolve)); + + // Check if the element has scrolled. + if (lastScrollTop == el.scrollTop) { + // No scrolling since the last frame. + stopFrameCount++; + } else { + // The element has scrolled. Reset the frame counter. + stopFrameCount = 0; + lastScrollTop = el.scrollTop; + } + } + + return lastScrollTop; +} + +/** + * Select a node in the inspector and try to delete it using the provided key. After that, + * check that the expected element is focused. + * + * @param {InspectorPanel} inspector + * The current inspector-panel instance. + * @param {String} key + * The key to simulate to delete the node + * @param {Object} + * - {String} selector: selector of the element to delete. + * - {String} focusedSelector: selector of the element that should be selected + * after deleting the node. + * - {String} pseudo: optional, "before" or "after" if the element focused after + * deleting the node is supposed to be a before/after pseudo-element. + */ +function* checkDeleteAndSelection(inspector, key, {selector, focusedSelector, pseudo}) { + info("Test deleting node " + selector + " with " + key + ", " + + "expecting " + focusedSelector + " to be focused"); + + info("Select node " + selector + " and make sure it is focused"); + yield selectNode(selector, inspector); + yield clickContainer(selector, inspector); + + info("Delete the node with: " + key); + let mutated = inspector.once("markupmutation"); + EventUtils.sendKey(key, inspector.panelWin); + yield Promise.all([mutated, inspector.once("inspector-updated")]); + + let nodeFront = yield getNodeFront(focusedSelector, inspector); + if (pseudo) { + // Update the selector for logging in case of failure. + focusedSelector = focusedSelector + "::" + pseudo; + // Retrieve the :before or :after pseudo element of the nodeFront. + let {nodes} = yield inspector.walker.children(nodeFront); + nodeFront = pseudo === "before" ? nodes[0] : nodes[nodes.length - 1]; + } + + is(inspector.selection.nodeFront, nodeFront, + focusedSelector + " is selected after deletion"); + + info("Check that the node was really removed"); + let node = yield getNodeFront(selector, inspector); + ok(!node, "The node can't be found in the page anymore"); + + info("Undo the deletion to restore the original markup"); + yield undoChange(inspector); + node = yield getNodeFront(selector, inspector); + ok(node, "The node is back"); +} diff --git a/devtools/client/inspector/markup/test/helper_attributes_test_runner.js b/devtools/client/inspector/markup/test/helper_attributes_test_runner.js new file mode 100644 index 000000000..20446d3d1 --- /dev/null +++ b/devtools/client/inspector/markup/test/helper_attributes_test_runner.js @@ -0,0 +1,160 @@ +/* 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/. */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from head.js */ +"use strict"; + +/** + * Run a series of add-attributes tests. + * This function will iterate over the provided tests array and run each test. + * Each test's goal is to provide some text to be entered into the test node's + * new-attribute field and check that the given attributes have been created. + * After each test has run, the markup-view's undo command will be called and + * the test runner will check if all the new attributes are gone. + * @param {Array} tests See runAddAttributesTest for the structure + * @param {DOMNode|String} nodeOrSelector The node or node selector + * corresponding to an element on the current test page that has *no attributes* + * when the test starts. It will be used to add and remove attributes. + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * opened + * @param {TestActorFront} testActor The current TestActorFront instance. + * @return a promise that resolves when the tests have run + */ +function runAddAttributesTests(tests, nodeOrSelector, inspector, testActor) { + info("Running " + tests.length + " add-attributes tests"); + return Task.spawn(function* () { + info("Selecting the test node"); + yield selectNode("div", inspector); + + for (let test of tests) { + yield runAddAttributesTest(test, "div", inspector, testActor); + } + }); +} + +/** + * Run a single add-attribute test. + * See runAddAttributesTests for a description. + * @param {Object} test A test object should contain the following properties: + * - desc {String} a textual description for that test, to help when + * reading logs + * - text {String} the string to be inserted into the new attribute field + * - expectedAttributes {Object} a key/value pair object that will be + * used to check the attributes on the test element + * - validate {Function} optional extra function that will be called + * after the attributes have been added and which should be used to + * assert some more things this test runner might not be checking. The + * function will be called with the following arguments: + * - {DOMNode} The element being tested + * - {MarkupContainer} The corresponding container in the markup-view + * - {InspectorPanel} The instance of the InspectorPanel opened + * @param {String} selector The node selector corresponding to the test element + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * @param {TestActorFront} testActor The current TestActorFront instance. + * opened + */ +function* runAddAttributesTest(test, selector, inspector, testActor) { + if (test.setUp) { + test.setUp(inspector); + } + + info("Starting add-attribute test: " + test.desc); + yield addNewAttributes(selector, test.text, inspector); + + info("Assert that the attribute(s) has/have been applied correctly"); + yield assertAttributes(selector, test.expectedAttributes, testActor); + + if (test.validate) { + let container = yield getContainerForSelector(selector, inspector); + test.validate(container, inspector); + } + + info("Undo the change"); + yield undoChange(inspector); + + info("Assert that the attribute(s) has/have been removed correctly"); + yield assertAttributes(selector, {}, testActor); + if (test.tearDown) { + test.tearDown(inspector); + } +} + +/** + * Run a series of edit-attributes tests. + * This function will iterate over the provided tests array and run each test. + * Each test's goal is to locate a given element on the current test page, + * assert its current attributes, then provide the name of one of them and a + * value to be set into it, and then check if the new attributes are correct. + * After each test has run, the markup-view's undo and redo commands will be + * called and the test runner will assert again that the attributes are correct. + * @param {Array} tests See runEditAttributesTest for the structure + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * opened + * @param {TestActorFront} testActor The current TestActorFront instance. + * @return a promise that resolves when the tests have run + */ +function runEditAttributesTests(tests, inspector, testActor) { + info("Running " + tests.length + " edit-attributes tests"); + return Task.spawn(function* () { + info("Expanding all nodes in the markup-view"); + yield inspector.markup.expandAll(); + + for (let test of tests) { + yield runEditAttributesTest(test, inspector, testActor); + } + }); +} + +/** + * Run a single edit-attribute test. + * See runEditAttributesTests for a description. + * @param {Object} test A test object should contain the following properties: + * - desc {String} a textual description for that test, to help when + * reading logs + * - node {String} a css selector that will be used to select the node + * which will be tested during this iteration + * - originalAttributes {Object} a key/value pair object that will be + * used to check the attributes of the node before the test runs + * - name {String} the name of the attribute to focus the editor for + * - value {String} the new value to be typed in the focused editor + * - expectedAttributes {Object} a key/value pair object that will be + * used to check the attributes on the test element + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * @param {TestActorFront} testActor The current TestActorFront instance. + * opened + */ +function* runEditAttributesTest(test, inspector, testActor) { + info("Starting edit-attribute test: " + test.desc); + + info("Selecting the test node " + test.node); + yield selectNode(test.node, inspector); + + info("Asserting that the node has the right attributes to start with"); + yield assertAttributes(test.node, test.originalAttributes, testActor); + + info("Editing attribute " + test.name + " with value " + test.value); + + let container = yield focusNode(test.node, inspector); + ok(container && container.editor, "The markup-container for " + test.node + + " was found"); + + info("Listening for the markupmutation event"); + let nodeMutated = inspector.once("markupmutation"); + let attr = container.editor.attrElements.get(test.name) + .querySelector(".editable"); + setEditableFieldValue(attr, test.value, inspector); + yield nodeMutated; + + info("Asserting the new attributes after edition"); + yield assertAttributes(test.node, test.expectedAttributes, testActor); + + info("Undo the change and assert that the attributes have been changed back"); + yield undoChange(inspector); + yield assertAttributes(test.node, test.originalAttributes, testActor); + + info("Redo the change and assert that the attributes have been changed " + + "again"); + yield redoChange(inspector); + yield assertAttributes(test.node, test.expectedAttributes, testActor); +} diff --git a/devtools/client/inspector/markup/test/helper_events_test_runner.js b/devtools/client/inspector/markup/test/helper_events_test_runner.js new file mode 100644 index 000000000..acef334fb --- /dev/null +++ b/devtools/client/inspector/markup/test/helper_events_test_runner.js @@ -0,0 +1,111 @@ +/* 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/. */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from head.js */ +"use strict"; + +/** + * Generator function that runs checkEventsForNode() for each object in the + * TEST_DATA array. + */ +function* runEventPopupTests(url, tests) { + let {inspector, testActor} = yield openInspectorForURL(url); + + yield inspector.markup.expandAll(); + + for (let test of tests) { + yield checkEventsForNode(test, inspector, testActor); + } + + // Wait for promises to avoid leaks when running this as a single test. + // We need to do this because we have opened a bunch of popups and don't them + // to affect other test runs when they are GCd. + yield promiseNextTick(); +} + +/** + * Generator function that takes a selector and expected results and returns + * the event info. + * + * @param {Object} test + * A test object should contain the following properties: + * - selector {String} a css selector targeting the node to edit + * - expected {Array} array of expected event objects + * - type {String} event type + * - filename {String} filename:line where the evt handler is defined + * - attributes {Array} array of event attributes ({String}) + * - handler {String} string representation of the handler + * - beforeTest {Function} (optional) a function to execute on the page + * before running the test + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * opened + * @param {TestActorFront} testActor + */ +function* checkEventsForNode(test, inspector, testActor) { + let {selector, expected, beforeTest} = test; + let container = yield getContainerForSelector(selector, inspector); + + if (typeof beforeTest === "function") { + yield beforeTest(inspector, testActor); + } + + let evHolder = container.elt.querySelector(".markupview-events"); + + if (expected.length === 0) { + // if no event is expected, simply check that the event bubble is hidden + is(evHolder.style.display, "none", "event bubble should be hidden"); + return; + } + + let tooltip = inspector.markup.eventDetailsTooltip; + + yield selectNode(selector, inspector); + + // Click button to show tooltip + info("Clicking evHolder"); + EventUtils.synthesizeMouseAtCenter(evHolder, {}, + inspector.markup.doc.defaultView); + yield tooltip.once("shown"); + info("tooltip shown"); + + // Check values + let headers = tooltip.panel.querySelectorAll(".event-header"); + let nodeFront = container.node; + let cssSelector = nodeFront.nodeName + "#" + nodeFront.id; + + for (let i = 0; i < headers.length; i++) { + info("Processing header[" + i + "] for " + cssSelector); + + let header = headers[i]; + let type = header.querySelector(".event-tooltip-event-type"); + let filename = header.querySelector(".event-tooltip-filename"); + let attributes = header.querySelectorAll(".event-tooltip-attributes"); + let contentBox = header.nextElementSibling; + + is(type.textContent, expected[i].type, + "type matches for " + cssSelector); + is(filename.textContent, expected[i].filename, + "filename matches for " + cssSelector); + + is(attributes.length, expected[i].attributes.length, + "we have the correct number of attributes"); + + for (let j = 0; j < expected[i].attributes.length; j++) { + is(attributes[j].textContent, expected[i].attributes[j], + "attribute[" + j + "] matches for " + cssSelector); + } + + // Make sure the header is not hidden by scrollbars before clicking. + header.scrollIntoView(); + + EventUtils.synthesizeMouseAtCenter(header, {}, type.ownerGlobal); + yield tooltip.once("event-tooltip-ready"); + + let editor = tooltip.eventTooltip._eventEditors.get(contentBox).editor; + is(editor.getText(), expected[i].handler, + "handler matches for " + cssSelector); + } + + tooltip.hide(); +} diff --git a/devtools/client/inspector/markup/test/helper_markup_accessibility_navigation.js b/devtools/client/inspector/markup/test/helper_markup_accessibility_navigation.js new file mode 100644 index 000000000..a49f1e7ba --- /dev/null +++ b/devtools/client/inspector/markup/test/helper_markup_accessibility_navigation.js @@ -0,0 +1,70 @@ +/* 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/. */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from head.js */ +"use strict"; + +/** + * Execute a keyboard event and check that the state is as expected (focused element, aria + * attribute etc...). + * + * @param {InspectorPanel} inspector + * Current instance of the inspector being tested. + * @param {Object} elms + * Map of elements that will be used to retrieve live references to children + * elements + * @param {Element} focused + * Element expected to be focused + * @param {Element} activedescendant + * Element expected to be the aria activedescendant of the root node + */ +function testNavigationState(inspector, elms, focused, activedescendant) { + let doc = inspector.markup.doc; + let id = activedescendant.getAttribute("id"); + is(doc.activeElement, focused, `Keyboard focus should be set to ${focused}`); + is(elms.root.elt.getAttribute("aria-activedescendant"), id, + `Active descendant should be set to ${id}`); +} + +/** + * Execute a keyboard event and check that the state is as expected (focused element, aria + * attribute etc...). + * + * @param {InspectorPanel} inspector + * Current instance of the inspector being tested. + * @param {Object} elms + * MarkupContainers/Elements that will be used to retrieve references to other + * elements based on objects' paths. + * @param {Object} testData + * - {String} desc: description for better logging. + * - {String} key: keyboard event's key. + * - {Object} options, optional: event data such as shiftKey, etc. + * - {String} focused: path to expected focused element in elms map. + * - {String} activedescendant: path to expected aria-activedescendant element in + * elms map. + * - {String} waitFor, optional: markupview event to wait for if keyboard actions + * result in async updates. Also accepts the inspector event "inspector-updated". + */ +function* runAccessibilityNavigationTest(inspector, elms, + {desc, key, options, focused, activedescendant, waitFor}) { + info(desc); + + let markup = inspector.markup; + let doc = markup.doc; + let win = doc.defaultView; + + let updated; + if (waitFor) { + updated = waitFor === "inspector-updated" ? + inspector.once(waitFor) : markup.once(waitFor); + } else { + updated = Promise.resolve(); + } + EventUtils.synthesizeKey(key, options, win); + yield updated; + + let focusedElement = lookupPath(elms, focused); + let activeDescendantElement = lookupPath(elms, activedescendant); + testNavigationState(inspector, elms, focusedElement, activeDescendantElement); +} diff --git a/devtools/client/inspector/markup/test/helper_outerhtml_test_runner.js b/devtools/client/inspector/markup/test/helper_outerhtml_test_runner.js new file mode 100644 index 000000000..f2de0876f --- /dev/null +++ b/devtools/client/inspector/markup/test/helper_outerhtml_test_runner.js @@ -0,0 +1,82 @@ +/* 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/. */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from head.js */ +"use strict"; + +/** + * Run a series of edit-outer-html tests. + * This function will iterate over the provided tests array and run each test. + * Each test's goal is to provide a node (a selector) and a new outer-HTML to be + * inserted in place of the current one for that node. + * This test runner will wait for the mutation event to be fired and will check + * a few things. Each test may also provide its own validate function to perform + * assertions and verify that the new outer html is correct. + * @param {Array} tests See runEditOuterHTMLTest for the structure + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * opened + * @param {TestActorFront} testActor The current TestActorFront instance + * @return a promise that resolves when the tests have run + */ +function runEditOuterHTMLTests(tests, inspector, testActor) { + info("Running " + tests.length + " edit-outer-html tests"); + return Task.spawn(function* () { + for (let step of tests) { + yield runEditOuterHTMLTest(step, inspector, testActor); + } + }); +} + +/** + * Run a single edit-outer-html test. + * See runEditOuterHTMLTests for a description. + * @param {Object} test A test object should contain the following properties: + * - selector {String} a css selector targeting the node to edit + * - oldHTML {String} + * - newHTML {String} + * - validate {Function} will be executed when the edition test is done, + * after the new outer-html has been inserted. Should be used to verify + * the actual DOM, see if it corresponds to the newHTML string provided + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * @param {TestActorFront} testActor The current TestActorFront instance + * opened + */ +function* runEditOuterHTMLTest(test, inspector, testActor) { + info("Running an edit outerHTML test on '" + test.selector + "'"); + yield selectNode(test.selector, inspector); + + let onUpdated = inspector.once("inspector-updated"); + + info("Listen for reselectedonremoved and edit the outerHTML"); + let onReselected = inspector.markup.once("reselectedonremoved"); + yield inspector.markup.updateNodeOuterHTML(inspector.selection.nodeFront, + test.newHTML, test.oldHTML); + yield onReselected; + + // Typically selectedNode will === pageNode, but if a new element has been + // injected in front of it, this will not be the case. If this happens. + let selectedNodeFront = inspector.selection.nodeFront; + let pageNodeFront = yield inspector.walker.querySelector( + inspector.walker.rootNode, test.selector); + + if (test.validate) { + yield test.validate({pageNodeFront, selectedNodeFront, + inspector, testActor}); + } else { + is(pageNodeFront, selectedNodeFront, + "Original node (grabbed by selector) is selected"); + let {outerHTML} = yield testActor.getNodeInfo(test.selector); + is(outerHTML, test.newHTML, "Outer HTML has been updated"); + } + + // Wait for the inspector to be fully updated to avoid causing errors by + // abruptly closing hanging requests when the test ends + yield onUpdated; + + let closeTagLine = inspector.markup.getContainer(pageNodeFront).closeTagLine; + if (closeTagLine) { + is(closeTagLine.querySelectorAll(".theme-fg-contrast").length, 0, + "No contrast class"); + } +} diff --git a/devtools/client/inspector/markup/test/helper_style_attr_test_runner.js b/devtools/client/inspector/markup/test/helper_style_attr_test_runner.js new file mode 100644 index 000000000..f884a8181 --- /dev/null +++ b/devtools/client/inspector/markup/test/helper_style_attr_test_runner.js @@ -0,0 +1,132 @@ +/* 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/. */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from head.js */ +"use strict"; + +/** + * Perform an style attribute edition and autocompletion test in the test + * url, for #node14. Test data should be an + * array of arrays structured as follows : + * [ + * what key to press, + * expected input box value after keypress, + * expected input.selectionStart, + * expected input.selectionEnd, + * is popup expected to be open ? + * ] + * + * The test will start by adding a new attribute on the node, and then send each + * key specified in the testData. The last item of this array should leave the + * new attribute editor, either by committing or cancelling the edit. + * + * @param {InspectorPanel} inspector + * @param {Array} testData + * Array of arrays representing the characters to type for the new + * attribute as well as the expected state at each step + */ +function* runStyleAttributeAutocompleteTests(inspector, testData) { + info("Expand all markup nodes"); + yield inspector.markup.expandAll(); + + info("Select #node14"); + let container = yield focusNode("#node14", inspector); + + info("Focus and open the new attribute inplace-editor"); + let attr = container.editor.newAttr; + attr.focus(); + EventUtils.sendKey("return", inspector.panelWin); + let editor = inplaceEditor(attr); + + for (let i = 0; i < testData.length; i++) { + let data = testData[i]; + + // Expect a markupmutation event at the last iteration since that's when the + // attribute is actually created. + let onMutation = i === testData.length - 1 + ? inspector.once("markupmutation") : null; + + info(`Entering test data ${i}: ${data[0]}, expecting: [${data[1]}]`); + yield enterData(data, editor, inspector); + + info(`Test data ${i} entered. Checking state.`); + yield checkData(data, editor, inspector); + + yield onMutation; + } + + // Undoing the action will remove the new attribute, so make sure to wait for + // the markupmutation event here again. + let onMutation = inspector.once("markupmutation"); + while (inspector.markup.undo.canUndo()) { + yield undoChange(inspector); + } + yield onMutation; +} + +/** + * Process a test data entry. + * @param {Array} data + * test data - click or key - to enter + * @param {InplaceEditor} editor + * @param {InspectorPanel} inspector + * @return {Promise} promise that will resolve when the test data has been + * applied + */ +function enterData(data, editor, inspector) { + let key = data[0]; + + if (/^click_[0-9]+$/.test(key)) { + let suggestionIndex = parseInt(key.split("_")[1], 10); + return clickOnSuggestion(suggestionIndex, editor); + } + + return sendKey(key, editor, inspector); +} + +function clickOnSuggestion(index, editor) { + return new Promise(resolve => { + info("Clicking on item " + index + " in the list"); + editor.once("after-suggest", () => executeSoon(resolve)); + editor.popup._list.childNodes[index].click(); + }); +} + +function sendKey(key, editor, inspector) { + return new Promise(resolve => { + if (/(down|left|right|back_space|return)/ig.test(key)) { + info("Adding event listener for down|left|right|back_space|return keys"); + editor.input.addEventListener("keypress", function onKeypress() { + if (editor.input) { + editor.input.removeEventListener("keypress", onKeypress); + } + executeSoon(resolve); + }); + } else { + editor.once("after-suggest", () => executeSoon(resolve)); + } + + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + }); +} + +/** + * Verify that the inplace editor is in the expected state for the provided + * test data. + */ +function* checkData(data, editor, inspector) { + let [, completion, selStart, selEnd, popupOpen] = data; + + if (selEnd != -1) { + is(editor.input.value, completion, "Completed value is correct"); + is(editor.input.selectionStart, selStart, "Selection start position is correct"); + is(editor.input.selectionEnd, selEnd, "Selection end position is correct"); + is(editor.popup.isOpen, popupOpen, "Popup is " + (popupOpen ? "open" : "closed")); + } else { + let nodeFront = yield getNodeFront("#node14", inspector); + let container = getContainerForNodeFront(nodeFront, inspector); + let attr = container.editor.attrElements.get("style").querySelector(".editable"); + is(attr.textContent, completion, "Correct value is persisted after pressing Enter"); + } +} diff --git a/devtools/client/inspector/markup/test/lib_jquery_1.0.js b/devtools/client/inspector/markup/test/lib_jquery_1.0.js new file mode 100644 index 000000000..564361282 --- /dev/null +++ b/devtools/client/inspector/markup/test/lib_jquery_1.0.js @@ -0,0 +1,1814 @@ +/* + * jQuery - New Wave Javascript + * + * Copyright (c) 2006 John Resig (jquery.com) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * $Date: 2006-10-27 23:14:48 -0400 (Fri, 27 Oct 2006) $ + * $Rev: 509 $ + */ + +// Global undefined variable +window.undefined = window.undefined; +function jQuery(a,c) { + + // Shortcut for document ready (because $(document).each() is silly) + if ( a && a.constructor == Function && jQuery.fn.ready ) + return jQuery(document).ready(a); + + // Make sure that a selection was provided + a = a || jQuery.context || document; + + // Watch for when a jQuery object is passed as the selector + if ( a.jquery ) + return $( jQuery.merge( a, [] ) ); + + // Watch for when a jQuery object is passed at the context + if ( c && c.jquery ) + return $( c ).find(a); + + // If the context is global, return a new object + if ( window == this ) + return new jQuery(a,c); + + // Handle HTML strings + var m = /^[^<]*(<.+>)[^>]*$/.exec(a); + if ( m ) a = jQuery.clean( [ m[1] ] ); + + // Watch for when an array is passed in + this.get( a.constructor == Array || a.length && !a.nodeType && a[0] != undefined && a[0].nodeType ? + // Assume that it is an array of DOM Elements + jQuery.merge( a, [] ) : + + // Find the matching elements and save them for later + jQuery.find( a, c ) ); + + // See if an extra function was provided + var fn = arguments[ arguments.length - 1 ]; + + // If so, execute it in context + if ( fn && fn.constructor == Function ) + this.each(fn); +} + +// Map over the $ in case of overwrite +if ( $ ) + jQuery._$ = $; + +// Map the jQuery namespace to the '$' one +var $ = jQuery; + +jQuery.fn = jQuery.prototype = { + jquery: "$Rev: 509 $", + + size: function() { + return this.length; + }, + + get: function( num ) { + // Watch for when an array (of elements) is passed in + if ( num && num.constructor == Array ) { + + // Use a tricky hack to make the jQuery object + // look and feel like an array + this.length = 0; + [].push.apply( this, num ); + + return this; + } else + return num == undefined ? + + // Return a 'clean' array + jQuery.map( this, function(a){ return a } ) : + + // Return just the object + this[num]; + }, + each: function( fn, args ) { + return jQuery.each( this, fn, args ); + }, + + index: function( obj ) { + var pos = -1; + this.each(function(i){ + if ( this == obj ) pos = i; + }); + return pos; + }, + + attr: function( key, value, type ) { + // Check to see if we're setting style values + return key.constructor != String || value != undefined ? + this.each(function(){ + // See if we're setting a hash of styles + if ( value == undefined ) + // Set all the styles + for ( var prop in key ) + jQuery.attr( + type ? this.style : this, + prop, key[prop] + ); + + // See if we're setting a single key/value style + else + jQuery.attr( + type ? this.style : this, + key, value + ); + }) : + + // Look for the case where we're accessing a style value + jQuery[ type || "attr" ]( this[0], key ); + }, + + css: function( key, value ) { + return this.attr( key, value, "curCSS" ); + }, + text: function(e) { + e = e || this; + var t = ""; + for ( var j = 0; j < e.length; j++ ) { + var r = e[j].childNodes; + for ( var i = 0; i < r.length; i++ ) + t += r[i].nodeType != 1 ? + r[i].nodeValue : jQuery.fn.text([ r[i] ]); + } + return t; + }, + wrap: function() { + // The elements to wrap the target around + var a = jQuery.clean(arguments); + + // Wrap each of the matched elements individually + return this.each(function(){ + // Clone the structure that we're using to wrap + var b = a[0].cloneNode(true); + + // Insert it before the element to be wrapped + this.parentNode.insertBefore( b, this ); + + // Find he deepest point in the wrap structure + while ( b.firstChild ) + b = b.firstChild; + + // Move the matched element to within the wrap structure + b.appendChild( this ); + }); + }, + append: function() { + return this.domManip(arguments, true, 1, function(a){ + this.appendChild( a ); + }); + }, + prepend: function() { + return this.domManip(arguments, true, -1, function(a){ + this.insertBefore( a, this.firstChild ); + }); + }, + before: function() { + return this.domManip(arguments, false, 1, function(a){ + this.parentNode.insertBefore( a, this ); + }); + }, + after: function() { + return this.domManip(arguments, false, -1, function(a){ + this.parentNode.insertBefore( a, this.nextSibling ); + }); + }, + end: function() { + return this.get( this.stack.pop() ); + }, + find: function(t) { + return this.pushStack( jQuery.map( this, function(a){ + return jQuery.find(t,a); + }), arguments ); + }, + + clone: function(deep) { + return this.pushStack( jQuery.map( this, function(a){ + return a.cloneNode( deep != undefined ? deep : true ); + }), arguments ); + }, + + filter: function(t) { + return this.pushStack( + t.constructor == Array && + jQuery.map(this,function(a){ + for ( var i = 0; i < t.length; i++ ) + if ( jQuery.filter(t[i],[a]).r.length ) + return a; + }) || + + t.constructor == Boolean && + ( t ? this.get() : [] ) || + + t.constructor == Function && + jQuery.grep( this, t ) || + + jQuery.filter(t,this).r, arguments ); + }, + + not: function(t) { + return this.pushStack( t.constructor == String ? + jQuery.filter(t,this,false).r : + jQuery.grep(this,function(a){ return a != t; }), arguments ); + }, + + add: function(t) { + return this.pushStack( jQuery.merge( this, t.constructor == String ? + jQuery.find(t) : t.constructor == Array ? t : [t] ), arguments ); + }, + is: function(expr) { + return expr ? jQuery.filter(expr,this).r.length > 0 : this.length > 0; + }, + domManip: function(args, table, dir, fn){ + var clone = this.size() > 1; + var a = jQuery.clean(args); + + return this.each(function(){ + var obj = this; + + if ( table && this.nodeName == "TABLE" && a[0].nodeName != "THEAD" ) { + var tbody = this.getElementsByTagName("tbody"); + + if ( !tbody.length ) { + obj = document.createElement("tbody"); + this.appendChild( obj ); + } else + obj = tbody[0]; + } + + for ( var i = ( dir < 0 ? a.length - 1 : 0 ); + i != ( dir < 0 ? dir : a.length ); i += dir ) { + fn.apply( obj, [ clone ? a[i].cloneNode(true) : a[i] ] ); + } + }); + }, + pushStack: function(a,args) { + var fn = args && args[args.length-1]; + + if ( !fn || fn.constructor != Function ) { + if ( !this.stack ) this.stack = []; + this.stack.push( this.get() ); + this.get( a ); + } else { + var old = this.get(); + this.get( a ); + if ( fn.constructor == Function ) + return this.each( fn ); + this.get( old ); + } + + return this; + } +}; + +jQuery.extend = jQuery.fn.extend = function(obj,prop) { + if ( !prop ) { prop = obj; obj = this; } + for ( var i in prop ) obj[i] = prop[i]; + return obj; +}; + +jQuery.extend({ + init: function(){ + jQuery.initDone = true; + + jQuery.each( jQuery.macros.axis, function(i,n){ + jQuery.fn[ i ] = function(a) { + var ret = jQuery.map(this,n); + if ( a && a.constructor == String ) + ret = jQuery.filter(a,ret).r; + return this.pushStack( ret, arguments ); + }; + }); + + jQuery.each( jQuery.macros.to, function(i,n){ + jQuery.fn[ i ] = function(){ + var a = arguments; + return this.each(function(){ + for ( var j = 0; j < a.length; j++ ) + $(a[j])[n]( this ); + }); + }; + }); + + jQuery.each( jQuery.macros.each, function(i,n){ + jQuery.fn[ i ] = function() { + return this.each( n, arguments ); + }; + }); + + jQuery.each( jQuery.macros.filter, function(i,n){ + jQuery.fn[ n ] = function(num,fn) { + return this.filter( ":" + n + "(" + num + ")", fn ); + }; + }); + + jQuery.each( jQuery.macros.attr, function(i,n){ + n = n || i; + jQuery.fn[ i ] = function(h) { + return h == undefined ? + this.length ? this[0][n] : null : + this.attr( n, h ); + }; + }); + + jQuery.each( jQuery.macros.css, function(i,n){ + jQuery.fn[ n ] = function(h) { + return h == undefined ? + ( this.length ? jQuery.css( this[0], n ) : null ) : + this.css( n, h ); + }; + }); + + }, + each: function( obj, fn, args ) { + if ( obj.length == undefined ) + for ( var i in obj ) + fn.apply( obj[i], args || [i, obj[i]] ); + else + for ( var i = 0; i < obj.length; i++ ) + fn.apply( obj[i], args || [i, obj[i]] ); + return obj; + }, + + className: { + add: function(o,c){ + if (jQuery.className.has(o,c)) return; + o.className += ( o.className ? " " : "" ) + c; + }, + remove: function(o,c){ + o.className = !c ? "" : + o.className.replace( + new RegExp("(^|\\s*\\b[^-])"+c+"($|\\b(?=[^-]))", "g"), ""); + }, + has: function(e,a) { + if ( e.className != undefined ) + e = e.className; + return new RegExp("(^|\\s)" + a + "(\\s|$)").test(e); + } + }, + swap: function(e,o,f) { + for ( var i in o ) { + e.style["old"+i] = e.style[i]; + e.style[i] = o[i]; + } + f.apply( e, [] ); + for ( var i in o ) + e.style[i] = e.style["old"+i]; + }, + + css: function(e,p) { + if ( p == "height" || p == "width" ) { + var old = {}, oHeight, oWidth, d = ["Top","Bottom","Right","Left"]; + + for ( var i in d ) { + old["padding" + d[i]] = 0; + old["border" + d[i] + "Width"] = 0; + } + + jQuery.swap( e, old, function() { + if (jQuery.css(e,"display") != "none") { + oHeight = e.offsetHeight; + oWidth = e.offsetWidth; + } else { + e = $(e.cloneNode(true)).css({ + visibility: "hidden", position: "absolute", display: "block" + }).prependTo("body")[0]; + + oHeight = e.clientHeight; + oWidth = e.clientWidth; + + e.parentNode.removeChild(e); + } + }); + + return p == "height" ? oHeight : oWidth; + } else if ( p == "opacity" && jQuery.browser.msie ) + return parseFloat( jQuery.curCSS(e,"filter").replace(/[^0-9.]/,"") ) || 1; + + return jQuery.curCSS( e, p ); + }, + + curCSS: function(elem, prop, force) { + var ret; + + if (!force && elem.style[prop]) { + + ret = elem.style[prop]; + + } else if (elem.currentStyle) { + + var newProp = prop.replace(/\-(\w)/g,function(m,c){return c.toUpperCase()}); + ret = elem.currentStyle[prop] || elem.currentStyle[newProp]; + + } else if (document.defaultView && document.defaultView.getComputedStyle) { + + prop = prop.replace(/([A-Z])/g,"-$1").toLowerCase(); + var cur = document.defaultView.getComputedStyle(elem, null); + + if ( cur ) + ret = cur.getPropertyValue(prop); + else if ( prop == 'display' ) + ret = 'none'; + else + jQuery.swap(elem, { display: 'block' }, function() { + ret = document.defaultView.getComputedStyle(this,null).getPropertyValue(prop); + }); + + } + + return ret; + }, + + clean: function(a) { + var r = []; + for ( var i = 0; i < a.length; i++ ) { + if ( a[i].constructor == String ) { + + var table = ""; + + if ( !a[i].indexOf("<thead") || !a[i].indexOf("<tbody") ) { + table = "thead"; + a[i] = "<table>" + a[i] + "</table>"; + } else if ( !a[i].indexOf("<tr") ) { + table = "tr"; + a[i] = "<table>" + a[i] + "</table>"; + } else if ( !a[i].indexOf("<td") || !a[i].indexOf("<th") ) { + table = "td"; + a[i] = "<table><tbody><tr>" + a[i] + "</tr></tbody></table>"; + } + + var div = document.createElement("div"); + div.innerHTML = a[i]; + + if ( table ) { + div = div.firstChild; + if ( table != "thead" ) div = div.firstChild; + if ( table == "td" ) div = div.firstChild; + } + + for ( var j = 0; j < div.childNodes.length; j++ ) + r.push( div.childNodes[j] ); + } else if ( a[i].jquery || a[i].length && !a[i].nodeType ) + for ( var k = 0; k < a[i].length; k++ ) + r.push( a[i][k] ); + else if ( a[i] !== null ) + r.push( a[i].nodeType ? a[i] : document.createTextNode(a[i].toString()) ); + } + return r; + }, + + expr: { + "": "m[2]== '*'||a.nodeName.toUpperCase()==m[2].toUpperCase()", + "#": "a.getAttribute('id')&&a.getAttribute('id')==m[2]", + ":": { + // Position Checks + lt: "i<m[3]-0", + gt: "i>m[3]-0", + nth: "m[3]-0==i", + eq: "m[3]-0==i", + first: "i==0", + last: "i==r.length-1", + even: "i%2==0", + odd: "i%2", + + // Child Checks + "first-child": "jQuery.sibling(a,0).cur", + "last-child": "jQuery.sibling(a,0).last", + "only-child": "jQuery.sibling(a).length==1", + + // Parent Checks + parent: "a.childNodes.length", + empty: "!a.childNodes.length", + + // Text Check + contains: "(a.innerText||a.innerHTML).indexOf(m[3])>=0", + + // Visibility + visible: "a.type!='hidden'&&jQuery.css(a,'display')!='none'&&jQuery.css(a,'visibility')!='hidden'", + hidden: "a.type=='hidden'||jQuery.css(a,'display')=='none'||jQuery.css(a,'visibility')=='hidden'", + + // Form elements + enabled: "!a.disabled", + disabled: "a.disabled", + checked: "a.checked", + selected: "a.selected" + }, + ".": "jQuery.className.has(a,m[2])", + "@": { + "=": "z==m[4]", + "!=": "z!=m[4]", + "^=": "!z.indexOf(m[4])", + "$=": "z.substr(z.length - m[4].length,m[4].length)==m[4]", + "*=": "z.indexOf(m[4])>=0", + "": "z" + }, + "[": "jQuery.find(m[2],a).length" + }, + + token: [ + "\\.\\.|/\\.\\.", "a.parentNode", + ">|/", "jQuery.sibling(a.firstChild)", + "\\+", "jQuery.sibling(a).next", + "~", function(a){ + var r = []; + var s = jQuery.sibling(a); + if ( s.n > 0 ) + for ( var i = s.n; i < s.length; i++ ) + r.push( s[i] ); + return r; + } + ], + find: function( t, context ) { + // Make sure that the context is a DOM Element + if ( context && context.nodeType == undefined ) + context = null; + + // Set the correct context (if none is provided) + context = context || jQuery.context || document; + + if ( t.constructor != String ) return [t]; + + if ( !t.indexOf("//") ) { + context = context.documentElement; + t = t.substr(2,t.length); + } else if ( !t.indexOf("/") ) { + context = context.documentElement; + t = t.substr(1,t.length); + // FIX Assume the root element is right :( + if ( t.indexOf("/") >= 1 ) + t = t.substr(t.indexOf("/"),t.length); + } + + var ret = [context]; + var done = []; + var last = null; + + while ( t.length > 0 && last != t ) { + var r = []; + last = t; + + t = jQuery.trim(t).replace( /^\/\//i, "" ); + + var foundToken = false; + + for ( var i = 0; i < jQuery.token.length; i += 2 ) { + var re = new RegExp("^(" + jQuery.token[i] + ")"); + var m = re.exec(t); + + if ( m ) { + r = ret = jQuery.map( ret, jQuery.token[i+1] ); + t = jQuery.trim( t.replace( re, "" ) ); + foundToken = true; + } + } + + if ( !foundToken ) { + if ( !t.indexOf(",") || !t.indexOf("|") ) { + if ( ret[0] == context ) ret.shift(); + done = jQuery.merge( done, ret ); + r = ret = [context]; + t = " " + t.substr(1,t.length); + } else { + var re2 = /^([#.]?)([a-z0-9\\*_-]*)/i; + var m = re2.exec(t); + + if ( m[1] == "#" ) { + // Ummm, should make this work in all XML docs + var oid = document.getElementById(m[2]); + r = ret = oid ? [oid] : []; + t = t.replace( re2, "" ); + } else { + if ( !m[2] || m[1] == "." ) m[2] = "*"; + + for ( var i = 0; i < ret.length; i++ ) + r = jQuery.merge( r, + m[2] == "*" ? + jQuery.getAll(ret[i]) : + ret[i].getElementsByTagName(m[2]) + ); + } + } + } + + if ( t ) { + var val = jQuery.filter(t,r); + ret = r = val.r; + t = jQuery.trim(val.t); + } + } + + if ( ret && ret[0] == context ) ret.shift(); + done = jQuery.merge( done, ret ); + + return done; + }, + + getAll: function(o,r) { + r = r || []; + var s = o.childNodes; + for ( var i = 0; i < s.length; i++ ) + if ( s[i].nodeType == 1 ) { + r.push( s[i] ); + jQuery.getAll( s[i], r ); + } + return r; + }, + + attr: function(elem, name, value){ + var fix = { + "for": "htmlFor", + "class": "className", + "float": "cssFloat", + innerHTML: "innerHTML", + className: "className" + }; + + if ( fix[name] ) { + if ( value != undefined ) elem[fix[name]] = value; + return elem[fix[name]]; + } else if ( elem.getAttribute ) { + if ( value != undefined ) elem.setAttribute( name, value ); + return elem.getAttribute( name, 2 ); + } else { + name = name.replace(/-([a-z])/ig,function(z,b){return b.toUpperCase();}); + if ( value != undefined ) elem[name] = value; + return elem[name]; + } + }, + + // The regular expressions that power the parsing engine + parse: [ + // Match: [@value='test'], [@foo] + [ "\\[ *(@)S *([!*$^=]*) *Q\\]", 1 ], + + // Match: [div], [div p] + [ "(\\[)Q\\]", 0 ], + + // Match: :contains('foo') + [ "(:)S\\(Q\\)", 0 ], + + // Match: :even, :last-chlid + [ "([:.#]*)S", 0 ] + ], + + filter: function(t,r,not) { + // Figure out if we're doing regular, or inverse, filtering + var g = not !== false ? jQuery.grep : + function(a,f) {return jQuery.grep(a,f,true);}; + + while ( t && /^[a-z[({<*:.#]/i.test(t) ) { + + var p = jQuery.parse; + + for ( var i = 0; i < p.length; i++ ) { + var re = new RegExp( "^" + p[i][0] + + // Look for a string-like sequence + .replace( 'S', "([a-z*_-][a-z0-9_-]*)" ) + + // Look for something (optionally) enclosed with quotes + .replace( 'Q', " *'?\"?([^'\"]*?)'?\"? *" ), "i" ); + + var m = re.exec( t ); + + if ( m ) { + // Re-organize the match + if ( p[i][1] ) + m = ["", m[1], m[3], m[2], m[4]]; + + // Remove what we just matched + t = t.replace( re, "" ); + + break; + } + } + + // :not() is a special case that can be optomized by + // keeping it out of the expression list + if ( m[1] == ":" && m[2] == "not" ) + r = jQuery.filter(m[3],r,false).r; + + // Otherwise, find the expression to execute + else { + var f = jQuery.expr[m[1]]; + if ( f.constructor != String ) + f = jQuery.expr[m[1]][m[2]]; + + // Build a custom macro to enclose it + eval("f = function(a,i){" + + ( m[1] == "@" ? "z=jQuery.attr(a,m[3]);" : "" ) + + "return " + f + "}"); + + // Execute it against the current filter + r = g( r, f ); + } + } + + // Return an array of filtered elements (r) + // and the modified expression string (t) + return { r: r, t: t }; + }, + trim: function(t){ + return t.replace(/^\s+|\s+$/g, ""); + }, + parents: function( elem ){ + var matched = []; + var cur = elem.parentNode; + while ( cur && cur != document ) { + matched.push( cur ); + cur = cur.parentNode; + } + return matched; + }, + sibling: function(elem, pos, not) { + var elems = []; + + var siblings = elem.parentNode.childNodes; + for ( var i = 0; i < siblings.length; i++ ) { + if ( not === true && siblings[i] == elem ) continue; + + if ( siblings[i].nodeType == 1 ) + elems.push( siblings[i] ); + if ( siblings[i] == elem ) + elems.n = elems.length - 1; + } + + return jQuery.extend( elems, { + last: elems.n == elems.length - 1, + cur: pos == "even" && elems.n % 2 == 0 || pos == "odd" && elems.n % 2 || elems[pos] == elem, + prev: elems[elems.n - 1], + next: elems[elems.n + 1] + }); + }, + merge: function(first, second) { + var result = []; + + // Move b over to the new array (this helps to avoid + // StaticNodeList instances) + for ( var k = 0; k < first.length; k++ ) + result[k] = first[k]; + + // Now check for duplicates between a and b and only + // add the unique items + for ( var i = 0; i < second.length; i++ ) { + var noCollision = true; + + // The collision-checking process + for ( var j = 0; j < first.length; j++ ) + if ( second[i] == first[j] ) + noCollision = false; + + // If the item is unique, add it + if ( noCollision ) + result.push( second[i] ); + } + + return result; + }, + grep: function(elems, fn, inv) { + // If a string is passed in for the function, make a function + // for it (a handy shortcut) + if ( fn.constructor == String ) + fn = new Function("a","i","return " + fn); + + var result = []; + + // Go through the array, only saving the items + // that pass the validator function + for ( var i = 0; i < elems.length; i++ ) + if ( !inv && fn(elems[i],i) || inv && !fn(elems[i],i) ) + result.push( elems[i] ); + + return result; + }, + map: function(elems, fn) { + // If a string is passed in for the function, make a function + // for it (a handy shortcut) + if ( fn.constructor == String ) + fn = new Function("a","return " + fn); + + var result = []; + + // Go through the array, translating each of the items to their + // new value (or values). + for ( var i = 0; i < elems.length; i++ ) { + var val = fn(elems[i],i); + + if ( val !== null && val != undefined ) { + if ( val.constructor != Array ) val = [val]; + result = jQuery.merge( result, val ); + } + } + + return result; + }, + + /* + * A number of helper functions used for managing events. + * Many of the ideas behind this code orignated from Dean Edwards' addEvent library. + */ + event: { + + // Bind an event to an element + // Original by Dean Edwards + add: function(element, type, handler) { + // For whatever reason, IE has trouble passing the window object + // around, causing it to be cloned in the process + if ( jQuery.browser.msie && element.setInterval != undefined ) + element = window; + + // Make sure that the function being executed has a unique ID + if ( !handler.guid ) + handler.guid = this.guid++; + + // Init the element's event structure + if (!element.events) + element.events = {}; + + // Get the current list of functions bound to this event + var handlers = element.events[type]; + + // If it hasn't been initialized yet + if (!handlers) { + // Init the event handler queue + handlers = element.events[type] = {}; + + // Remember an existing handler, if it's already there + if (element["on" + type]) + handlers[0] = element["on" + type]; + } + + // Add the function to the element's handler list + handlers[handler.guid] = handler; + + // And bind the global event handler to the element + element["on" + type] = this.handle; + + // Remember the function in a global list (for triggering) + if (!this.global[type]) + this.global[type] = []; + this.global[type].push( element ); + }, + + guid: 1, + global: {}, + + // Detach an event or set of events from an element + remove: function(element, type, handler) { + if (element.events) + if (type && element.events[type]) + if ( handler ) + delete element.events[type][handler.guid]; + else + for ( var i in element.events[type] ) + delete element.events[type][i]; + else + for ( var j in element.events ) + this.remove( element, j ); + }, + + trigger: function(type,data,element) { + // Touch up the incoming data + data = data || []; + + // Handle a global trigger + if ( !element ) { + var g = this.global[type]; + if ( g ) + for ( var i = 0; i < g.length; i++ ) + this.trigger( type, data, g[i] ); + + // Handle triggering a single element + } else if ( element["on" + type] ) { + // Pass along a fake event + data.unshift( this.fix({ type: type, target: element }) ); + + // Trigger the event + element["on" + type].apply( element, data ); + } + }, + + handle: function(event) { + if ( typeof jQuery == "undefined" ) return; + + event = event || jQuery.event.fix( window.event ); + + // If no correct event was found, fail + if ( !event ) return; + + var returnValue = true; + + var c = this.events[event.type]; + + for ( var j in c ) { + if ( c[j].apply( this, [event] ) === false ) { + event.preventDefault(); + event.stopPropagation(); + returnValue = false; + } + } + + return returnValue; + }, + + fix: function(event) { + if ( event ) { + event.preventDefault = function() { + this.returnValue = false; + }; + + event.stopPropagation = function() { + this.cancelBubble = true; + }; + } + + return event; + } + + } +}); + +new function() { + var b = navigator.userAgent.toLowerCase(); + + // Figure out what browser is being used + jQuery.browser = { + safari: /webkit/.test(b), + opera: /opera/.test(b), + msie: /msie/.test(b) && !/opera/.test(b), + mozilla: /mozilla/.test(b) && !/compatible/.test(b) + }; + + // Check to see if the W3C box model is being used + jQuery.boxModel = !jQuery.browser.msie || document.compatMode == "CSS1Compat"; +}; + +jQuery.macros = { + to: { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after" + }, + + + css: "width,height,top,left,position,float,overflow,color,background".split(","), + + filter: [ "eq", "lt", "gt", "contains" ], + + attr: { + + val: "value", + + html: "innerHTML", + + id: null, + + title: null, + + name: null, + + href: null, + + src: null, + + rel: null + }, + + axis: { + + parent: "a.parentNode", + + ancestors: jQuery.parents, + + parents: jQuery.parents, + + next: "jQuery.sibling(a).next", + + prev: "jQuery.sibling(a).prev", + + siblings: jQuery.sibling, + + children: "a.childNodes" + }, + + each: { + + removeAttr: function( key ) { + this.removeAttribute( key ); + }, + show: function(){ + this.style.display = this.oldblock ? this.oldblock : ""; + if ( jQuery.css(this,"display") == "none" ) + this.style.display = "block"; + }, + hide: function(){ + this.oldblock = this.oldblock || jQuery.css(this,"display"); + if ( this.oldblock == "none" ) + this.oldblock = "block"; + this.style.display = "none"; + }, + toggle: function(){ + $(this)[ $(this).is(":hidden") ? "show" : "hide" ].apply( $(this), arguments ); + }, + addClass: function(c){ + jQuery.className.add(this,c); + }, + removeClass: function(c){ + jQuery.className.remove(this,c); + }, + toggleClass: function( c ){ + jQuery.className[ jQuery.className.has(this,c) ? "remove" : "add" ](this,c); + }, + + remove: function(a){ + if ( !a || jQuery.filter( [this], a ).r ) + this.parentNode.removeChild( this ); + }, + empty: function(){ + while ( this.firstChild ) + this.removeChild( this.firstChild ); + }, + bind: function( type, fn ) { + if ( fn.constructor == String ) + fn = new Function("e", ( !fn.indexOf(".") ? "$(this)" : "return " ) + fn); + jQuery.event.add( this, type, fn ); + }, + + unbind: function( type, fn ) { + jQuery.event.remove( this, type, fn ); + }, + trigger: function( type, data ) { + jQuery.event.trigger( type, data, this ); + } + } +}; + +jQuery.init();jQuery.fn.extend({ + + // We're overriding the old toggle function, so + // remember it for later + _toggle: jQuery.fn.toggle, + toggle: function(a,b) { + // If two functions are passed in, we're + // toggling on a click + return a && b && a.constructor == Function && b.constructor == Function ? this.click(function(e){ + // Figure out which function to execute + this.last = this.last == a ? b : a; + + // Make sure that clicks stop + e.preventDefault(); + + // and execute the function + return this.last.apply( this, [e] ) || false; + }) : + + // Otherwise, execute the old toggle function + this._toggle.apply( this, arguments ); + }, + + hover: function(f,g) { + + // A private function for haandling mouse 'hovering' + function handleHover(e) { + // Check if mouse(over|out) are still within the same parent element + var p = (e.type == "mouseover" ? e.fromElement : e.toElement) || e.relatedTarget; + + // Traverse up the tree + while ( p && p != this ) p = p.parentNode; + + // If we actually just moused on to a sub-element, ignore it + if ( p == this ) return false; + + // Execute the right function + return (e.type == "mouseover" ? f : g).apply(this, [e]); + } + + // Bind the function to the two event listeners + return this.mouseover(handleHover).mouseout(handleHover); + }, + ready: function(f) { + // If the DOM is already ready + if ( jQuery.isReady ) + // Execute the function immediately + f.apply( document ); + + // Otherwise, remember the function for later + else { + // Add the function to the wait list + jQuery.readyList.push( f ); + } + + return this; + } +}); + +jQuery.extend({ + /* + * All the code that makes DOM Ready work nicely. + */ + isReady: false, + readyList: [], + + // Handle when the DOM is ready + ready: function() { + // Make sure that the DOM is not already loaded + if ( !jQuery.isReady ) { + // Remember that the DOM is ready + jQuery.isReady = true; + + // If there are functions bound, to execute + if ( jQuery.readyList ) { + // Execute all of them + for ( var i = 0; i < jQuery.readyList.length; i++ ) + jQuery.readyList[i].apply( document ); + + // Reset the list of functions + jQuery.readyList = null; + } + } + } +}); + +new function(){ + + var e = ("blur,focus,load,resize,scroll,unload,click,dblclick," + + "mousedown,mouseup,mousemove,mouseover,mouseout,change,reset,select," + + "submit,keydown,keypress,keyup,error").split(","); + + // Go through all the event names, but make sure that + // it is enclosed properly + for ( var i = 0; i < e.length; i++ ) new function(){ + + var o = e[i]; + + // Handle event binding + jQuery.fn[o] = function(f){ + return f ? this.bind(o, f) : this.trigger(o); + }; + + // Handle event unbinding + jQuery.fn["un"+o] = function(f){ return this.unbind(o, f); }; + + // Finally, handle events that only fire once + jQuery.fn["one"+o] = function(f){ + // Attach the event listener + return this.each(function(){ + + var count = 0; + + // Add the event + jQuery.event.add( this, o, function(e){ + // If this function has already been executed, stop + if ( count++ ) return; + + // And execute the bound function + return f.apply(this, [e]); + }); + }); + }; + + }; + + // If Mozilla is used + if ( jQuery.browser.mozilla || jQuery.browser.opera ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", jQuery.ready, false ); + + // If IE is used, use the excellent hack by Matthias Miller + // http://www.outofhanwell.com/blog/index.php?title=the_window_onload_problem_revisited + } else if ( jQuery.browser.msie ) { + + // Only works if you document.write() it + document.write("<scr" + "ipt id=__ie_init defer=true " + + "src=//:><\/script>"); + + // Use the defer script hack + var script = document.getElementById("__ie_init"); + script.onreadystatechange = function() { + if ( this.readyState == "complete" ) + jQuery.ready(); + }; + + // Clear from memory + script = null; + + // If Safari is used + } else if ( jQuery.browser.safari ) { + // Continually check to see if the document.readyState is valid + jQuery.safariTimer = setInterval(function(){ + // loaded and complete are both valid states + if ( document.readyState == "loaded" || + document.readyState == "complete" ) { + + // If either one are found, remove the timer + clearInterval( jQuery.safariTimer ); + jQuery.safariTimer = null; + + // and execute any waiting functions + jQuery.ready(); + } + }, 10); + } + + // A fallback to window.onload, that will always work + jQuery.event.add( window, "load", jQuery.ready ); + +}; +jQuery.fn.extend({ + + // overwrite the old show method + _show: jQuery.fn.show, + + show: function(speed,callback){ + return speed ? this.animate({ + height: "show", width: "show", opacity: "show" + }, speed, callback) : this._show(); + }, + + // Overwrite the old hide method + _hide: jQuery.fn.hide, + + hide: function(speed,callback){ + return speed ? this.animate({ + height: "hide", width: "hide", opacity: "hide" + }, speed, callback) : this._hide(); + }, + + slideDown: function(speed,callback){ + return this.animate({height: "show"}, speed, callback); + }, + + slideUp: function(speed,callback){ + return this.animate({height: "hide"}, speed, callback); + }, + + slideToggle: function(speed,callback){ + return this.each(function(){ + var state = $(this).is(":hidden") ? "show" : "hide"; + $(this).animate({height: state}, speed, callback); + }); + }, + + fadeIn: function(speed,callback){ + return this.animate({opacity: "show"}, speed, callback); + }, + + fadeOut: function(speed,callback){ + return this.animate({opacity: "hide"}, speed, callback); + }, + + fadeTo: function(speed,to,callback){ + return this.animate({opacity: to}, speed, callback); + }, + animate: function(prop,speed,callback) { + return this.queue(function(){ + + this.curAnim = prop; + + for ( var p in prop ) { + var e = new jQuery.fx( this, jQuery.speed(speed,callback), p ); + if ( prop[p].constructor == Number ) + e.custom( e.cur(), prop[p] ); + else + e[ prop[p] ]( prop ); + } + + }); + }, + queue: function(type,fn){ + if ( !fn ) { + fn = type; + type = "fx"; + } + + return this.each(function(){ + if ( !this.queue ) + this.queue = {}; + + if ( !this.queue[type] ) + this.queue[type] = []; + + this.queue[type].push( fn ); + + if ( this.queue[type].length == 1 ) + fn.apply(this); + }); + } + +}); + +jQuery.extend({ + + setAuto: function(e,p) { + if ( e.notAuto ) return; + + if ( p == "height" && e.scrollHeight != parseInt(jQuery.curCSS(e,p)) ) return; + if ( p == "width" && e.scrollWidth != parseInt(jQuery.curCSS(e,p)) ) return; + + // Remember the original height + var a = e.style[p]; + + // Figure out the size of the height right now + var o = jQuery.curCSS(e,p,1); + + if ( p == "height" && e.scrollHeight != o || + p == "width" && e.scrollWidth != o ) return; + + // Set the height to auto + e.style[p] = e.currentStyle ? "" : "auto"; + + // See what the size of "auto" is + var n = jQuery.curCSS(e,p,1); + + // Revert back to the original size + if ( o != n && n != "auto" ) { + e.style[p] = a; + e.notAuto = true; + } + }, + + speed: function(s,o) { + o = o || {}; + + if ( o.constructor == Function ) + o = { complete: o }; + + var ss = { slow: 600, fast: 200 }; + o.duration = (s && s.constructor == Number ? s : ss[s]) || 400; + + // Queueing + o.oldComplete = o.complete; + o.complete = function(){ + jQuery.dequeue(this, "fx"); + if ( o.oldComplete && o.oldComplete.constructor == Function ) + o.oldComplete.apply( this ); + }; + + return o; + }, + + queue: {}, + + dequeue: function(elem,type){ + type = type || "fx"; + + if ( elem.queue && elem.queue[type] ) { + // Remove self + elem.queue[type].shift(); + + // Get next function + var f = elem.queue[type][0]; + + if ( f ) f.apply( elem ); + } + }, + + /* + * I originally wrote fx() as a clone of moo.fx and in the process + * of making it small in size the code became illegible to sane + * people. You've been warned. + */ + + fx: function( elem, options, prop ){ + + var z = this; + + // The users options + z.o = { + duration: options.duration || 400, + complete: options.complete, + step: options.step + }; + + // The element + z.el = elem; + + // The styles + var y = z.el.style; + + // Simple function for setting a style value + z.a = function(){ + if ( options.step ) + options.step.apply( elem, [ z.now ] ); + + if ( prop == "opacity" ) { + if (z.now == 1) z.now = 0.9999; + if (window.ActiveXObject) + y.filter = "alpha(opacity=" + z.now*100 + ")"; + else + y.opacity = z.now; + + // My hate for IE will never die + } else if ( parseInt(z.now) ) + y[prop] = parseInt(z.now) + "px"; + + y.display = "block"; + }; + + // Figure out the maximum number to run to + z.max = function(){ + return parseFloat( jQuery.css(z.el,prop) ); + }; + + // Get the current size + z.cur = function(){ + var r = parseFloat( jQuery.curCSS(z.el, prop) ); + return r && r > -10000 ? r : z.max(); + }; + + // Start an animation from one number to another + z.custom = function(from,to){ + z.startTime = (new Date()).getTime(); + z.now = from; + z.a(); + + z.timer = setInterval(function(){ + z.step(from, to); + }, 13); + }; + + // Simple 'show' function + z.show = function( p ){ + if ( !z.el.orig ) z.el.orig = {}; + + // Remember where we started, so that we can go back to it later + z.el.orig[prop] = this.cur(); + + z.custom( 0, z.el.orig[prop] ); + + // Stupid IE, look what you made me do + if ( prop != "opacity" ) + y[prop] = "1px"; + }; + + // Simple 'hide' function + z.hide = function(){ + if ( !z.el.orig ) z.el.orig = {}; + + // Remember where we started, so that we can go back to it later + z.el.orig[prop] = this.cur(); + + z.o.hide = true; + + // Begin the animation + z.custom(z.el.orig[prop], 0); + }; + + // IE has trouble with opacity if it does not have layout + if ( jQuery.browser.msie && !z.el.currentStyle.hasLayout ) + y.zoom = "1"; + + // Remember the overflow of the element + if ( !z.el.oldOverlay ) + z.el.oldOverflow = jQuery.css( z.el, "overflow" ); + + // Make sure that nothing sneaks out + y.overflow = "hidden"; + + // Each step of an animation + z.step = function(firstNum, lastNum){ + var t = (new Date()).getTime(); + + if (t > z.o.duration + z.startTime) { + // Stop the timer + clearInterval(z.timer); + z.timer = null; + + z.now = lastNum; + z.a(); + + z.el.curAnim[ prop ] = true; + + var done = true; + for ( var i in z.el.curAnim ) + if ( z.el.curAnim[i] !== true ) + done = false; + + if ( done ) { + // Reset the overflow + y.overflow = z.el.oldOverflow; + + // Hide the element if the "hide" operation was done + if ( z.o.hide ) + y.display = 'none'; + + // Reset the property, if the item has been hidden + if ( z.o.hide ) { + for ( var p in z.el.curAnim ) { + y[ p ] = z.el.orig[p] + ( p == "opacity" ? "" : "px" ); + + // set its height and/or width to auto + if ( p == 'height' || p == 'width' ) + jQuery.setAuto( z.el, p ); + } + } + } + + // If a callback was provided, execute it + if( done && z.o.complete && z.o.complete.constructor == Function ) + // Execute the complete function + z.o.complete.apply( z.el ); + } else { + // Figure out where in the animation we are and set the number + var p = (t - this.startTime) / z.o.duration; + z.now = ((-Math.cos(p*Math.PI)/2) + 0.5) * (lastNum-firstNum) + firstNum; + + // Perform the next step of the animation + z.a(); + } + }; + + } + +}); +// AJAX Plugin +// Docs Here: +// http://jquery.com/docs/ajax/ +jQuery.fn.loadIfModified = function( url, params, callback ) { + this.load( url, params, callback, 1 ); +}; + +jQuery.fn.load = function( url, params, callback, ifModified ) { + if ( url.constructor == Function ) + return this.bind("load", url); + + callback = callback || function(){}; + + // Default to a GET request + var type = "GET"; + + // If the second parameter was provided + if ( params ) { + // If it's a function + if ( params.constructor == Function ) { + // We assume that it's the callback + callback = params; + params = null; + + // Otherwise, build a param string + } else { + params = jQuery.param( params ); + type = "POST"; + } + } + + var self = this; + + // Request the remote document + jQuery.ajax( type, url, params,function(res, status){ + + if ( status == "success" || !ifModified && status == "notmodified" ) { + // Inject the HTML into all the matched elements + self.html(res.responseText).each( callback, [res.responseText, status] ); + + // Execute all the scripts inside of the newly-injected HTML + $("script", self).each(function(){ + if ( this.src ) + $.getScript( this.src ); + else + eval.call( window, this.text || this.textContent || this.innerHTML || "" ); + }); + } else + callback.apply( self, [res.responseText, status] ); + + }, ifModified); + + return this; +}; + +// If IE is used, create a wrapper for the XMLHttpRequest object +if ( jQuery.browser.msie ) + XMLHttpRequest = function(){ + return new ActiveXObject( + navigator.userAgent.indexOf("MSIE 5") >= 0 ? + "Microsoft.XMLHTTP" : "Msxml2.XMLHTTP" + ); + }; + +// Attach a bunch of functions for handling common AJAX events +new function(){ + var e = "ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess".split(','); + + for ( var i = 0; i < e.length; i++ ) new function(){ + var o = e[i]; + jQuery.fn[o] = function(f){ + return this.bind(o, f); + }; + }; +}; + +jQuery.extend({ + get: function( url, data, callback, type, ifModified ) { + if ( data.constructor == Function ) { + type = callback; + callback = data; + data = null; + } + + if ( data ) url += "?" + jQuery.param(data); + + // Build and start the HTTP Request + jQuery.ajax( "GET", url, null, function(r, status) { + if ( callback ) callback( jQuery.httpData(r,type), status ); + }, ifModified); + }, + + getIfModified: function( url, data, callback, type ) { + jQuery.get(url, data, callback, type, 1); + }, + + getScript: function( url, data, callback ) { + jQuery.get(url, data, callback, "script"); + }, + post: function( url, data, callback, type ) { + // Build and start the HTTP Request + jQuery.ajax( "POST", url, jQuery.param(data), function(r, status) { + if ( callback ) callback( jQuery.httpData(r,type), status ); + }); + }, + + // timeout (ms) + timeout: 0, + + ajaxTimeout: function(timeout) { + jQuery.timeout = timeout; + }, + + // Last-Modified header cache for next request + lastModified: {}, + ajax: function( type, url, data, ret, ifModified ) { + // If only a single argument was passed in, + // assume that it is a object of key/value pairs + if ( !url ) { + ret = type.complete; + var success = type.success; + var error = type.error; + data = type.data; + url = type.url; + type = type.type; + } + + // Watch for a new set of requests + if ( ! jQuery.active++ ) + jQuery.event.trigger( "ajaxStart" ); + + var requestDone = false; + + // Create the request object + var xml = new XMLHttpRequest(); + + // Open the socket + xml.open(type || "GET", url, true); + + // Set the correct header, if data is being sent + if ( data ) + xml.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + + // Set the If-Modified-Since header, if ifModified mode. + if ( ifModified ) + xml.setRequestHeader("If-Modified-Since", + jQuery.lastModified[url] || "Thu, 01 Jan 1970 00:00:00 GMT" ); + + // Set header so calling script knows that it's an XMLHttpRequest + xml.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + + // Make sure the browser sends the right content length + if ( xml.overrideMimeType ) + xml.setRequestHeader("Connection", "close"); + + // Wait for a response to come back + var onreadystatechange = function(istimeout){ + // The transfer is complete and the data is available, or the request timed out + if ( xml && (xml.readyState == 4 || istimeout == "timeout") ) { + requestDone = true; + + var status = jQuery.httpSuccess( xml ) && istimeout != "timeout" ? + ifModified && jQuery.httpNotModified( xml, url ) ? "notmodified" : "success" : "error"; + + // Make sure that the request was successful or notmodified + if ( status != "error" ) { + // Cache Last-Modified header, if ifModified mode. + var modRes = xml.getResponseHeader("Last-Modified"); + if ( ifModified && modRes ) jQuery.lastModified[url] = modRes; + + // If a local callback was specified, fire it + if ( success ) success( xml, status ); + + // Fire the global callback + jQuery.event.trigger( "ajaxSuccess" ); + + // Otherwise, the request was not successful + } else { + // If a local callback was specified, fire it + if ( error ) error( xml, status ); + + // Fire the global callback + jQuery.event.trigger( "ajaxError" ); + } + + // The request was completed + jQuery.event.trigger( "ajaxComplete" ); + + // Handle the global AJAX counter + if ( ! --jQuery.active ) + jQuery.event.trigger( "ajaxStop" ); + + // Process result + if ( ret ) ret(xml, status); + + // Stop memory leaks + xml.onreadystatechange = function(){}; + xml = null; + + } + }; + xml.onreadystatechange = onreadystatechange; + + // Timeout checker + if(jQuery.timeout > 0) + setTimeout(function(){ + // Check to see if the request is still happening + if (xml) { + // Cancel the request + xml.abort(); + + if ( !requestDone ) onreadystatechange( "timeout" ); + + // Clear from memory + xml = null; + } + }, jQuery.timeout); + + // Send the data + xml.send(data); + }, + + // Counter for holding the number of active queries + active: 0, + + // Determines if an XMLHttpRequest was successful or not + httpSuccess: function(r) { + try { + return !r.status && location.protocol == "file:" || + ( r.status >= 200 && r.status < 300 ) || r.status == 304 || + jQuery.browser.safari && r.status == undefined; + } catch(e){} + + return false; + }, + + // Determines if an XMLHttpRequest returns NotModified + httpNotModified: function(xml, url) { + try { + var xmlRes = xml.getResponseHeader("Last-Modified"); + + // Firefox always returns 200. check Last-Modified date + return xml.status == 304 || xmlRes == jQuery.lastModified[url] || + jQuery.browser.safari && xml.status == undefined; + } catch(e){} + + return false; + }, + + // Get the data out of an XMLHttpRequest. + // Return parsed XML if content-type header is "xml" and type is "xml" or omitted, + // otherwise return plain text. + httpData: function(r,type) { + var ct = r.getResponseHeader("content-type"); + var data = !type && ct && ct.indexOf("xml") >= 0; + data = type == "xml" || data ? r.responseXML : r.responseText; + + // If the type is "script", eval it + if ( type == "script" ) eval.call( window, data ); + + return data; + }, + + // Serialize an array of form elements or a set of + // key/values into a query string + param: function(a) { + var s = []; + + // If an array was passed in, assume that it is an array + // of form elements + if ( a.constructor == Array ) { + // Serialize the form elements + for ( var i = 0; i < a.length; i++ ) + s.push( a[i].name + "=" + encodeURIComponent( a[i].value ) ); + + // Otherwise, assume that it's an object of key/value pairs + } else { + // Serialize the key/values + for ( var j in a ) + s.push( j + "=" + encodeURIComponent( a[j] ) ); + } + + // Return the resulting serialization + return s.join("&"); + } + +}); diff --git a/devtools/client/inspector/markup/test/lib_jquery_1.1.js b/devtools/client/inspector/markup/test/lib_jquery_1.1.js new file mode 100644 index 000000000..981a3bdc1 --- /dev/null +++ b/devtools/client/inspector/markup/test/lib_jquery_1.1.js @@ -0,0 +1,2172 @@ +/* prevent execution of jQuery if included more than once */ +if(typeof window.jQuery == "undefined") { +/* + * jQuery 1.1 - New Wave Javascript + * + * Copyright (c) 2007 John Resig (jquery.com) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * $Date: 2007-01-14 17:37:33 -0500 (Sun, 14 Jan 2007) $ + * $Rev: 1073 $ + */ + +// Global undefined variable +window.undefined = window.undefined; +var jQuery = function(a,c) { + // If the context is global, return a new object + if ( window == this ) + return new jQuery(a,c); + + // Make sure that a selection was provided + a = a || document; + + // HANDLE: $(function) + // Shortcut for document ready + // Safari reports typeof on DOM NodeLists as a function + if ( jQuery.isFunction(a) && !a.nodeType && a[0] == undefined ) + return new jQuery(document)[ jQuery.fn.ready ? "ready" : "load" ]( a ); + + // Handle HTML strings + if ( typeof a == "string" ) { + var m = /^[^<]*(<.+>)[^>]*$/.exec(a); + + a = m ? + // HANDLE: $(html) -> $(array) + jQuery.clean( [ m[1] ] ) : + + // HANDLE: $(expr) + jQuery.find( a, c ); + } + + return this.setArray( + // HANDLE: $(array) + a.constructor == Array && a || + + // HANDLE: $(arraylike) + // Watch for when an array-like object is passed as the selector + (a.jquery || a.length && a != window && !a.nodeType && a[0] != undefined && a[0].nodeType) && jQuery.makeArray( a ) || + + // HANDLE: $(*) + [ a ] ); +}; + +// Map over the $ in case of overwrite +if ( typeof $ != "undefined" ) + jQuery._$ = $; + +// Map the jQuery namespace to the '$' one +var $ = jQuery; + +jQuery.fn = jQuery.prototype = { + jquery: "1.1", + + size: function() { + return this.length; + }, + + length: 0, + + get: function( num ) { + return num == undefined ? + + // Return a 'clean' array + jQuery.makeArray( this ) : + + // Return just the object + this[num]; + }, + pushStack: function( a ) { + var ret = jQuery(this); + ret.prevObject = this; + return ret.setArray( a ); + }, + setArray: function( a ) { + this.length = 0; + [].push.apply( this, a ); + return this; + }, + each: function( fn, args ) { + return jQuery.each( this, fn, args ); + }, + index: function( obj ) { + var pos = -1; + this.each(function(i){ + if ( this == obj ) pos = i; + }); + return pos; + }, + + attr: function( key, value, type ) { + var obj = key; + + // Look for the case where we're accessing a style value + if ( key.constructor == String ) + if ( value == undefined ) + return jQuery[ type || "attr" ]( this[0], key ); + else { + obj = {}; + obj[ key ] = value; + } + + // Check to see if we're setting style values + return this.each(function(){ + // Set all the styles + for ( var prop in obj ) + jQuery.attr( + type ? this.style : this, + prop, jQuery.prop(this, obj[prop], type) + ); + }); + }, + + css: function( key, value ) { + return this.attr( key, value, "curCSS" ); + }, + + text: function(e) { + if ( typeof e == "string" ) + return this.empty().append( document.createTextNode( e ) ); + + var t = ""; + jQuery.each( e || this, function(){ + jQuery.each( this.childNodes, function(){ + if ( this.nodeType != 8 ) + t += this.nodeType != 1 ? + this.nodeValue : jQuery.fn.text([ this ]); + }); + }); + return t; + }, + + wrap: function() { + // The elements to wrap the target around + var a = jQuery.clean(arguments); + + // Wrap each of the matched elements individually + return this.each(function(){ + // Clone the structure that we're using to wrap + var b = a[0].cloneNode(true); + + // Insert it before the element to be wrapped + this.parentNode.insertBefore( b, this ); + + // Find the deepest point in the wrap structure + while ( b.firstChild ) + b = b.firstChild; + + // Move the matched element to within the wrap structure + b.appendChild( this ); + }); + }, + append: function() { + return this.domManip(arguments, true, 1, function(a){ + this.appendChild( a ); + }); + }, + prepend: function() { + return this.domManip(arguments, true, -1, function(a){ + this.insertBefore( a, this.firstChild ); + }); + }, + before: function() { + return this.domManip(arguments, false, 1, function(a){ + this.parentNode.insertBefore( a, this ); + }); + }, + after: function() { + return this.domManip(arguments, false, -1, function(a){ + this.parentNode.insertBefore( a, this.nextSibling ); + }); + }, + end: function() { + return this.prevObject || jQuery([]); + }, + find: function(t) { + return this.pushStack( jQuery.map( this, function(a){ + return jQuery.find(t,a); + }) ); + }, + clone: function(deep) { + return this.pushStack( jQuery.map( this, function(a){ + return a.cloneNode( deep != undefined ? deep : true ); + }) ); + }, + + filter: function(t) { + return this.pushStack( + jQuery.isFunction( t ) && + jQuery.grep(this, function(el, index){ + return t.apply(el, [index]) + }) || + + jQuery.multiFilter(t,this) ); + }, + + not: function(t) { + return this.pushStack( + t.constructor == String && + jQuery.multiFilter(t,this,true) || + + jQuery.grep(this,function(a){ + if ( t.constructor == Array || t.jquery ) + return jQuery.inArray( t, a ) < 0; + else + return a != t; + }) ); + }, + + add: function(t) { + return this.pushStack( jQuery.merge( + this.get(), + typeof t == "string" ? jQuery(t).get() : t ) + ); + }, + is: function(expr) { + return expr ? jQuery.filter(expr,this).r.length > 0 : false; + }, + + val: function( val ) { + return val == undefined ? + ( this.length ? this[0].value : null ) : + this.attr( "value", val ); + }, + + html: function( val ) { + return val == undefined ? + ( this.length ? this[0].innerHTML : null ) : + this.empty().append( val ); + }, + domManip: function(args, table, dir, fn){ + var clone = this.length > 1; + var a = jQuery.clean(args); + if ( dir < 0 ) + a.reverse(); + + return this.each(function(){ + var obj = this; + + if ( table && this.nodeName.toUpperCase() == "TABLE" && a[0].nodeName.toUpperCase() == "TR" ) + obj = this.getElementsByTagName("tbody")[0] || this.appendChild(document.createElement("tbody")); + + jQuery.each( a, function(){ + fn.apply( obj, [ clone ? this.cloneNode(true) : this ] ); + }); + + }); + } +}; + +jQuery.extend = jQuery.fn.extend = function() { + // copy reference to target object + var target = arguments[0], + a = 1; + + // extend jQuery itself if only one argument is passed + if ( arguments.length == 1 ) { + target = this; + a = 0; + } + var prop; + while (prop = arguments[a++]) + // Extend the base object + for ( var i in prop ) target[i] = prop[i]; + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function() { + if ( jQuery._$ ) + $ = jQuery._$; + }, + + isFunction: function( fn ) { + return fn && typeof fn == "function"; + }, + // args is for internal usage only + each: function( obj, fn, args ) { + if ( obj.length == undefined ) + for ( var i in obj ) + fn.apply( obj[i], args || [i, obj[i]] ); + else + for ( var i = 0, ol = obj.length; i < ol; i++ ) + if ( fn.apply( obj[i], args || [i, obj[i]] ) === false ) break; + return obj; + }, + + prop: function(elem, value, type){ + // Handle executable functions + if ( jQuery.isFunction( value ) ) + return value.call( elem ); + + // Handle passing in a number to a CSS property + if ( value.constructor == Number && type == "curCSS" ) + return value + "px"; + + return value; + }, + + className: { + // internal only, use addClass("class") + add: function( elem, c ){ + jQuery.each( c.split(/\s+/), function(i, cur){ + if ( !jQuery.className.has( elem.className, cur ) ) + elem.className += ( elem.className ? " " : "" ) + cur; + }); + }, + + // internal only, use removeClass("class") + remove: function( elem, c ){ + elem.className = c ? + jQuery.grep( elem.className.split(/\s+/), function(cur){ + return !jQuery.className.has( c, cur ); + }).join(" ") : ""; + }, + + // internal only, use is(".class") + has: function( t, c ) { + t = t.className || t; + return t && new RegExp("(^|\\s)" + c + "(\\s|$)").test( t ); + } + }, + swap: function(e,o,f) { + for ( var i in o ) { + e.style["old"+i] = e.style[i]; + e.style[i] = o[i]; + } + f.apply( e, [] ); + for ( var i in o ) + e.style[i] = e.style["old"+i]; + }, + + css: function(e,p) { + if ( p == "height" || p == "width" ) { + var old = {}, oHeight, oWidth, d = ["Top","Bottom","Right","Left"]; + + jQuery.each( d, function(){ + old["padding" + this] = 0; + old["border" + this + "Width"] = 0; + }); + + jQuery.swap( e, old, function() { + if (jQuery.css(e,"display") != "none") { + oHeight = e.offsetHeight; + oWidth = e.offsetWidth; + } else { + e = jQuery(e.cloneNode(true)) + .find(":radio").removeAttr("checked").end() + .css({ + visibility: "hidden", position: "absolute", display: "block", right: "0", left: "0" + }).appendTo(e.parentNode)[0]; + + var parPos = jQuery.css(e.parentNode,"position"); + if ( parPos == "" || parPos == "static" ) + e.parentNode.style.position = "relative"; + + oHeight = e.clientHeight; + oWidth = e.clientWidth; + + if ( parPos == "" || parPos == "static" ) + e.parentNode.style.position = "static"; + + e.parentNode.removeChild(e); + } + }); + + return p == "height" ? oHeight : oWidth; + } + + return jQuery.curCSS( e, p ); + }, + + curCSS: function(elem, prop, force) { + var ret; + + if (prop == "opacity" && jQuery.browser.msie) + return jQuery.attr(elem.style, "opacity"); + + if (prop == "float" || prop == "cssFloat") + prop = jQuery.browser.msie ? "styleFloat" : "cssFloat"; + + if (!force && elem.style[prop]) + ret = elem.style[prop]; + + else if (document.defaultView && document.defaultView.getComputedStyle) { + + if (prop == "cssFloat" || prop == "styleFloat") + prop = "float"; + + prop = prop.replace(/([A-Z])/g,"-$1").toLowerCase(); + var cur = document.defaultView.getComputedStyle(elem, null); + + if ( cur ) + ret = cur.getPropertyValue(prop); + else if ( prop == "display" ) + ret = "none"; + else + jQuery.swap(elem, { display: "block" }, function() { + var c = document.defaultView.getComputedStyle(this, ""); + ret = c && c.getPropertyValue(prop) || ""; + }); + + } else if (elem.currentStyle) { + + var newProp = prop.replace(/\-(\w)/g,function(m,c){return c.toUpperCase();}); + ret = elem.currentStyle[prop] || elem.currentStyle[newProp]; + + } + + return ret; + }, + + clean: function(a) { + var r = []; + + jQuery.each( a, function(i,arg){ + if ( !arg ) return; + + if ( arg.constructor == Number ) + arg = arg.toString(); + + // Convert html string into DOM nodes + if ( typeof arg == "string" ) { + // Trim whitespace, otherwise indexOf won't work as expected + var s = jQuery.trim(arg), div = document.createElement("div"), tb = []; + + var wrap = + // option or optgroup + !s.indexOf("<opt") && + [1, "<select>", "</select>"] || + + (!s.indexOf("<thead") || !s.indexOf("<tbody") || !s.indexOf("<tfoot")) && + [1, "<table>", "</table>"] || + + !s.indexOf("<tr") && + [2, "<table><tbody>", "</tbody></table>"] || + + // <thead> matched above + (!s.indexOf("<td") || !s.indexOf("<th")) && + [3, "<table><tbody><tr>", "</tr></tbody></table>"] || + + [0,"",""]; + + // Go to html and back, then peel off extra wrappers + div.innerHTML = wrap[1] + s + wrap[2]; + + // Move to the right depth + while ( wrap[0]-- ) + div = div.firstChild; + + // Remove IE's autoinserted <tbody> from table fragments + if ( jQuery.browser.msie ) { + + // String was a <table>, *may* have spurious <tbody> + if ( !s.indexOf("<table") && s.indexOf("<tbody") < 0 ) + tb = div.firstChild && div.firstChild.childNodes; + + // String was a bare <thead> or <tfoot> + else if ( wrap[1] == "<table>" && s.indexOf("<tbody") < 0 ) + tb = div.childNodes; + + for ( var n = tb.length-1; n >= 0 ; --n ) + if ( tb[n].nodeName.toUpperCase() == "TBODY" && !tb[n].childNodes.length ) + tb[n].parentNode.removeChild(tb[n]); + + } + + arg = div.childNodes; + } + + if ( arg.length === 0 ) + return; + + if ( arg[0] == undefined ) + r.push( arg ); + else + r = jQuery.merge( r, arg ); + + }); + + return r; + }, + + attr: function(elem, name, value){ + var fix = { + "for": "htmlFor", + "class": "className", + "float": jQuery.browser.msie ? "styleFloat" : "cssFloat", + cssFloat: jQuery.browser.msie ? "styleFloat" : "cssFloat", + innerHTML: "innerHTML", + className: "className", + value: "value", + disabled: "disabled", + checked: "checked", + readonly: "readOnly", + selected: "selected" + }; + + // IE actually uses filters for opacity ... elem is actually elem.style + if ( name == "opacity" && jQuery.browser.msie && value != undefined ) { + // IE has trouble with opacity if it does not have layout + // Force it by setting the zoom level + elem.zoom = 1; + + // Set the alpha filter to set the opacity + return elem.filter = elem.filter.replace(/alpha\([^\)]*\)/gi,"") + + ( value == 1 ? "" : "alpha(opacity=" + value * 100 + ")" ); + + } else if ( name == "opacity" && jQuery.browser.msie ) + return elem.filter ? + parseFloat( elem.filter.match(/alpha\(opacity=(.*)\)/)[1] ) / 100 : 1; + + // Mozilla doesn't play well with opacity 1 + if ( name == "opacity" && jQuery.browser.mozilla && value == 1 ) + value = 0.9999; + + // Certain attributes only work when accessed via the old DOM 0 way + if ( fix[name] ) { + if ( value != undefined ) elem[fix[name]] = value; + return elem[fix[name]]; + + } else if ( value == undefined && jQuery.browser.msie && elem.nodeName && elem.nodeName.toUpperCase() == "FORM" && (name == "action" || name == "method") ) + return elem.getAttributeNode(name).nodeValue; + + // IE elem.getAttribute passes even for style + else if ( elem.tagName ) { + if ( value != undefined ) elem.setAttribute( name, value ); + return elem.getAttribute( name ); + + } else { + name = name.replace(/-([a-z])/ig,function(z,b){return b.toUpperCase();}); + if ( value != undefined ) elem[name] = value; + return elem[name]; + } + }, + trim: function(t){ + return t.replace(/^\s+|\s+$/g, ""); + }, + + makeArray: function( a ) { + var r = []; + + if ( a.constructor != Array ) + for ( var i = 0, al = a.length; i < al; i++ ) + r.push( a[i] ); + else + r = a.slice( 0 ); + + return r; + }, + + inArray: function( b, a ) { + for ( var i = 0, al = a.length; i < al; i++ ) + if ( a[i] == b ) + return i; + return -1; + }, + merge: function(first, second) { + var r = [].slice.call( first, 0 ); + + // Now check for duplicates between the two arrays + // and only add the unique items + for ( var i = 0, sl = second.length; i < sl; i++ ) + // Check for duplicates + if ( jQuery.inArray( second[i], r ) == -1 ) + // The item is unique, add it + first.push( second[i] ); + + return first; + }, + grep: function(elems, fn, inv) { + // If a string is passed in for the function, make a function + // for it (a handy shortcut) + if ( typeof fn == "string" ) + fn = new Function("a","i","return " + fn); + + var result = []; + + // Go through the array, only saving the items + // that pass the validator function + for ( var i = 0, el = elems.length; i < el; i++ ) + if ( !inv && fn(elems[i],i) || inv && !fn(elems[i],i) ) + result.push( elems[i] ); + + return result; + }, + map: function(elems, fn) { + // If a string is passed in for the function, make a function + // for it (a handy shortcut) + if ( typeof fn == "string" ) + fn = new Function("a","return " + fn); + + var result = [], r = []; + + // Go through the array, translating each of the items to their + // new value (or values). + for ( var i = 0, el = elems.length; i < el; i++ ) { + var val = fn(elems[i],i); + + if ( val !== null && val != undefined ) { + if ( val.constructor != Array ) val = [val]; + result = result.concat( val ); + } + } + + var r = result.length ? [ result[0] ] : []; + + check: for ( var i = 1, rl = result.length; i < rl; i++ ) { + for ( var j = 0; j < i; j++ ) + if ( result[i] == r[j] ) + continue check; + + r.push( result[i] ); + } + + return r; + } +}); + +/* + * Whether the W3C compliant box model is being used. + * + * @property + * @name $.boxModel + * @type Boolean + * @cat JavaScript + */ +new function() { + var b = navigator.userAgent.toLowerCase(); + + // Figure out what browser is being used + jQuery.browser = { + safari: /webkit/.test(b), + opera: /opera/.test(b), + msie: /msie/.test(b) && !/opera/.test(b), + mozilla: /mozilla/.test(b) && !/(compatible|webkit)/.test(b) + }; + + // Check to see if the W3C box model is being used + jQuery.boxModel = !jQuery.browser.msie || document.compatMode == "CSS1Compat"; +}; + +jQuery.each({ + parent: "a.parentNode", + parents: "jQuery.parents(a)", + next: "jQuery.nth(a,2,'nextSibling')", + prev: "jQuery.nth(a,2,'previousSibling')", + siblings: "jQuery.sibling(a.parentNode.firstChild,a)", + children: "jQuery.sibling(a.firstChild)" +}, function(i,n){ + jQuery.fn[ i ] = function(a) { + var ret = jQuery.map(this,n); + if ( a && typeof a == "string" ) + ret = jQuery.multiFilter(a,ret); + return this.pushStack( ret ); + }; +}); + +jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after" +}, function(i,n){ + jQuery.fn[ i ] = function(){ + var a = arguments; + return this.each(function(){ + for ( var j = 0, al = a.length; j < al; j++ ) + jQuery(a[j])[n]( this ); + }); + }; +}); + +jQuery.each( { + removeAttr: function( key ) { + jQuery.attr( this, key, "" ); + this.removeAttribute( key ); + }, + addClass: function(c){ + jQuery.className.add(this,c); + }, + removeClass: function(c){ + jQuery.className.remove(this,c); + }, + toggleClass: function( c ){ + jQuery.className[ jQuery.className.has(this,c) ? "remove" : "add" ](this, c); + }, + remove: function(a){ + if ( !a || jQuery.filter( a, [this] ).r.length ) + this.parentNode.removeChild( this ); + }, + empty: function() { + while ( this.firstChild ) + this.removeChild( this.firstChild ); + } +}, function(i,n){ + jQuery.fn[ i ] = function() { + return this.each( n, arguments ); + }; +}); + +jQuery.each( [ "eq", "lt", "gt", "contains" ], function(i,n){ + jQuery.fn[ n ] = function(num,fn) { + return this.filter( ":" + n + "(" + num + ")", fn ); + }; +}); + +jQuery.each( [ "height", "width" ], function(i,n){ + jQuery.fn[ n ] = function(h) { + return h == undefined ? + ( this.length ? jQuery.css( this[0], n ) : null ) : + this.css( n, h.constructor == String ? h : h + "px" ); + }; +}); +jQuery.extend({ + expr: { + "": "m[2]=='*'||a.nodeName.toUpperCase()==m[2].toUpperCase()", + "#": "a.getAttribute('id')==m[2]", + ":": { + // Position Checks + lt: "i<m[3]-0", + gt: "i>m[3]-0", + nth: "m[3]-0==i", + eq: "m[3]-0==i", + first: "i==0", + last: "i==r.length-1", + even: "i%2==0", + odd: "i%2", + + // Child Checks + "nth-child": "jQuery.nth(a.parentNode.firstChild,m[3],'nextSibling',a)==a", + "first-child": "jQuery.nth(a.parentNode.firstChild,1,'nextSibling')==a", + "last-child": "jQuery.nth(a.parentNode.lastChild,1,'previousSibling')==a", + "only-child": "jQuery.sibling(a.parentNode.firstChild).length==1", + + // Parent Checks + parent: "a.firstChild", + empty: "!a.firstChild", + + // Text Check + contains: "jQuery.fn.text.apply([a]).indexOf(m[3])>=0", + + // Visibility + visible: 'a.type!="hidden"&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden"', + hidden: 'a.type=="hidden"||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden"', + + // Form attributes + enabled: "!a.disabled", + disabled: "a.disabled", + checked: "a.checked", + selected: "a.selected||jQuery.attr(a,'selected')", + + // Form elements + text: "a.type=='text'", + radio: "a.type=='radio'", + checkbox: "a.type=='checkbox'", + file: "a.type=='file'", + password: "a.type=='password'", + submit: "a.type=='submit'", + image: "a.type=='image'", + reset: "a.type=='reset'", + button: 'a.type=="button"||a.nodeName=="BUTTON"', + input: "/input|select|textarea|button/i.test(a.nodeName)" + }, + ".": "jQuery.className.has(a,m[2])", + "@": { + "=": "z==m[4]", + "!=": "z!=m[4]", + "^=": "z&&!z.indexOf(m[4])", + "$=": "z&&z.substr(z.length - m[4].length,m[4].length)==m[4]", + "*=": "z&&z.indexOf(m[4])>=0", + "": "z", + _resort: function(m){ + return ["", m[1], m[3], m[2], m[5]]; + }, + _prefix: "z=a[m[3]]||jQuery.attr(a,m[3]);" + }, + "[": "jQuery.find(m[2],a).length" + }, + + // The regular expressions that power the parsing engine + parse: [ + // Match: [@value='test'], [@foo] + /^\[ *(@)([a-z0-9_-]*) *([!*$^=]*) *('?"?)(.*?)\4 *\]/i, + + // Match: [div], [div p] + /^(\[)\s*(.*?(\[.*?\])?[^[]*?)\s*\]/, + + // Match: :contains('foo') + /^(:)([a-z0-9_-]*)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/i, + + // Match: :even, :last-chlid + /^([:.#]*)([a-z0-9_*-]*)/i + ], + + token: [ + /^(\/?\.\.)/, "a.parentNode", + /^(>|\/)/, "jQuery.sibling(a.firstChild)", + /^(\+)/, "jQuery.nth(a,2,'nextSibling')", + /^(~)/, function(a){ + var s = jQuery.sibling(a.parentNode.firstChild); + return s.slice(0, jQuery.inArray(a,s)); + } + ], + + multiFilter: function( expr, elems, not ) { + var old, cur = []; + + while ( expr && expr != old ) { + old = expr; + var f = jQuery.filter( expr, elems, not ); + expr = f.t.replace(/^\s*,\s*/, "" ); + cur = not ? elems = f.r : jQuery.merge( cur, f.r ); + } + + return cur; + }, + find: function( t, context ) { + // Quickly handle non-string expressions + if ( typeof t != "string" ) + return [ t ]; + + // Make sure that the context is a DOM Element + if ( context && !context.nodeType ) + context = null; + + // Set the correct context (if none is provided) + context = context || document; + + // Handle the common XPath // expression + if ( !t.indexOf("//") ) { + context = context.documentElement; + t = t.substr(2,t.length); + + // And the / root expression + } else if ( !t.indexOf("/") ) { + context = context.documentElement; + t = t.substr(1,t.length); + if ( t.indexOf("/") >= 1 ) + t = t.substr(t.indexOf("/"),t.length); + } + + // Initialize the search + var ret = [context], done = [], last = null; + + // Continue while a selector expression exists, and while + // we're no longer looping upon ourselves + while ( t && last != t ) { + var r = []; + last = t; + + t = jQuery.trim(t).replace( /^\/\//i, "" ); + + var foundToken = false; + + // An attempt at speeding up child selectors that + // point to a specific element tag + var re = /^[\/>]\s*([a-z0-9*-]+)/i; + var m = re.exec(t); + + if ( m ) { + // Perform our own iteration and filter + jQuery.each( ret, function(){ + for ( var c = this.firstChild; c; c = c.nextSibling ) + if ( c.nodeType == 1 && ( c.nodeName == m[1].toUpperCase() || m[1] == "*" ) ) + r.push( c ); + }); + + ret = r; + t = jQuery.trim( t.replace( re, "" ) ); + foundToken = true; + } else { + // Look for pre-defined expression tokens + for ( var i = 0; i < jQuery.token.length; i += 2 ) { + // Attempt to match each, individual, token in + // the specified order + var re = jQuery.token[i]; + var m = re.exec(t); + + // If the token match was found + if ( m ) { + // Map it against the token's handler + r = ret = jQuery.map( ret, jQuery.isFunction( jQuery.token[i+1] ) ? + jQuery.token[i+1] : + function(a){ return eval(jQuery.token[i+1]); }); + + // And remove the token + t = jQuery.trim( t.replace( re, "" ) ); + foundToken = true; + break; + } + } + } + + // See if there's still an expression, and that we haven't already + // matched a token + if ( t && !foundToken ) { + // Handle multiple expressions + if ( !t.indexOf(",") ) { + // Clean the result set + if ( ret[0] == context ) ret.shift(); + + // Merge the result sets + jQuery.merge( done, ret ); + + // Reset the context + r = ret = [context]; + + // Touch up the selector string + t = " " + t.substr(1,t.length); + + } else { + // Optomize for the case nodeName#idName + var re2 = /^([a-z0-9_-]+)(#)([a-z0-9\\*_-]*)/i; + var m = re2.exec(t); + + // Re-organize the results, so that they're consistent + if ( m ) { + m = [ 0, m[2], m[3], m[1] ]; + + } else { + // Otherwise, do a traditional filter check for + // ID, class, and element selectors + re2 = /^([#.]?)([a-z0-9\\*_-]*)/i; + m = re2.exec(t); + } + + // Try to do a global search by ID, where we can + if ( m[1] == "#" && ret[ret.length-1].getElementById ) { + // Optimization for HTML document case + var oid = ret[ret.length-1].getElementById(m[2]); + + // Do a quick check for node name (where applicable) so + // that div#foo searches will be really fast + ret = r = oid && + (!m[3] || oid.nodeName == m[3].toUpperCase()) ? [oid] : []; + + } else { + // Pre-compile a regular expression to handle class searches + if ( m[1] == "." ) + var rec = new RegExp("(^|\\s)" + m[2] + "(\\s|$)"); + + // We need to find all descendant elements, it is more + // efficient to use getAll() when we are already further down + // the tree - we try to recognize that here + jQuery.each( ret, function(){ + // Grab the tag name being searched for + var tag = m[1] != "" || m[0] == "" ? "*" : m[2]; + + // Handle IE7 being really dumb about <object>s + if ( this.nodeName.toUpperCase() == "OBJECT" && tag == "*" ) + tag = "param"; + + jQuery.merge( r, + m[1] != "" && ret.length != 1 ? + jQuery.getAll( this, [], m[1], m[2], rec ) : + this.getElementsByTagName( tag ) + ); + }); + + // It's faster to filter by class and be done with it + if ( m[1] == "." && ret.length == 1 ) + r = jQuery.grep( r, function(e) { + return rec.test(e.className); + }); + + // Same with ID filtering + if ( m[1] == "#" && ret.length == 1 ) { + // Remember, then wipe out, the result set + var tmp = r; + r = []; + + // Then try to find the element with the ID + jQuery.each( tmp, function(){ + if ( this.getAttribute("id") == m[2] ) { + r = [ this ]; + return false; + } + }); + } + + ret = r; + } + + t = t.replace( re2, "" ); + } + + } + + // If a selector string still exists + if ( t ) { + // Attempt to filter it + var val = jQuery.filter(t,r); + ret = r = val.r; + t = jQuery.trim(val.t); + } + } + + // Remove the root context + if ( ret && ret[0] == context ) ret.shift(); + + // And combine the results + jQuery.merge( done, ret ); + + return done; + }, + + filter: function(t,r,not) { + // Look for common filter expressions + while ( t && /^[a-z[({<*:.#]/i.test(t) ) { + + var p = jQuery.parse, m; + + jQuery.each( p, function(i,re){ + + // Look for, and replace, string-like sequences + // and finally build a regexp out of it + m = re.exec( t ); + + if ( m ) { + // Remove what we just matched + t = t.substring( m[0].length ); + + // Re-organize the first match + if ( jQuery.expr[ m[1] ]._resort ) + m = jQuery.expr[ m[1] ]._resort( m ); + + return false; + } + }); + + // :not() is a special case that can be optimized by + // keeping it out of the expression list + if ( m[1] == ":" && m[2] == "not" ) + r = jQuery.filter(m[3], r, true).r; + + // Handle classes as a special case (this will help to + // improve the speed, as the regexp will only be compiled once) + else if ( m[1] == "." ) { + + var re = new RegExp("(^|\\s)" + m[2] + "(\\s|$)"); + r = jQuery.grep( r, function(e){ + return re.test(e.className || ""); + }, not); + + // Otherwise, find the expression to execute + } else { + var f = jQuery.expr[m[1]]; + if ( typeof f != "string" ) + f = jQuery.expr[m[1]][m[2]]; + + // Build a custom macro to enclose it + eval("f = function(a,i){" + + ( jQuery.expr[ m[1] ]._prefix || "" ) + + "return " + f + "}"); + + // Execute it against the current filter + r = jQuery.grep( r, f, not ); + } + } + + // Return an array of filtered elements (r) + // and the modified expression string (t) + return { r: r, t: t }; + }, + + getAll: function( o, r, token, name, re ) { + for ( var s = o.firstChild; s; s = s.nextSibling ) + if ( s.nodeType == 1 ) { + var add = true; + + if ( token == "." ) + add = s.className && re.test(s.className); + else if ( token == "#" ) + add = s.getAttribute("id") == name; + + if ( add ) + r.push( s ); + + if ( token == "#" && r.length ) break; + + if ( s.firstChild ) + jQuery.getAll( s, r, token, name, re ); + } + + return r; + }, + parents: function( elem ){ + var matched = []; + var cur = elem.parentNode; + while ( cur && cur != document ) { + matched.push( cur ); + cur = cur.parentNode; + } + return matched; + }, + nth: function(cur,result,dir,elem){ + result = result || 1; + var num = 0; + for ( ; cur; cur = cur[dir] ) { + if ( cur.nodeType == 1 ) num++; + if ( num == result || result == "even" && num % 2 == 0 && num > 1 && cur == elem || + result == "odd" && num % 2 == 1 && cur == elem ) return cur; + } + }, + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType == 1 && (!elem || n != elem) ) + r.push( n ); + } + + return r; + } +}); +/* + * A number of helper functions used for managing events. + * Many of the ideas behind this code orignated from + * Dean Edwards' addEvent library. + */ +jQuery.event = { + + // Bind an event to an element + // Original by Dean Edwards + add: function(element, type, handler, data) { + // For whatever reason, IE has trouble passing the window object + // around, causing it to be cloned in the process + if ( jQuery.browser.msie && element.setInterval != undefined ) + element = window; + + // if data is passed, bind to handler + if( data ) + handler.data = data; + + // Make sure that the function being executed has a unique ID + if ( !handler.guid ) + handler.guid = this.guid++; + + // Init the element's event structure + if (!element.events) + element.events = {}; + + // Get the current list of functions bound to this event + var handlers = element.events[type]; + + // If it hasn't been initialized yet + if (!handlers) { + // Init the event handler queue + handlers = element.events[type] = {}; + + // Remember an existing handler, if it's already there + if (element["on" + type]) + handlers[0] = element["on" + type]; + } + + // Add the function to the element's handler list + handlers[handler.guid] = handler; + + // And bind the global event handler to the element + element["on" + type] = this.handle; + + // Remember the function in a global list (for triggering) + if (!this.global[type]) + this.global[type] = []; + this.global[type].push( element ); + }, + + guid: 1, + global: {}, + + // Detach an event or set of events from an element + remove: function(element, type, handler) { + if (element.events) + if ( type && type.type ) + delete element.events[ type.type ][ type.handler.guid ]; + else if (type && element.events[type]) + if ( handler ) + delete element.events[type][handler.guid]; + else + for ( var i in element.events[type] ) + delete element.events[type][i]; + else + for ( var j in element.events ) + this.remove( element, j ); + }, + + trigger: function(type,data,element) { + // Clone the incoming data, if any + data = jQuery.makeArray(data || []); + + // Handle a global trigger + if ( !element ) { + var g = this.global[type]; + if ( g ) + jQuery.each( g, function(){ + jQuery.event.trigger( type, data, this ); + }); + + // Handle triggering a single element + } else if ( element["on" + type] ) { + // Pass along a fake event + data.unshift( this.fix({ type: type, target: element }) ); + + // Trigger the event + var val = element["on" + type].apply( element, data ); + + if ( val !== false && jQuery.isFunction( element[ type ] ) ) + element[ type ](); + } + }, + + handle: function(event) { + if ( typeof jQuery == "undefined" ) return false; + + // Empty object is for triggered events with no data + event = jQuery.event.fix( event || window.event || {} ); + + // returned undefined or false + var returnValue; + + var c = this.events[event.type]; + + var args = [].slice.call( arguments, 1 ); + args.unshift( event ); + + for ( var j in c ) { + // Pass in a reference to the handler function itself + // So that we can later remove it + args[0].handler = c[j]; + args[0].data = c[j].data; + + if ( c[j].apply( this, args ) === false ) { + event.preventDefault(); + event.stopPropagation(); + returnValue = false; + } + } + + // Clean up added properties in IE to prevent memory leak + if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null; + + return returnValue; + }, + + fix: function(event) { + // Fix target property, if necessary + if ( !event.target && event.srcElement ) + event.target = event.srcElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == undefined && event.clientX != undefined ) { + var e = document.documentElement, b = document.body; + event.pageX = event.clientX + (e.scrollLeft || b.scrollLeft); + event.pageY = event.clientY + (e.scrollTop || b.scrollTop); + } + + // check if target is a textnode (safari) + if (jQuery.browser.safari && event.target.nodeType == 3) { + // store a copy of the original event object + // and clone because target is read only + var originalEvent = event; + event = jQuery.extend({}, originalEvent); + + // get parentnode from textnode + event.target = originalEvent.target.parentNode; + + // add preventDefault and stopPropagation since + // they will not work on the clone + event.preventDefault = function() { + return originalEvent.preventDefault(); + }; + event.stopPropagation = function() { + return originalEvent.stopPropagation(); + }; + } + + // fix preventDefault and stopPropagation + if (!event.preventDefault) + event.preventDefault = function() { + this.returnValue = false; + }; + + if (!event.stopPropagation) + event.stopPropagation = function() { + this.cancelBubble = true; + }; + + return event; + } +}; + +jQuery.fn.extend({ + bind: function( type, data, fn ) { + return this.each(function(){ + jQuery.event.add( this, type, fn || data, data ); + }); + }, + one: function( type, data, fn ) { + return this.each(function(){ + jQuery.event.add( this, type, function(event) { + jQuery(this).unbind(event); + return (fn || data).apply( this, arguments); + }, data); + }); + }, + unbind: function( type, fn ) { + return this.each(function(){ + jQuery.event.remove( this, type, fn ); + }); + }, + trigger: function( type, data ) { + return this.each(function(){ + jQuery.event.trigger( type, data, this ); + }); + }, + toggle: function() { + // Save reference to arguments for access in closure + var a = arguments; + + return this.click(function(e) { + // Figure out which function to execute + this.lastToggle = this.lastToggle == 0 ? 1 : 0; + + // Make sure that clicks stop + e.preventDefault(); + + // and execute the function + return a[this.lastToggle].apply( this, [e] ) || false; + }); + }, + hover: function(f,g) { + + // A private function for handling mouse 'hovering' + function handleHover(e) { + // Check if mouse(over|out) are still within the same parent element + var p = (e.type == "mouseover" ? e.fromElement : e.toElement) || e.relatedTarget; + + // Traverse up the tree + while ( p && p != this ) try { p = p.parentNode } catch(e) { p = this; }; + + // If we actually just moused on to a sub-element, ignore it + if ( p == this ) return false; + + // Execute the right function + return (e.type == "mouseover" ? f : g).apply(this, [e]); + } + + // Bind the function to the two event listeners + return this.mouseover(handleHover).mouseout(handleHover); + }, + ready: function(f) { + // If the DOM is already ready + if ( jQuery.isReady ) + // Execute the function immediately + f.apply( document, [jQuery] ); + + // Otherwise, remember the function for later + else { + // Add the function to the wait list + jQuery.readyList.push( function() { return f.apply(this, [jQuery]) } ); + } + + return this; + } +}); + +jQuery.extend({ + /* + * All the code that makes DOM Ready work nicely. + */ + isReady: false, + readyList: [], + + // Handle when the DOM is ready + ready: function() { + // Make sure that the DOM is not already loaded + if ( !jQuery.isReady ) { + // Remember that the DOM is ready + jQuery.isReady = true; + + // If there are functions bound, to execute + if ( jQuery.readyList ) { + // Execute all of them + jQuery.each( jQuery.readyList, function(){ + this.apply( document ); + }); + + // Reset the list of functions + jQuery.readyList = null; + } + // Remove event lisenter to avoid memory leak + if ( jQuery.browser.mozilla || jQuery.browser.opera ) + document.removeEventListener( "DOMContentLoaded", jQuery.ready, false ); + } + } +}); + +new function(){ + + jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," + + "mousedown,mouseup,mousemove,mouseover,mouseout,change,select," + + "submit,keydown,keypress,keyup,error").split(","), function(i,o){ + + // Handle event binding + jQuery.fn[o] = function(f){ + return f ? this.bind(o, f) : this.trigger(o); + }; + + }); + + // If Mozilla is used + if ( jQuery.browser.mozilla || jQuery.browser.opera ) + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", jQuery.ready, false ); + + // If IE is used, use the excellent hack by Matthias Miller + // http://www.outofhanwell.com/blog/index.php?title=the_window_onload_problem_revisited + else if ( jQuery.browser.msie ) { + + // Only works if you document.write() it + document.write("<scr" + "ipt id=__ie_init defer=true " + + "src=//:><\/script>"); + + // Use the defer script hack + var script = document.getElementById("__ie_init"); + + // script does not exist if jQuery is loaded dynamically + if ( script ) + script.onreadystatechange = function() { + if ( this.readyState != "complete" ) return; + this.parentNode.removeChild( this ); + jQuery.ready(); + }; + + // Clear from memory + script = null; + + // If Safari is used + } else if ( jQuery.browser.safari ) + // Continually check to see if the document.readyState is valid + jQuery.safariTimer = setInterval(function(){ + // loaded and complete are both valid states + if ( document.readyState == "loaded" || + document.readyState == "complete" ) { + + // If either one are found, remove the timer + clearInterval( jQuery.safariTimer ); + jQuery.safariTimer = null; + + // and execute any waiting functions + jQuery.ready(); + } + }, 10); + + // A fallback to window.onload, that will always work + jQuery.event.add( window, "load", jQuery.ready ); + +}; + +// Clean up after IE to avoid memory leaks +if (jQuery.browser.msie) + jQuery(window).one("unload", function() { + var global = jQuery.event.global; + for ( var type in global ) { + var els = global[type], i = els.length; + if ( i && type != 'unload' ) + do + jQuery.event.remove(els[i-1], type); + while (--i); + } + }); +jQuery.fn.extend({ + + show: function(speed,callback){ + var hidden = this.filter(":hidden"); + return speed ? + hidden.animate({ + height: "show", width: "show", opacity: "show" + }, speed, callback) : + + hidden.each(function(){ + this.style.display = this.oldblock ? this.oldblock : ""; + if ( jQuery.css(this,"display") == "none" ) + this.style.display = "block"; + }); + }, + + hide: function(speed,callback){ + var visible = this.filter(":visible"); + return speed ? + visible.animate({ + height: "hide", width: "hide", opacity: "hide" + }, speed, callback) : + + visible.each(function(){ + this.oldblock = this.oldblock || jQuery.css(this,"display"); + if ( this.oldblock == "none" ) + this.oldblock = "block"; + this.style.display = "none"; + }); + }, + + // Save the old toggle function + _toggle: jQuery.fn.toggle, + toggle: function( fn, fn2 ){ + var args = arguments; + return jQuery.isFunction(fn) && jQuery.isFunction(fn2) ? + this._toggle( fn, fn2 ) : + this.each(function(){ + jQuery(this)[ jQuery(this).is(":hidden") ? "show" : "hide" ] + .apply( jQuery(this), args ); + }); + }, + slideDown: function(speed,callback){ + return this.animate({height: "show"}, speed, callback); + }, + slideUp: function(speed,callback){ + return this.animate({height: "hide"}, speed, callback); + }, + slideToggle: function(speed, callback){ + return this.each(function(){ + var state = jQuery(this).is(":hidden") ? "show" : "hide"; + jQuery(this).animate({height: state}, speed, callback); + }); + }, + fadeIn: function(speed, callback){ + return this.animate({opacity: "show"}, speed, callback); + }, + fadeOut: function(speed, callback){ + return this.animate({opacity: "hide"}, speed, callback); + }, + fadeTo: function(speed,to,callback){ + return this.animate({opacity: to}, speed, callback); + }, + animate: function( prop, speed, easing, callback ) { + return this.queue(function(){ + + this.curAnim = jQuery.extend({}, prop); + var opt = jQuery.speed(speed, easing, callback); + + for ( var p in prop ) { + var e = new jQuery.fx( this, opt, p ); + if ( prop[p].constructor == Number ) + e.custom( e.cur(), prop[p] ); + else + e[ prop[p] ]( prop ); + } + + }); + }, + queue: function(type,fn){ + if ( !fn ) { + fn = type; + type = "fx"; + } + + return this.each(function(){ + if ( !this.queue ) + this.queue = {}; + + if ( !this.queue[type] ) + this.queue[type] = []; + + this.queue[type].push( fn ); + + if ( this.queue[type].length == 1 ) + fn.apply(this); + }); + } + +}); + +jQuery.extend({ + + speed: function(speed, easing, fn) { + var opt = speed && speed.constructor == Object ? speed : { + complete: fn || !fn && easing || + jQuery.isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && easing.constructor != Function && easing + }; + + opt.duration = (opt.duration && opt.duration.constructor == Number ? + opt.duration : + { slow: 600, fast: 200 }[opt.duration]) || 400; + + // Queueing + opt.old = opt.complete; + opt.complete = function(){ + jQuery.dequeue(this, "fx"); + if ( jQuery.isFunction( opt.old ) ) + opt.old.apply( this ); + }; + + return opt; + }, + + easing: {}, + + queue: {}, + + dequeue: function(elem,type){ + type = type || "fx"; + + if ( elem.queue && elem.queue[type] ) { + // Remove self + elem.queue[type].shift(); + + // Get next function + var f = elem.queue[type][0]; + + if ( f ) f.apply( elem ); + } + }, + + /* + * I originally wrote fx() as a clone of moo.fx and in the process + * of making it small in size the code became illegible to sane + * people. You've been warned. + */ + + fx: function( elem, options, prop ){ + + var z = this; + + // The styles + var y = elem.style; + + // Store display property + var oldDisplay = jQuery.css(elem, "display"); + + // Set display property to block for animation + y.display = "block"; + + // Make sure that nothing sneaks out + y.overflow = "hidden"; + + // Simple function for setting a style value + z.a = function(){ + if ( options.step ) + options.step.apply( elem, [ z.now ] ); + + if ( prop == "opacity" ) + jQuery.attr(y, "opacity", z.now); // Let attr handle opacity + else if ( parseInt(z.now) ) // My hate for IE will never die + y[prop] = parseInt(z.now) + "px"; + }; + + // Figure out the maximum number to run to + z.max = function(){ + return parseFloat( jQuery.css(elem,prop) ); + }; + + // Get the current size + z.cur = function(){ + var r = parseFloat( jQuery.curCSS(elem, prop) ); + return r && r > -10000 ? r : z.max(); + }; + + // Start an animation from one number to another + z.custom = function(from,to){ + z.startTime = (new Date()).getTime(); + z.now = from; + z.a(); + + z.timer = setInterval(function(){ + z.step(from, to); + }, 13); + }; + + // Simple 'show' function + z.show = function(){ + if ( !elem.orig ) elem.orig = {}; + + // Remember where we started, so that we can go back to it later + elem.orig[prop] = this.cur(); + + options.show = true; + + // Begin the animation + z.custom(0, elem.orig[prop]); + + // Stupid IE, look what you made me do + if ( prop != "opacity" ) + y[prop] = "1px"; + }; + + // Simple 'hide' function + z.hide = function(){ + if ( !elem.orig ) elem.orig = {}; + + // Remember where we started, so that we can go back to it later + elem.orig[prop] = this.cur(); + + options.hide = true; + + // Begin the animation + z.custom(elem.orig[prop], 0); + }; + + //Simple 'toggle' function + z.toggle = function() { + if ( !elem.orig ) elem.orig = {}; + + // Remember where we started, so that we can go back to it later + elem.orig[prop] = this.cur(); + + if(oldDisplay == "none") { + options.show = true; + + // Stupid IE, look what you made me do + if ( prop != "opacity" ) + y[prop] = "1px"; + + // Begin the animation + z.custom(0, elem.orig[prop]); + } else { + options.hide = true; + + // Begin the animation + z.custom(elem.orig[prop], 0); + } + }; + + // Each step of an animation + z.step = function(firstNum, lastNum){ + var t = (new Date()).getTime(); + + if (t > options.duration + z.startTime) { + // Stop the timer + clearInterval(z.timer); + z.timer = null; + + z.now = lastNum; + z.a(); + + if (elem.curAnim) elem.curAnim[ prop ] = true; + + var done = true; + for ( var i in elem.curAnim ) + if ( elem.curAnim[i] !== true ) + done = false; + + if ( done ) { + // Reset the overflow + y.overflow = ""; + + // Reset the display + y.display = oldDisplay; + if (jQuery.css(elem, "display") == "none") + y.display = "block"; + + // Hide the element if the "hide" operation was done + if ( options.hide ) + y.display = "none"; + + // Reset the properties, if the item has been hidden or shown + if ( options.hide || options.show ) + for ( var p in elem.curAnim ) + if (p == "opacity") + jQuery.attr(y, p, elem.orig[p]); + else + y[p] = ""; + } + + // If a callback was provided, execute it + if ( done && jQuery.isFunction( options.complete ) ) + // Execute the complete function + options.complete.apply( elem ); + } else { + var n = t - this.startTime; + // Figure out where in the animation we are and set the number + var p = n / options.duration; + + // If the easing function exists, then use it + z.now = options.easing && jQuery.easing[options.easing] ? + jQuery.easing[options.easing](p, n, firstNum, (lastNum-firstNum), options.duration) : + // else use default linear easing + ((-Math.cos(p*Math.PI)/2) + 0.5) * (lastNum-firstNum) + firstNum; + + // Perform the next step of the animation + z.a(); + } + }; + + } +}); +jQuery.fn.extend({ + loadIfModified: function( url, params, callback ) { + this.load( url, params, callback, 1 ); + }, + load: function( url, params, callback, ifModified ) { + if ( jQuery.isFunction( url ) ) + return this.bind("load", url); + + callback = callback || function(){}; + + // Default to a GET request + var type = "GET"; + + // If the second parameter was provided + if ( params ) + // If it's a function + if ( jQuery.isFunction( params.constructor ) ) { + // We assume that it's the callback + callback = params; + params = null; + + // Otherwise, build a param string + } else { + params = jQuery.param( params ); + type = "POST"; + } + + var self = this; + + // Request the remote document + jQuery.ajax({ + url: url, + type: type, + data: params, + ifModified: ifModified, + complete: function(res, status){ + if ( status == "success" || !ifModified && status == "notmodified" ) + // Inject the HTML into all the matched elements + self.attr("innerHTML", res.responseText) + // Execute all the scripts inside of the newly-injected HTML + .evalScripts() + // Execute callback + .each( callback, [res.responseText, status, res] ); + else + callback.apply( self, [res.responseText, status, res] ); + } + }); + return this; + }, + serialize: function() { + return jQuery.param( this ); + }, + evalScripts: function() { + return this.find("script").each(function(){ + if ( this.src ) + jQuery.getScript( this.src ); + else + jQuery.globalEval( this.text || this.textContent || this.innerHTML || "" ); + }).end(); + } + +}); + +// If IE is used, create a wrapper for the XMLHttpRequest object +if ( jQuery.browser.msie && typeof XMLHttpRequest == "undefined" ) + XMLHttpRequest = function(){ + return new ActiveXObject("Microsoft.XMLHTTP"); + }; + +// Attach a bunch of functions for handling common AJAX events + +jQuery.each( "ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","), function(i,o){ + jQuery.fn[o] = function(f){ + return this.bind(o, f); + }; +}); + +jQuery.extend({ + get: function( url, data, callback, type, ifModified ) { + // shift arguments if data argument was ommited + if ( jQuery.isFunction( data ) ) { + callback = data; + data = null; + } + + return jQuery.ajax({ + url: url, + data: data, + success: callback, + dataType: type, + ifModified: ifModified + }); + }, + getIfModified: function( url, data, callback, type ) { + return jQuery.get(url, data, callback, type, 1); + }, + getScript: function( url, callback ) { + return jQuery.get(url, null, callback, "script"); + }, + getJSON: function( url, data, callback ) { + return jQuery.get(url, data, callback, "json"); + }, + post: function( url, data, callback, type ) { + return jQuery.ajax({ + type: "POST", + url: url, + data: data, + success: callback, + dataType: type + }); + }, + + // timeout (ms) + //timeout: 0, + ajaxTimeout: function( timeout ) { + jQuery.ajaxSettings.timeout = timeout; + }, + ajaxSetup: function( settings ) { + jQuery.extend( jQuery.ajaxSettings, settings ); + }, + + ajaxSettings: { + global: true, + type: "GET", + timeout: 0, + contentType: "application/x-www-form-urlencoded", + processData: true, + async: true, + data: null + }, + + // Last-Modified header cache for next request + lastModified: {}, + ajax: function( s ) { + // TODO introduce global settings, allowing the client to modify them for all requests, not only timeout + s = jQuery.extend({}, jQuery.ajaxSettings, s); + + // if data available + if ( s.data ) { + // convert data if not already a string + if (s.processData && typeof s.data != "string") + s.data = jQuery.param(s.data); + // append data to url for get requests + if( s.type.toLowerCase() == "get" ) + // "?" + data or "&" + data (in case there are already params) + s.url += ((s.url.indexOf("?") > -1) ? "&" : "?") + s.data; + } + + // Watch for a new set of requests + if ( s.global && ! jQuery.active++ ) + jQuery.event.trigger( "ajaxStart" ); + + var requestDone = false; + + // Create the request object + var xml = new XMLHttpRequest(); + + // Open the socket + xml.open(s.type, s.url, s.async); + + // Set the correct header, if data is being sent + if ( s.data ) + xml.setRequestHeader("Content-Type", s.contentType); + + // Set the If-Modified-Since header, if ifModified mode. + if ( s.ifModified ) + xml.setRequestHeader("If-Modified-Since", + jQuery.lastModified[s.url] || "Thu, 01 Jan 1970 00:00:00 GMT" ); + + // Set header so the called script knows that it's an XMLHttpRequest + xml.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + + // Make sure the browser sends the right content length + if ( xml.overrideMimeType ) + xml.setRequestHeader("Connection", "close"); + + // Allow custom headers/mimetypes + if( s.beforeSend ) + s.beforeSend(xml); + + if ( s.global ) + jQuery.event.trigger("ajaxSend", [xml, s]); + + // Wait for a response to come back + var onreadystatechange = function(isTimeout){ + // The transfer is complete and the data is available, or the request timed out + if ( xml && (xml.readyState == 4 || isTimeout == "timeout") ) { + requestDone = true; + var status; + try { + status = jQuery.httpSuccess( xml ) && isTimeout != "timeout" ? + s.ifModified && jQuery.httpNotModified( xml, s.url ) ? "notmodified" : "success" : "error"; + // Make sure that the request was successful or notmodified + if ( status != "error" ) { + // Cache Last-Modified header, if ifModified mode. + var modRes; + try { + modRes = xml.getResponseHeader("Last-Modified"); + } catch(e) {} // swallow exception thrown by FF if header is not available + + if ( s.ifModified && modRes ) + jQuery.lastModified[s.url] = modRes; + + // process the data (runs the xml through httpData regardless of callback) + var data = jQuery.httpData( xml, s.dataType ); + + // If a local callback was specified, fire it and pass it the data + if ( s.success ) + s.success( data, status ); + + // Fire the global callback + if( s.global ) + jQuery.event.trigger( "ajaxSuccess", [xml, s] ); + } else + jQuery.handleError(s, xml, status); + } catch(e) { + status = "error"; + jQuery.handleError(s, xml, status, e); + } + + // The request was completed + if( s.global ) + jQuery.event.trigger( "ajaxComplete", [xml, s] ); + + // Handle the global AJAX counter + if ( s.global && ! --jQuery.active ) + jQuery.event.trigger( "ajaxStop" ); + + // Process result + if ( s.complete ) + s.complete(xml, status); + + // Stop memory leaks + xml.onreadystatechange = function(){}; + xml = null; + } + }; + xml.onreadystatechange = onreadystatechange; + + // Timeout checker + if ( s.timeout > 0 ) + setTimeout(function(){ + // Check to see if the request is still happening + if ( xml ) { + // Cancel the request + xml.abort(); + + if( !requestDone ) + onreadystatechange( "timeout" ); + } + }, s.timeout); + + // save non-leaking reference + var xml2 = xml; + + // Send the data + try { + xml2.send(s.data); + } catch(e) { + jQuery.handleError(s, xml, null, e); + } + + // firefox 1.5 doesn't fire statechange for sync requests + if ( !s.async ) + onreadystatechange(); + + // return XMLHttpRequest to allow aborting the request etc. + return xml2; + }, + + handleError: function( s, xml, status, e ) { + // If a local callback was specified, fire it + if ( s.error ) s.error( xml, status, e ); + + // Fire the global callback + if ( s.global ) + jQuery.event.trigger( "ajaxError", [xml, s, e] ); + }, + + // Counter for holding the number of active queries + active: 0, + + // Determines if an XMLHttpRequest was successful or not + httpSuccess: function( r ) { + try { + return !r.status && location.protocol == "file:" || + ( r.status >= 200 && r.status < 300 ) || r.status == 304 || + jQuery.browser.safari && r.status == undefined; + } catch(e){} + return false; + }, + + // Determines if an XMLHttpRequest returns NotModified + httpNotModified: function( xml, url ) { + try { + var xmlRes = xml.getResponseHeader("Last-Modified"); + + // Firefox always returns 200. check Last-Modified date + return xml.status == 304 || xmlRes == jQuery.lastModified[url] || + jQuery.browser.safari && xml.status == undefined; + } catch(e){} + return false; + }, + + /* Get the data out of an XMLHttpRequest. + * Return parsed XML if content-type header is "xml" and type is "xml" or omitted, + * otherwise return plain text. + * (String) data - The type of data that you're expecting back, + * (e.g. "xml", "html", "script") + */ + httpData: function( r, type ) { + var ct = r.getResponseHeader("content-type"); + var data = !type && ct && ct.indexOf("xml") >= 0; + data = type == "xml" || data ? r.responseXML : r.responseText; + + // If the type is "script", eval it in global context + if ( type == "script" ) + jQuery.globalEval( data ); + + // Get the JavaScript object, if JSON is used. + if ( type == "json" ) + eval( "data = " + data ); + + // evaluate scripts within html + if ( type == "html" ) + jQuery("<div>").html(data).evalScripts(); + + return data; + }, + + // Serialize an array of form elements or a set of + // key/values into a query string + param: function( a ) { + var s = []; + + // If an array was passed in, assume that it is an array + // of form elements + if ( a.constructor == Array || a.jquery ) + // Serialize the form elements + jQuery.each( a, function(){ + s.push( encodeURIComponent(this.name) + "=" + encodeURIComponent( this.value ) ); + }); + + // Otherwise, assume that it's an object of key/value pairs + else + // Serialize the key/values + for ( var j in a ) + // If the value is an array then the key names need to be repeated + if ( a[j].constructor == Array ) + jQuery.each( a[j], function(){ + s.push( encodeURIComponent(j) + "=" + encodeURIComponent( this ) ); + }); + else + s.push( encodeURIComponent(j) + "=" + encodeURIComponent( a[j] ) ); + + // Return the resulting serialization + return s.join("&"); + }, + + // evalulates a script in global context + // not reliable for safari + globalEval: function( data ) { + if ( window.execScript ) + window.execScript( data ); + else if ( jQuery.browser.safari ) + // safari doesn't provide a synchronous global eval + window.setTimeout( data, 0 ); + else + eval.call( window, data ); + } + +}); +} diff --git a/devtools/client/inspector/markup/test/lib_jquery_1.11.1_min.js b/devtools/client/inspector/markup/test/lib_jquery_1.11.1_min.js new file mode 100644 index 000000000..ab28a2472 --- /dev/null +++ b/devtools/client/inspector/markup/test/lib_jquery_1.11.1_min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.11.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.1",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b=a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="<div class='a'></div><div class='a i'></div>",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="<select msallowclip=''><option selected=''></option></select>",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=lb(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=mb(b);function pb(){}pb.prototype=d.filters=d.pseudos,d.setFilters=new pb,g=fb.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=S.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=T.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(R," ")}),h=h.slice(c.length));for(g in d.filter)!(e=X[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?fb.error(a):z(a,i).slice(0)};function qb(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h; +if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?m.queue(this[0],a):void 0===b?this:this.each(function(){var c=m.queue(this,a,b);m._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&m.dequeue(this,a)})},dequeue:function(a){return this.each(function(){m.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=m.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=m._data(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var S=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=["Top","Right","Bottom","Left"],U=function(a,b){return a=b||a,"none"===m.css(a,"display")||!m.contains(a.ownerDocument,a)},V=m.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===m.type(c)){e=!0;for(h in c)m.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,m.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(m(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav></:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="<input type='radio' checked='checked' name='t'/>",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function ab(){return!0}function bb(){return!1}function cb(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},fix:function(a){if(a[m.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=Z.test(e)?this.mouseHooks:Y.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new m.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=f.srcElement||y),3===a.target.nodeType&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,g.filter?g.filter(a,f):a},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button,g=b.fromElement;return null==a.pageX&&null!=b.clientX&&(d=a.target.ownerDocument||y,e=d.documentElement,c=d.body,a.pageX=b.clientX+(e&&e.scrollLeft||c&&c.scrollLeft||0)-(e&&e.clientLeft||c&&c.clientLeft||0),a.pageY=b.clientY+(e&&e.scrollTop||c&&c.scrollTop||0)-(e&&e.clientTop||c&&c.clientTop||0)),!a.relatedTarget&&g&&(a.relatedTarget=g===a.target?b.toElement:g),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==cb()&&this.focus)try{return this.focus(),!1}catch(a){}},delegateType:"focusin"},blur:{trigger:function(){return this===cb()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return m.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(a){return m.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=m.extend(new m.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?m.event.trigger(e,null,b):m.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},m.removeEvent=y.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){var d="on"+b;a.detachEvent&&(typeof a[d]===K&&(a[d]=null),a.detachEvent(d,c))},m.Event=function(a,b){return this instanceof m.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?ab:bb):this.type=a,b&&m.extend(this,b),this.timeStamp=a&&a.timeStamp||m.now(),void(this[m.expando]=!0)):new m.Event(a,b)},m.Event.prototype={isDefaultPrevented:bb,isPropagationStopped:bb,isImmediatePropagationStopped:bb,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=ab,a&&(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=ab,a&&(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=ab,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},m.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){m.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!m.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),k.submitBubbles||(m.event.special.submit={setup:function(){return m.nodeName(this,"form")?!1:void m.event.add(this,"click._submit keypress._submit",function(a){var b=a.target,c=m.nodeName(b,"input")||m.nodeName(b,"button")?b.form:void 0;c&&!m._data(c,"submitBubbles")&&(m.event.add(c,"submit._submit",function(a){a._submit_bubble=!0}),m._data(c,"submitBubbles",!0))})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&m.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){return m.nodeName(this,"form")?!1:void m.event.remove(this,"._submit")}}),k.changeBubbles||(m.event.special.change={setup:function(){return X.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(m.event.add(this,"propertychange._change",function(a){"checked"===a.originalEvent.propertyName&&(this._just_changed=!0)}),m.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1),m.event.simulate("change",this,a,!0)})),!1):void m.event.add(this,"beforeactivate._change",function(a){var b=a.target;X.test(b.nodeName)&&!m._data(b,"changeBubbles")&&(m.event.add(b,"change._change",function(a){!this.parentNode||a.isSimulated||a.isTrigger||m.event.simulate("change",this.parentNode,a,!0)}),m._data(b,"changeBubbles",!0))})},handle:function(a){var b=a.target;return this!==b||a.isSimulated||a.isTrigger||"radio"!==b.type&&"checkbox"!==b.type?a.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return m.event.remove(this,"._change"),!X.test(this.nodeName)}}),k.focusinBubbles||m.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){m.event.simulate(b,a.target,m.event.fix(a),!0)};m.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=m._data(d,b);e||d.addEventListener(a,c,!0),m._data(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=m._data(d,b)-1;e?m._data(d,b,e):(d.removeEventListener(a,c,!0),m._removeData(d,b))}}}),m.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(f in a)this.on(f,b,c,a[f],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=bb;else if(!d)return this;return 1===e&&(g=d,d=function(a){return m().off(a),g.apply(this,arguments)},d.guid=g.guid||(g.guid=m.guid++)),this.each(function(){m.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,m(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=bb),this.each(function(){m.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){m.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?m.event.trigger(a,b,c,!0):void 0}});function db(a){var b=eb.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}var eb="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",fb=/ jQuery\d+="(?:null|\d+)"/g,gb=new RegExp("<(?:"+eb+")[\\s/>]","i"),hb=/^\s+/,ib=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,jb=/<([\w:]+)/,kb=/<tbody/i,lb=/<|&#?\w+;/,mb=/<(?:script|style|link)/i,nb=/checked\s*(?:[^=]|=\s*.checked.)/i,ob=/^$|\/(?:java|ecma)script/i,pb=/^true\/(.*)/,qb=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,rb={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:k.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},sb=db(y),tb=sb.appendChild(y.createElement("div"));rb.optgroup=rb.option,rb.tbody=rb.tfoot=rb.colgroup=rb.caption=rb.thead,rb.th=rb.td;function ub(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ub(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function vb(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wb(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xb(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function yb(a){var b=pb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function zb(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Ab(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Bb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xb(b).text=a.text,yb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!gb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(tb.innerHTML=a.outerHTML,tb.removeChild(f=tb.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ub(f),h=ub(a),g=0;null!=(e=h[g]);++g)d[g]&&Bb(e,d[g]);if(b)if(c)for(h=h||ub(a),d=d||ub(f),g=0;null!=(e=h[g]);g++)Ab(e,d[g]);else Ab(a,f);return d=ub(f,"script"),d.length>0&&zb(d,!i&&ub(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=db(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(lb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(jb.exec(f)||["",""])[1].toLowerCase(),l=rb[i]||rb._default,h.innerHTML=l[1]+f.replace(ib,"<$1></$2>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&hb.test(f)&&p.push(b.createTextNode(hb.exec(f)[0])),!k.tbody){f="table"!==i||kb.test(f)?"<table>"!==l[1]||kb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ub(p,"input"),vb),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ub(o.appendChild(f),"script"),g&&zb(h),c)){e=0;while(f=h[e++])ob.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ub(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&zb(ub(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ub(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fb,""):void 0;if(!("string"!=typeof a||mb.test(a)||!k.htmlSerialize&&gb.test(a)||!k.leadingWhitespace&&hb.test(a)||rb[(jb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ib,"<$1></$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ub(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ub(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&nb.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ub(i,"script"),xb),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ub(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,yb),j=0;f>j;j++)d=g[j],ob.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qb,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Cb,Db={};function Eb(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fb(a){var b=y,c=Db[a];return c||(c=Eb(a,b),"none"!==c&&c||(Cb=(Cb||m("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=(Cb[0].contentWindow||Cb[0].contentDocument).document,b.write(),b.close(),c=Eb(a,b),Cb.detach()),Db[a]=c),c}!function(){var a;k.shrinkWrapBlocks=function(){if(null!=a)return a;a=!1;var b,c,d;return c=y.getElementsByTagName("body")[0],c&&c.style?(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",b.appendChild(y.createElement("div")).style.width="5px",a=3!==b.offsetWidth),c.removeChild(d),a):void 0}}();var Gb=/^margin/,Hb=new RegExp("^("+S+")(?!px)[a-z%]+$","i"),Ib,Jb,Kb=/^(top|right|bottom|left)$/;a.getComputedStyle?(Ib=function(a){return a.ownerDocument.defaultView.getComputedStyle(a,null)},Jb=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ib(a),g=c?c.getPropertyValue(b)||c[b]:void 0,c&&(""!==g||m.contains(a.ownerDocument,a)||(g=m.style(a,b)),Hb.test(g)&&Gb.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0===g?g:g+""}):y.documentElement.currentStyle&&(Ib=function(a){return a.currentStyle},Jb=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ib(a),g=c?c[b]:void 0,null==g&&h&&h[b]&&(g=h[b]),Hb.test(g)&&!Kb.test(b)&&(d=h.left,e=a.runtimeStyle,f=e&&e.left,f&&(e.left=a.currentStyle.left),h.left="fontSize"===b?"1em":g,g=h.pixelLeft+"px",h.left=d,f&&(e.left=f)),void 0===g?g:g+""||"auto"});function Lb(a,b){return{get:function(){var c=a();if(null!=c)return c?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d,e,f,g,h;if(b=y.createElement("div"),b.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",d=b.getElementsByTagName("a")[0],c=d&&d.style){c.cssText="float:left;opacity:.5",k.opacity="0.5"===c.opacity,k.cssFloat=!!c.cssFloat,b.style.backgroundClip="content-box",b.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===b.style.backgroundClip,k.boxSizing=""===c.boxSizing||""===c.MozBoxSizing||""===c.WebkitBoxSizing,m.extend(k,{reliableHiddenOffsets:function(){return null==g&&i(),g},boxSizingReliable:function(){return null==f&&i(),f},pixelPosition:function(){return null==e&&i(),e},reliableMarginRight:function(){return null==h&&i(),h}});function i(){var b,c,d,i;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),b.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",e=f=!1,h=!0,a.getComputedStyle&&(e="1%"!==(a.getComputedStyle(b,null)||{}).top,f="4px"===(a.getComputedStyle(b,null)||{width:"4px"}).width,i=b.appendChild(y.createElement("div")),i.style.cssText=b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",i.style.marginRight=i.style.width="0",b.style.width="1px",h=!parseFloat((a.getComputedStyle(i,null)||{}).marginRight)),b.innerHTML="<table><tr><td></td><td>t</td></tr></table>",i=b.getElementsByTagName("td"),i[0].style.cssText="margin:0;border:0;padding:0;display:none",g=0===i[0].offsetHeight,g&&(i[0].style.display="",i[1].style.display="none",g=0===i[0].offsetHeight),c.removeChild(d))}}}(),m.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var Mb=/alpha\([^)]*\)/i,Nb=/opacity\s*=\s*([^)]*)/,Ob=/^(none|table(?!-c[ea]).+)/,Pb=new RegExp("^("+S+")(.*)$","i"),Qb=new RegExp("^([+-])=("+S+")","i"),Rb={position:"absolute",visibility:"hidden",display:"block"},Sb={letterSpacing:"0",fontWeight:"400"},Tb=["Webkit","O","Moz","ms"];function Ub(a,b){if(b in a)return b;var c=b.charAt(0).toUpperCase()+b.slice(1),d=b,e=Tb.length;while(e--)if(b=Tb[e]+c,b in a)return b;return d}function Vb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=m._data(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&U(d)&&(f[g]=m._data(d,"olddisplay",Fb(d.nodeName)))):(e=U(d),(c&&"none"!==c||!e)&&m._data(d,"olddisplay",e?c:m.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}function Wb(a,b,c){var d=Pb.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Xb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=m.css(a,c+T[f],!0,e)),d?("content"===c&&(g-=m.css(a,"padding"+T[f],!0,e)),"margin"!==c&&(g-=m.css(a,"border"+T[f]+"Width",!0,e))):(g+=m.css(a,"padding"+T[f],!0,e),"padding"!==c&&(g+=m.css(a,"border"+T[f]+"Width",!0,e)));return g}function Yb(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=Ib(a),g=k.boxSizing&&"border-box"===m.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=Jb(a,b,f),(0>e||null==e)&&(e=a.style[b]),Hb.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Xb(a,b,c||(g?"border":"content"),d,f)+"px"}m.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Jb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":k.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=m.camelCase(b),i=a.style;if(b=m.cssProps[h]||(m.cssProps[h]=Ub(i,h)),g=m.cssHooks[b]||m.cssHooks[h],void 0===c)return g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b];if(f=typeof c,"string"===f&&(e=Qb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(m.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||m.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),!(g&&"set"in g&&void 0===(c=g.set(a,c,d)))))try{i[b]=c}catch(j){}}},css:function(a,b,c,d){var e,f,g,h=m.camelCase(b);return b=m.cssProps[h]||(m.cssProps[h]=Ub(a.style,h)),g=m.cssHooks[b]||m.cssHooks[h],g&&"get"in g&&(f=g.get(a,!0,c)),void 0===f&&(f=Jb(a,b,d)),"normal"===f&&b in Sb&&(f=Sb[b]),""===c||c?(e=parseFloat(f),c===!0||m.isNumeric(e)?e||0:f):f}}),m.each(["height","width"],function(a,b){m.cssHooks[b]={get:function(a,c,d){return c?Ob.test(m.css(a,"display"))&&0===a.offsetWidth?m.swap(a,Rb,function(){return Yb(a,b,d)}):Yb(a,b,d):void 0},set:function(a,c,d){var e=d&&Ib(a);return Wb(a,c,d?Xb(a,b,d,k.boxSizing&&"border-box"===m.css(a,"boxSizing",!1,e),e):0)}}}),k.opacity||(m.cssHooks.opacity={get:function(a,b){return Nb.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=m.isNumeric(b)?"alpha(opacity="+100*b+")":"",f=d&&d.filter||c.filter||"";c.zoom=1,(b>=1||""===b)&&""===m.trim(f.replace(Mb,""))&&c.removeAttribute&&(c.removeAttribute("filter"),""===b||d&&!d.filter)||(c.filter=Mb.test(f)?f.replace(Mb,e):f+" "+e)}}),m.cssHooks.marginRight=Lb(k.reliableMarginRight,function(a,b){return b?m.swap(a,{display:"inline-block"},Jb,[a,"marginRight"]):void 0}),m.each({margin:"",padding:"",border:"Width"},function(a,b){m.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+T[d]+b]=f[d]||f[d-2]||f[0];return e}},Gb.test(a)||(m.cssHooks[a+b].set=Wb)}),m.fn.extend({css:function(a,b){return V(this,function(a,b,c){var d,e,f={},g=0;if(m.isArray(b)){for(d=Ib(a),e=b.length;e>g;g++)f[b[g]]=m.css(a,b[g],!1,d);return f}return void 0!==c?m.style(a,b,c):m.css(a,b)},a,b,arguments.length>1)},show:function(){return Vb(this,!0)},hide:function(){return Vb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){U(this)?m(this).show():m(this).hide()})}});function Zb(a,b,c,d,e){return new Zb.prototype.init(a,b,c,d,e)}m.Tween=Zb,Zb.prototype={constructor:Zb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(m.cssNumber[c]?"":"px") +},cur:function(){var a=Zb.propHooks[this.prop];return a&&a.get?a.get(this):Zb.propHooks._default.get(this)},run:function(a){var b,c=Zb.propHooks[this.prop];return this.pos=b=this.options.duration?m.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Zb.propHooks._default.set(this),this}},Zb.prototype.init.prototype=Zb.prototype,Zb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=m.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){m.fx.step[a.prop]?m.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[m.cssProps[a.prop]]||m.cssHooks[a.prop])?m.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Zb.propHooks.scrollTop=Zb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},m.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},m.fx=Zb.prototype.init,m.fx.step={};var $b,_b,ac=/^(?:toggle|show|hide)$/,bc=new RegExp("^(?:([+-])=|)("+S+")([a-z%]*)$","i"),cc=/queueHooks$/,dc=[ic],ec={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=bc.exec(b),f=e&&e[3]||(m.cssNumber[a]?"":"px"),g=(m.cssNumber[a]||"px"!==f&&+d)&&bc.exec(m.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,m.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function fc(){return setTimeout(function(){$b=void 0}),$b=m.now()}function gc(a,b){var c,d={height:a},e=0;for(b=b?1:0;4>e;e+=2-b)c=T[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function hc(a,b,c){for(var d,e=(ec[b]||[]).concat(ec["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function ic(a,b,c){var d,e,f,g,h,i,j,l,n=this,o={},p=a.style,q=a.nodeType&&U(a),r=m._data(a,"fxshow");c.queue||(h=m._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,n.always(function(){n.always(function(){h.unqueued--,m.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[p.overflow,p.overflowX,p.overflowY],j=m.css(a,"display"),l="none"===j?m._data(a,"olddisplay")||Fb(a.nodeName):j,"inline"===l&&"none"===m.css(a,"float")&&(k.inlineBlockNeedsLayout&&"inline"!==Fb(a.nodeName)?p.zoom=1:p.display="inline-block")),c.overflow&&(p.overflow="hidden",k.shrinkWrapBlocks()||n.always(function(){p.overflow=c.overflow[0],p.overflowX=c.overflow[1],p.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],ac.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(q?"hide":"show")){if("show"!==e||!r||void 0===r[d])continue;q=!0}o[d]=r&&r[d]||m.style(a,d)}else j=void 0;if(m.isEmptyObject(o))"inline"===("none"===j?Fb(a.nodeName):j)&&(p.display=j);else{r?"hidden"in r&&(q=r.hidden):r=m._data(a,"fxshow",{}),f&&(r.hidden=!q),q?m(a).show():n.done(function(){m(a).hide()}),n.done(function(){var b;m._removeData(a,"fxshow");for(b in o)m.style(a,b,o[b])});for(d in o)g=hc(q?r[d]:0,d,n),d in r||(r[d]=g.start,q&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function jc(a,b){var c,d,e,f,g;for(c in a)if(d=m.camelCase(c),e=b[d],f=a[c],m.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=m.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function kc(a,b,c){var d,e,f=0,g=dc.length,h=m.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=$b||fc(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:m.extend({},b),opts:m.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:$b||fc(),duration:c.duration,tweens:[],createTween:function(b,c){var d=m.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(jc(k,j.opts.specialEasing);g>f;f++)if(d=dc[f].call(j,a,k,j.opts))return d;return m.map(k,hc,j),m.isFunction(j.opts.start)&&j.opts.start.call(a,j),m.fx.timer(m.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}m.Animation=m.extend(kc,{tweener:function(a,b){m.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],ec[c]=ec[c]||[],ec[c].unshift(b)},prefilter:function(a,b){b?dc.unshift(a):dc.push(a)}}),m.speed=function(a,b,c){var d=a&&"object"==typeof a?m.extend({},a):{complete:c||!c&&b||m.isFunction(a)&&a,duration:a,easing:c&&b||b&&!m.isFunction(b)&&b};return d.duration=m.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in m.fx.speeds?m.fx.speeds[d.duration]:m.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){m.isFunction(d.old)&&d.old.call(this),d.queue&&m.dequeue(this,d.queue)},d},m.fn.extend({fadeTo:function(a,b,c,d){return this.filter(U).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=m.isEmptyObject(a),f=m.speed(b,c,d),g=function(){var b=kc(this,m.extend({},a),f);(e||m._data(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=m.timers,g=m._data(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&cc.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&m.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=m._data(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=m.timers,g=d?d.length:0;for(c.finish=!0,m.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),m.each(["toggle","show","hide"],function(a,b){var c=m.fn[b];m.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(gc(b,!0),a,d,e)}}),m.each({slideDown:gc("show"),slideUp:gc("hide"),slideToggle:gc("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){m.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),m.timers=[],m.fx.tick=function(){var a,b=m.timers,c=0;for($b=m.now();c<b.length;c++)a=b[c],a()||b[c]!==a||b.splice(c--,1);b.length||m.fx.stop(),$b=void 0},m.fx.timer=function(a){m.timers.push(a),a()?m.fx.start():m.timers.pop()},m.fx.interval=13,m.fx.start=function(){_b||(_b=setInterval(m.fx.tick,m.fx.interval))},m.fx.stop=function(){clearInterval(_b),_b=null},m.fx.speeds={slow:600,fast:200,_default:400},m.fn.delay=function(a,b){return a=m.fx?m.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a,b,c,d,e;b=y.createElement("div"),b.setAttribute("className","t"),b.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",d=b.getElementsByTagName("a")[0],c=y.createElement("select"),e=c.appendChild(y.createElement("option")),a=b.getElementsByTagName("input")[0],d.style.cssText="top:1px",k.getSetAttribute="t"!==b.className,k.style=/top/.test(d.getAttribute("style")),k.hrefNormalized="/a"===d.getAttribute("href"),k.checkOn=!!a.value,k.optSelected=e.selected,k.enctype=!!y.createElement("form").enctype,c.disabled=!0,k.optDisabled=!e.disabled,a=y.createElement("input"),a.setAttribute("value",""),k.input=""===a.getAttribute("value"),a.value="t",a.setAttribute("type","radio"),k.radioValue="t"===a.value}();var lc=/\r/g;m.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=m.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,m(this).val()):a,null==e?e="":"number"==typeof e?e+="":m.isArray(e)&&(e=m.map(e,function(a){return null==a?"":a+""})),b=m.valHooks[this.type]||m.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=m.valHooks[e.type]||m.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(lc,""):null==c?"":c)}}}),m.extend({valHooks:{option:{get:function(a){var b=m.find.attr(a,"value");return null!=b?b:m.trim(m.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&m.nodeName(c.parentNode,"optgroup"))){if(b=m(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=m.makeArray(b),g=e.length;while(g--)if(d=e[g],m.inArray(m.valHooks.option.get(d),f)>=0)try{d.selected=c=!0}catch(h){d.scrollHeight}else d.selected=!1;return c||(a.selectedIndex=-1),e}}}}),m.each(["radio","checkbox"],function(){m.valHooks[this]={set:function(a,b){return m.isArray(b)?a.checked=m.inArray(m(a).val(),b)>=0:void 0}},k.checkOn||(m.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var mc,nc,oc=m.expr.attrHandle,pc=/^(?:checked|selected)$/i,qc=k.getSetAttribute,rc=k.input;m.fn.extend({attr:function(a,b){return V(this,m.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){m.removeAttr(this,a)})}}),m.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===K?m.prop(a,b,c):(1===f&&m.isXMLDoc(a)||(b=b.toLowerCase(),d=m.attrHooks[b]||(m.expr.match.bool.test(b)?nc:mc)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=m.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void m.removeAttr(a,b))},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=m.propFix[c]||c,m.expr.match.bool.test(c)?rc&&qc||!pc.test(c)?a[d]=!1:a[m.camelCase("default-"+c)]=a[d]=!1:m.attr(a,c,""),a.removeAttribute(qc?c:d)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&m.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),nc={set:function(a,b,c){return b===!1?m.removeAttr(a,c):rc&&qc||!pc.test(c)?a.setAttribute(!qc&&m.propFix[c]||c,c):a[m.camelCase("default-"+c)]=a[c]=!0,c}},m.each(m.expr.match.bool.source.match(/\w+/g),function(a,b){var c=oc[b]||m.find.attr;oc[b]=rc&&qc||!pc.test(b)?function(a,b,d){var e,f;return d||(f=oc[b],oc[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,oc[b]=f),e}:function(a,b,c){return c?void 0:a[m.camelCase("default-"+b)]?b.toLowerCase():null}}),rc&&qc||(m.attrHooks.value={set:function(a,b,c){return m.nodeName(a,"input")?void(a.defaultValue=b):mc&&mc.set(a,b,c)}}),qc||(mc={set:function(a,b,c){var d=a.getAttributeNode(c);return d||a.setAttributeNode(d=a.ownerDocument.createAttribute(c)),d.value=b+="","value"===c||b===a.getAttribute(c)?b:void 0}},oc.id=oc.name=oc.coords=function(a,b,c){var d;return c?void 0:(d=a.getAttributeNode(b))&&""!==d.value?d.value:null},m.valHooks.button={get:function(a,b){var c=a.getAttributeNode(b);return c&&c.specified?c.value:void 0},set:mc.set},m.attrHooks.contenteditable={set:function(a,b,c){mc.set(a,""===b?!1:b,c)}},m.each(["width","height"],function(a,b){m.attrHooks[b]={set:function(a,c){return""===c?(a.setAttribute(b,"auto"),c):void 0}}})),k.style||(m.attrHooks.style={get:function(a){return a.style.cssText||void 0},set:function(a,b){return a.style.cssText=b+""}});var sc=/^(?:input|select|textarea|button|object)$/i,tc=/^(?:a|area)$/i;m.fn.extend({prop:function(a,b){return V(this,m.prop,a,b,arguments.length>1)},removeProp:function(a){return a=m.propFix[a]||a,this.each(function(){try{this[a]=void 0,delete this[a]}catch(b){}})}}),m.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!m.isXMLDoc(a),f&&(b=m.propFix[b]||b,e=m.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=m.find.attr(a,"tabindex");return b?parseInt(b,10):sc.test(a.nodeName)||tc.test(a.nodeName)&&a.href?0:-1}}}}),k.hrefNormalized||m.each(["href","src"],function(a,b){m.propHooks[b]={get:function(a){return a.getAttribute(b,4)}}}),k.optSelected||(m.propHooks.selected={get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null}}),m.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){m.propFix[this.toLowerCase()]=this}),k.enctype||(m.propFix.enctype="encoding");var uc=/[\t\r\n\f]/g;m.fn.extend({addClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j="string"==typeof a&&a;if(m.isFunction(a))return this.each(function(b){m(this).addClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(E)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(uc," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=m.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j=0===arguments.length||"string"==typeof a&&a;if(m.isFunction(a))return this.each(function(b){m(this).removeClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(E)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(uc," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?m.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(m.isFunction(a)?function(c){m(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=m(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===K||"boolean"===c)&&(this.className&&m._data(this,"__className__",this.className),this.className=this.className||a===!1?"":m._data(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(uc," ").indexOf(b)>=0)return!0;return!1}}),m.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){m.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),m.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var vc=m.now(),wc=/\?/,xc=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;m.parseJSON=function(b){if(a.JSON&&a.JSON.parse)return a.JSON.parse(b+"");var c,d=null,e=m.trim(b+"");return e&&!m.trim(e.replace(xc,function(a,b,e,f){return c&&b&&(d=0),0===d?a:(c=e||b,d+=!f-!e,"")}))?Function("return "+e)():m.error("Invalid JSON: "+b)},m.parseXML=function(b){var c,d;if(!b||"string"!=typeof b)return null;try{a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b))}catch(e){c=void 0}return c&&c.documentElement&&!c.getElementsByTagName("parsererror").length||m.error("Invalid XML: "+b),c};var yc,zc,Ac=/#.*$/,Bc=/([?&])_=[^&]*/,Cc=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Dc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Ec=/^(?:GET|HEAD)$/,Fc=/^\/\//,Gc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Hc={},Ic={},Jc="*/".concat("*");try{zc=location.href}catch(Kc){zc=y.createElement("a"),zc.href="",zc=zc.href}yc=Gc.exec(zc.toLowerCase())||[];function Lc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(m.isFunction(c))while(d=f[e++])"+"===d.charAt(0)?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Mc(a,b,c,d){var e={},f=a===Ic;function g(h){var i;return e[h]=!0,m.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Nc(a,b){var c,d,e=m.ajaxSettings.flatOptions||{};for(d in b)void 0!==b[d]&&((e[d]?a:c||(c={}))[d]=b[d]);return c&&m.extend(!0,a,c),a}function Oc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===e&&(e=a.mimeType||b.getResponseHeader("Content-Type"));if(e)for(g in h)if(h[g]&&h[g].test(e)){i.unshift(g);break}if(i[0]in c)f=i[0];else{for(g in c){if(!i[0]||a.converters[g+" "+i[0]]){f=g;break}d||(d=g)}f=f||d}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function Pc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}m.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:zc,type:"GET",isLocal:Dc.test(yc[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Jc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":m.parseJSON,"text xml":m.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Nc(Nc(a,m.ajaxSettings),b):Nc(m.ajaxSettings,a)},ajaxPrefilter:Lc(Hc),ajaxTransport:Lc(Ic),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=m.ajaxSetup({},b),l=k.context||k,n=k.context&&(l.nodeType||l.jquery)?m(l):m.event,o=m.Deferred(),p=m.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!j){j={};while(b=Cc.exec(f))j[b[1].toLowerCase()]=b[2]}b=j[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?f:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return i&&i.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||zc)+"").replace(Ac,"").replace(Fc,yc[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=m.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(c=Gc.exec(k.url.toLowerCase()),k.crossDomain=!(!c||c[1]===yc[1]&&c[2]===yc[2]&&(c[3]||("http:"===c[1]?"80":"443"))===(yc[3]||("http:"===yc[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=m.param(k.data,k.traditional)),Mc(Hc,k,b,v),2===t)return v;h=k.global,h&&0===m.active++&&m.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!Ec.test(k.type),e=k.url,k.hasContent||(k.data&&(e=k.url+=(wc.test(e)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=Bc.test(e)?e.replace(Bc,"$1_="+vc++):e+(wc.test(e)?"&":"?")+"_="+vc++)),k.ifModified&&(m.lastModified[e]&&v.setRequestHeader("If-Modified-Since",m.lastModified[e]),m.etag[e]&&v.setRequestHeader("If-None-Match",m.etag[e])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+Jc+"; q=0.01":""):k.accepts["*"]);for(d in k.headers)v.setRequestHeader(d,k.headers[d]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(d in{success:1,error:1,complete:1})v[d](k[d]);if(i=Mc(Ic,k,b,v)){v.readyState=1,h&&n.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,i.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,c,d){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),i=void 0,f=d||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,c&&(u=Oc(k,v,c)),u=Pc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(m.lastModified[e]=w),w=v.getResponseHeader("etag"),w&&(m.etag[e]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,h&&n.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),h&&(n.trigger("ajaxComplete",[v,k]),--m.active||m.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return m.get(a,b,c,"json")},getScript:function(a,b){return m.get(a,void 0,b,"script")}}),m.each(["get","post"],function(a,b){m[b]=function(a,c,d,e){return m.isFunction(c)&&(e=e||d,d=c,c=void 0),m.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),m.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){m.fn[b]=function(a){return this.on(b,a)}}),m._evalUrl=function(a){return m.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},m.fn.extend({wrapAll:function(a){if(m.isFunction(a))return this.each(function(b){m(this).wrapAll(a.call(this,b))});if(this[0]){var b=m(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&1===a.firstChild.nodeType)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return this.each(m.isFunction(a)?function(b){m(this).wrapInner(a.call(this,b))}:function(){var b=m(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=m.isFunction(a);return this.each(function(c){m(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){m.nodeName(this,"body")||m(this).replaceWith(this.childNodes)}).end()}}),m.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0||!k.reliableHiddenOffsets()&&"none"===(a.style&&a.style.display||m.css(a,"display"))},m.expr.filters.visible=function(a){return!m.expr.filters.hidden(a)};var Qc=/%20/g,Rc=/\[\]$/,Sc=/\r?\n/g,Tc=/^(?:submit|button|image|reset|file)$/i,Uc=/^(?:input|select|textarea|keygen)/i;function Vc(a,b,c,d){var e;if(m.isArray(b))m.each(b,function(b,e){c||Rc.test(a)?d(a,e):Vc(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==m.type(b))d(a,b);else for(e in b)Vc(a+"["+e+"]",b[e],c,d)}m.param=function(a,b){var c,d=[],e=function(a,b){b=m.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=m.ajaxSettings&&m.ajaxSettings.traditional),m.isArray(a)||a.jquery&&!m.isPlainObject(a))m.each(a,function(){e(this.name,this.value)});else for(c in a)Vc(c,a[c],b,e);return d.join("&").replace(Qc,"+")},m.fn.extend({serialize:function(){return m.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=m.prop(this,"elements");return a?m.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!m(this).is(":disabled")&&Uc.test(this.nodeName)&&!Tc.test(a)&&(this.checked||!W.test(a))}).map(function(a,b){var c=m(this).val();return null==c?null:m.isArray(c)?m.map(c,function(a){return{name:b.name,value:a.replace(Sc,"\r\n")}}):{name:b.name,value:c.replace(Sc,"\r\n")}}).get()}}),m.ajaxSettings.xhr=void 0!==a.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&Zc()||$c()}:Zc;var Wc=0,Xc={},Yc=m.ajaxSettings.xhr();a.ActiveXObject&&m(a).on("unload",function(){for(var a in Xc)Xc[a](void 0,!0)}),k.cors=!!Yc&&"withCredentials"in Yc,Yc=k.ajax=!!Yc,Yc&&m.ajaxTransport(function(a){if(!a.crossDomain||k.cors){var b;return{send:function(c,d){var e,f=a.xhr(),g=++Wc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)void 0!==c[e]&&f.setRequestHeader(e,c[e]+"");f.send(a.hasContent&&a.data||null),b=function(c,e){var h,i,j;if(b&&(e||4===f.readyState))if(delete Xc[g],b=void 0,f.onreadystatechange=m.noop,e)4!==f.readyState&&f.abort();else{j={},h=f.status,"string"==typeof f.responseText&&(j.text=f.responseText);try{i=f.statusText}catch(k){i=""}h||!a.isLocal||a.crossDomain?1223===h&&(h=204):h=j.text?200:404}j&&d(h,i,j,f.getAllResponseHeaders())},a.async?4===f.readyState?setTimeout(b):f.onreadystatechange=Xc[g]=b:b()},abort:function(){b&&b(void 0,!0)}}}});function Zc(){try{return new a.XMLHttpRequest}catch(b){}}function $c(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}m.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return m.globalEval(a),a}}}),m.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),m.ajaxTransport("script",function(a){if(a.crossDomain){var b,c=y.head||m("head")[0]||y.documentElement;return{send:function(d,e){b=y.createElement("script"),b.async=!0,a.scriptCharset&&(b.charset=a.scriptCharset),b.src=a.url,b.onload=b.onreadystatechange=function(a,c){(c||!b.readyState||/loaded|complete/.test(b.readyState))&&(b.onload=b.onreadystatechange=null,b.parentNode&&b.parentNode.removeChild(b),b=null,c||e(200,"success"))},c.insertBefore(b,c.firstChild)},abort:function(){b&&b.onload(void 0,!0)}}}});var _c=[],ad=/(=)\?(?=&|$)|\?\?/;m.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=_c.pop()||m.expando+"_"+vc++;return this[a]=!0,a}}),m.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(ad.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&ad.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=m.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(ad,"$1"+e):b.jsonp!==!1&&(b.url+=(wc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||m.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,_c.push(e)),g&&m.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),m.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||y;var d=u.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=m.buildFragment([a],b,e),e&&e.length&&m(e).remove(),m.merge([],d.childNodes))};var bd=m.fn.load;m.fn.load=function(a,b,c){if("string"!=typeof a&&bd)return bd.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=m.trim(a.slice(h,a.length)),a=a.slice(0,h)),m.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(f="POST"),g.length>0&&m.ajax({url:a,type:f,dataType:"html",data:b}).done(function(a){e=arguments,g.html(d?m("<div>").append(m.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,e||[a.responseText,b,a])}),this},m.expr.filters.animated=function(a){return m.grep(m.timers,function(b){return a===b.elem}).length};var cd=a.document.documentElement;function dd(a){return m.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}m.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=m.css(a,"position"),l=m(a),n={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=m.css(a,"top"),i=m.css(a,"left"),j=("absolute"===k||"fixed"===k)&&m.inArray("auto",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),m.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(n.top=b.top-h.top+g),null!=b.left&&(n.left=b.left-h.left+e),"using"in b?b.using.call(a,n):l.css(n)}},m.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){m.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,m.contains(b,e)?(typeof e.getBoundingClientRect!==K&&(d=e.getBoundingClientRect()),c=dd(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return"fixed"===m.css(d,"position")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),m.nodeName(a[0],"html")||(c=a.offset()),c.top+=m.css(a[0],"borderTopWidth",!0),c.left+=m.css(a[0],"borderLeftWidth",!0)),{top:b.top-c.top-m.css(d,"marginTop",!0),left:b.left-c.left-m.css(d,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||cd;while(a&&!m.nodeName(a,"html")&&"static"===m.css(a,"position"))a=a.offsetParent;return a||cd})}}),m.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c=/Y/.test(b);m.fn[a]=function(d){return V(this,function(a,d,e){var f=dd(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?m(f).scrollLeft():e,c?e:m(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),m.each(["top","left"],function(a,b){m.cssHooks[b]=Lb(k.pixelPosition,function(a,c){return c?(c=Jb(a,b),Hb.test(c)?m(a).position()[b]+"px":c):void 0})}),m.each({Height:"height",Width:"width"},function(a,b){m.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){m.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return V(this,function(b,c,d){var e;return m.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?m.css(b,c,g):m.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),m.fn.size=function(){return this.length},m.fn.andSelf=m.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return m});var ed=a.jQuery,fd=a.$;return m.noConflict=function(b){return a.$===m&&(a.$=fd),b&&a.jQuery===m&&(a.jQuery=ed),m},typeof b===K&&(a.jQuery=a.$=m),m}); diff --git a/devtools/client/inspector/markup/test/lib_jquery_1.2_min.js b/devtools/client/inspector/markup/test/lib_jquery_1.2_min.js new file mode 100644 index 000000000..f10d4943f --- /dev/null +++ b/devtools/client/inspector/markup/test/lib_jquery_1.2_min.js @@ -0,0 +1,32 @@ +/* + * jQuery 1.2 - New Wave Javascript + * + * Copyright (c) 2007 John Resig (jquery.com) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * $Date: 2007-09-10 15:45:49 -0400 (Mon, 10 Sep 2007) $ + * $Rev: 3219 $ + */ +(function(){if(typeof jQuery!="undefined")var _jQuery=jQuery;var jQuery=window.jQuery=function(a,c){if(window==this||!this.init)return new jQuery(a,c);return this.init(a,c);};if(typeof $!="undefined")var _$=$;window.$=jQuery;var quickExpr=/^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/;jQuery.fn=jQuery.prototype={init:function(a,c){a=a||document;if(typeof a=="string"){var m=quickExpr.exec(a);if(m&&(m[1]||!c)){if(m[1])a=jQuery.clean([m[1]],c);else{var tmp=document.getElementById(m[3]);if(tmp)if(tmp.id!=m[3])return jQuery().find(a);else{this[0]=tmp;this.length=1;return this;}else +a=[];}}else +return new jQuery(c).find(a);}else if(jQuery.isFunction(a))return new jQuery(document)[jQuery.fn.ready?"ready":"load"](a);return this.setArray(a.constructor==Array&&a||(a.jquery||a.length&&a!=window&&!a.nodeType&&a[0]!=undefined&&a[0].nodeType)&&jQuery.makeArray(a)||[a]);},jquery:"1.2",size:function(){return this.length;},length:0,get:function(num){return num==undefined?jQuery.makeArray(this):this[num];},pushStack:function(a){var ret=jQuery(a);ret.prevObject=this;return ret;},setArray:function(a){this.length=0;Array.prototype.push.apply(this,a);return this;},each:function(fn,args){return jQuery.each(this,fn,args);},index:function(obj){var pos=-1;this.each(function(i){if(this==obj)pos=i;});return pos;},attr:function(key,value,type){var obj=key;if(key.constructor==String)if(value==undefined)return this.length&&jQuery[type||"attr"](this[0],key)||undefined;else{obj={};obj[key]=value;}return this.each(function(index){for(var prop in obj)jQuery.attr(type?this.style:this,prop,jQuery.prop(this,obj[prop],type,index,prop));});},css:function(key,value){return this.attr(key,value,"curCSS");},text:function(e){if(typeof e!="object"&&e!=null)return this.empty().append(document.createTextNode(e));var t="";jQuery.each(e||this,function(){jQuery.each(this.childNodes,function(){if(this.nodeType!=8)t+=this.nodeType!=1?this.nodeValue:jQuery.fn.text([this]);});});return t;},wrapAll:function(html){if(this[0])jQuery(html,this[0].ownerDocument).clone().insertBefore(this[0]).map(function(){var elem=this;while(elem.firstChild)elem=elem.firstChild;return elem;}).append(this);return this;},wrapInner:function(html){return this.each(function(){jQuery(this).contents().wrapAll(html);});},wrap:function(html){return this.each(function(){jQuery(this).wrapAll(html);});},append:function(){return this.domManip(arguments,true,1,function(a){this.appendChild(a);});},prepend:function(){return this.domManip(arguments,true,-1,function(a){this.insertBefore(a,this.firstChild);});},before:function(){return this.domManip(arguments,false,1,function(a){this.parentNode.insertBefore(a,this);});},after:function(){return this.domManip(arguments,false,-1,function(a){this.parentNode.insertBefore(a,this.nextSibling);});},end:function(){return this.prevObject||jQuery([]);},find:function(t){var data=jQuery.map(this,function(a){return jQuery.find(t,a);});return this.pushStack(/[^+>] [^+>]/.test(t)||t.indexOf("..")>-1?jQuery.unique(data):data);},clone:function(events){var ret=this.map(function(){return this.outerHTML?jQuery(this.outerHTML)[0]:this.cloneNode(true);});if(events===true){var clone=ret.find("*").andSelf();this.find("*").andSelf().each(function(i){var events=jQuery.data(this,"events");for(var type in events)for(var handler in events[type])jQuery.event.add(clone[i],type,events[type][handler],events[type][handler].data);});}return ret;},filter:function(t){return this.pushStack(jQuery.isFunction(t)&&jQuery.grep(this,function(el,index){return t.apply(el,[index]);})||jQuery.multiFilter(t,this));},not:function(t){return this.pushStack(t.constructor==String&&jQuery.multiFilter(t,this,true)||jQuery.grep(this,function(a){return(t.constructor==Array||t.jquery)?jQuery.inArray(a,t)<0:a!=t;}));},add:function(t){return this.pushStack(jQuery.merge(this.get(),t.constructor==String?jQuery(t).get():t.length!=undefined&&(!t.nodeName||t.nodeName=="FORM")?t:[t]));},is:function(expr){return expr?jQuery.multiFilter(expr,this).length>0:false;},hasClass:function(expr){return this.is("."+expr);},val:function(val){if(val==undefined){if(this.length){var elem=this[0];if(jQuery.nodeName(elem,"select")){var index=elem.selectedIndex,a=[],options=elem.options,one=elem.type=="select-one";if(index<0)return null;for(var i=one?index:0,max=one?index+1:options.length;i<max;i++){var option=options[i];if(option.selected){var val=jQuery.browser.msie&&!option.attributes["value"].specified?option.text:option.value;if(one)return val;a.push(val);}}return a;}else +return this[0].value.replace(/\r/g,"");}}else +return this.each(function(){if(val.constructor==Array&&/radio|checkbox/.test(this.type))this.checked=(jQuery.inArray(this.value,val)>=0||jQuery.inArray(this.name,val)>=0);else if(jQuery.nodeName(this,"select")){var tmp=val.constructor==Array?val:[val];jQuery("option",this).each(function(){this.selected=(jQuery.inArray(this.value,tmp)>=0||jQuery.inArray(this.text,tmp)>=0);});if(!tmp.length)this.selectedIndex=-1;}else +this.value=val;});},html:function(val){return val==undefined?(this.length?this[0].innerHTML:null):this.empty().append(val);},replaceWith:function(val){return this.after(val).remove();},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments));},map:function(fn){return this.pushStack(jQuery.map(this,function(elem,i){return fn.call(elem,i,elem);}));},andSelf:function(){return this.add(this.prevObject);},domManip:function(args,table,dir,fn){var clone=this.length>1,a;return this.each(function(){if(!a){a=jQuery.clean(args,this.ownerDocument);if(dir<0)a.reverse();}var obj=this;if(table&&jQuery.nodeName(this,"table")&&jQuery.nodeName(a[0],"tr"))obj=this.getElementsByTagName("tbody")[0]||this.appendChild(document.createElement("tbody"));jQuery.each(a,function(){if(jQuery.nodeName(this,"script")){if(this.src)jQuery.ajax({url:this.src,async:false,dataType:"script"});else +jQuery.globalEval(this.text||this.textContent||this.innerHTML||"");}else +fn.apply(obj,[clone?this.cloneNode(true):this]);});});}};jQuery.extend=jQuery.fn.extend=function(){var target=arguments[0]||{},a=1,al=arguments.length,deep=false;if(target.constructor==Boolean){deep=target;target=arguments[1]||{};}if(al==1){target=this;a=0;}var prop;for(;a<al;a++)if((prop=arguments[a])!=null)for(var i in prop){if(target==prop[i])continue;if(deep&&typeof prop[i]=='object'&&target[i])jQuery.extend(target[i],prop[i]);else if(prop[i]!=undefined)target[i]=prop[i];}return target;};var expando="jQuery"+(new Date()).getTime(),uuid=0,win={};jQuery.extend({noConflict:function(deep){window.$=_$;if(deep)window.jQuery=_jQuery;return jQuery;},isFunction:function(fn){return!!fn&&typeof fn!="string"&&!fn.nodeName&&fn.constructor!=Array&&/function/i.test(fn+"");},isXMLDoc:function(elem){return elem.documentElement&&!elem.body||elem.tagName&&elem.ownerDocument&&!elem.ownerDocument.body;},globalEval:function(data){data=jQuery.trim(data);if(data){if(window.execScript)window.execScript(data);else if(jQuery.browser.safari)window.setTimeout(data,0);else +eval.call(window,data);}},nodeName:function(elem,name){return elem.nodeName&&elem.nodeName.toUpperCase()==name.toUpperCase();},cache:{},data:function(elem,name,data){elem=elem==window?win:elem;var id=elem[expando];if(!id)id=elem[expando]=++uuid;if(name&&!jQuery.cache[id])jQuery.cache[id]={};if(data!=undefined)jQuery.cache[id][name]=data;return name?jQuery.cache[id][name]:id;},removeData:function(elem,name){elem=elem==window?win:elem;var id=elem[expando];if(name){if(jQuery.cache[id]){delete jQuery.cache[id][name];name="";for(name in jQuery.cache[id])break;if(!name)jQuery.removeData(elem);}}else{try{delete elem[expando];}catch(e){if(elem.removeAttribute)elem.removeAttribute(expando);}delete jQuery.cache[id];}},each:function(obj,fn,args){if(args){if(obj.length==undefined)for(var i in obj)fn.apply(obj[i],args);else +for(var i=0,ol=obj.length;i<ol;i++)if(fn.apply(obj[i],args)===false)break;}else{if(obj.length==undefined)for(var i in obj)fn.call(obj[i],i,obj[i]);else +for(var i=0,ol=obj.length,val=obj[0];i<ol&&fn.call(val,i,val)!==false;val=obj[++i]){}}return obj;},prop:function(elem,value,type,index,prop){if(jQuery.isFunction(value))value=value.call(elem,[index]);var exclude=/z-?index|font-?weight|opacity|zoom|line-?height/i;return value&&value.constructor==Number&&type=="curCSS"&&!exclude.test(prop)?value+"px":value;},className:{add:function(elem,c){jQuery.each((c||"").split(/\s+/),function(i,cur){if(!jQuery.className.has(elem.className,cur))elem.className+=(elem.className?" ":"")+cur;});},remove:function(elem,c){elem.className=c!=undefined?jQuery.grep(elem.className.split(/\s+/),function(cur){return!jQuery.className.has(c,cur);}).join(" "):"";},has:function(t,c){return jQuery.inArray(c,(t.className||t).toString().split(/\s+/))>-1;}},swap:function(e,o,f){for(var i in o){e.style["old"+i]=e.style[i];e.style[i]=o[i];}f.apply(e,[]);for(var i in o)e.style[i]=e.style["old"+i];},css:function(e,p){if(p=="height"||p=="width"){var old={},oHeight,oWidth,d=["Top","Bottom","Right","Left"];jQuery.each(d,function(){old["padding"+this]=0;old["border"+this+"Width"]=0;});jQuery.swap(e,old,function(){if(jQuery(e).is(':visible')){oHeight=e.offsetHeight;oWidth=e.offsetWidth;}else{e=jQuery(e.cloneNode(true)).find(":radio").removeAttr("checked").end().css({visibility:"hidden",position:"absolute",display:"block",right:"0",left:"0"}).appendTo(e.parentNode)[0];var parPos=jQuery.css(e.parentNode,"position")||"static";if(parPos=="static")e.parentNode.style.position="relative";oHeight=e.clientHeight;oWidth=e.clientWidth;if(parPos=="static")e.parentNode.style.position="static";e.parentNode.removeChild(e);}});return p=="height"?oHeight:oWidth;}return jQuery.curCSS(e,p);},curCSS:function(elem,prop,force){var ret,stack=[],swap=[];function color(a){if(!jQuery.browser.safari)return false;var ret=document.defaultView.getComputedStyle(a,null);return!ret||ret.getPropertyValue("color")=="";}if(prop=="opacity"&&jQuery.browser.msie){ret=jQuery.attr(elem.style,"opacity");return ret==""?"1":ret;}if(prop.match(/float/i))prop=styleFloat;if(!force&&elem.style[prop])ret=elem.style[prop];else if(document.defaultView&&document.defaultView.getComputedStyle){if(prop.match(/float/i))prop="float";prop=prop.replace(/([A-Z])/g,"-$1").toLowerCase();var cur=document.defaultView.getComputedStyle(elem,null);if(cur&&!color(elem))ret=cur.getPropertyValue(prop);else{for(var a=elem;a&&color(a);a=a.parentNode)stack.unshift(a);for(a=0;a<stack.length;a++)if(color(stack[a])){swap[a]=stack[a].style.display;stack[a].style.display="block";}ret=prop=="display"&&swap[stack.length-1]!=null?"none":document.defaultView.getComputedStyle(elem,null).getPropertyValue(prop)||"";for(a=0;a<swap.length;a++)if(swap[a]!=null)stack[a].style.display=swap[a];}if(prop=="opacity"&&ret=="")ret="1";}else if(elem.currentStyle){var newProp=prop.replace(/\-(\w)/g,function(m,c){return c.toUpperCase();});ret=elem.currentStyle[prop]||elem.currentStyle[newProp];if(!/^\d+(px)?$/i.test(ret)&&/^\d/.test(ret)){var style=elem.style.left;var runtimeStyle=elem.runtimeStyle.left;elem.runtimeStyle.left=elem.currentStyle.left;elem.style.left=ret||0;ret=elem.style.pixelLeft+"px";elem.style.left=style;elem.runtimeStyle.left=runtimeStyle;}}return ret;},clean:function(a,doc){var r=[];doc=doc||document;jQuery.each(a,function(i,arg){if(!arg)return;if(arg.constructor==Number)arg=arg.toString();if(typeof arg=="string"){arg=arg.replace(/(<(\w+)[^>]*?)\/>/g,function(m,all,tag){return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area)$/i)?m:all+"></"+tag+">";});var s=jQuery.trim(arg).toLowerCase(),div=doc.createElement("div"),tb=[];var wrap=!s.indexOf("<opt")&&[1,"<select>","</select>"]||!s.indexOf("<leg")&&[1,"<fieldset>","</fieldset>"]||s.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"<table>","</table>"]||!s.indexOf("<tr")&&[2,"<table><tbody>","</tbody></table>"]||(!s.indexOf("<td")||!s.indexOf("<th"))&&[3,"<table><tbody><tr>","</tr></tbody></table>"]||!s.indexOf("<col")&&[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]||jQuery.browser.msie&&[1,"div<div>","</div>"]||[0,"",""];div.innerHTML=wrap[1]+arg+wrap[2];while(wrap[0]--)div=div.lastChild;if(jQuery.browser.msie){if(!s.indexOf("<table")&&s.indexOf("<tbody")<0)tb=div.firstChild&&div.firstChild.childNodes;else if(wrap[1]=="<table>"&&s.indexOf("<tbody")<0)tb=div.childNodes;for(var n=tb.length-1;n>=0;--n)if(jQuery.nodeName(tb[n],"tbody")&&!tb[n].childNodes.length)tb[n].parentNode.removeChild(tb[n]);if(/^\s/.test(arg))div.insertBefore(doc.createTextNode(arg.match(/^\s*/)[0]),div.firstChild);}arg=jQuery.makeArray(div.childNodes);}if(0===arg.length&&(!jQuery.nodeName(arg,"form")&&!jQuery.nodeName(arg,"select")))return;if(arg[0]==undefined||jQuery.nodeName(arg,"form")||arg.options)r.push(arg);else +r=jQuery.merge(r,arg);});return r;},attr:function(elem,name,value){var fix=jQuery.isXMLDoc(elem)?{}:jQuery.props;if(name=="selected"&&jQuery.browser.safari)elem.parentNode.selectedIndex;if(fix[name]){if(value!=undefined)elem[fix[name]]=value;return elem[fix[name]];}else if(jQuery.browser.msie&&name=="style")return jQuery.attr(elem.style,"cssText",value);else if(value==undefined&&jQuery.browser.msie&&jQuery.nodeName(elem,"form")&&(name=="action"||name=="method"))return elem.getAttributeNode(name).nodeValue;else if(elem.tagName){if(value!=undefined){if(name=="type"&&jQuery.nodeName(elem,"input")&&elem.parentNode)throw"type property can't be changed";elem.setAttribute(name,value);}if(jQuery.browser.msie&&/href|src/.test(name)&&!jQuery.isXMLDoc(elem))return elem.getAttribute(name,2);return elem.getAttribute(name);}else{if(name=="opacity"&&jQuery.browser.msie){if(value!=undefined){elem.zoom=1;elem.filter=(elem.filter||"").replace(/alpha\([^)]*\)/,"")+(parseFloat(value).toString()=="NaN"?"":"alpha(opacity="+value*100+")");}return elem.filter?(parseFloat(elem.filter.match(/opacity=([^)]*)/)[1])/100).toString():"";}name=name.replace(/-([a-z])/ig,function(z,b){return b.toUpperCase();});if(value!=undefined)elem[name]=value;return elem[name];}},trim:function(t){return(t||"").replace(/^\s+|\s+$/g,"");},makeArray:function(a){var r=[];if(typeof a!="array")for(var i=0,al=a.length;i<al;i++)r.push(a[i]);else +r=a.slice(0);return r;},inArray:function(b,a){for(var i=0,al=a.length;i<al;i++)if(a[i]==b)return i;return-1;},merge:function(first,second){if(jQuery.browser.msie){for(var i=0;second[i];i++)if(second[i].nodeType!=8)first.push(second[i]);}else +for(var i=0;second[i];i++)first.push(second[i]);return first;},unique:function(first){var r=[],done={};try{for(var i=0,fl=first.length;i<fl;i++){var id=jQuery.data(first[i]);if(!done[id]){done[id]=true;r.push(first[i]);}}}catch(e){r=first;}return r;},grep:function(elems,fn,inv){if(typeof fn=="string")fn=eval("false||function(a,i){return "+fn+"}");var result=[];for(var i=0,el=elems.length;i<el;i++)if(!inv&&fn(elems[i],i)||inv&&!fn(elems[i],i))result.push(elems[i]);return result;},map:function(elems,fn){if(typeof fn=="string")fn=eval("false||function(a){return "+fn+"}");var result=[];for(var i=0,el=elems.length;i<el;i++){var val=fn(elems[i],i);if(val!==null&&val!=undefined){if(val.constructor!=Array)val=[val];result=result.concat(val);}}return result;}});var userAgent=navigator.userAgent.toLowerCase();jQuery.browser={version:(userAgent.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[])[1],safari:/webkit/.test(userAgent),opera:/opera/.test(userAgent),msie:/msie/.test(userAgent)&&!/opera/.test(userAgent),mozilla:/mozilla/.test(userAgent)&&!/(compatible|webkit)/.test(userAgent)};var styleFloat=jQuery.browser.msie?"styleFloat":"cssFloat";jQuery.extend({boxModel:!jQuery.browser.msie||document.compatMode=="CSS1Compat",styleFloat:jQuery.browser.msie?"styleFloat":"cssFloat",props:{"for":"htmlFor","class":"className","float":styleFloat,cssFloat:styleFloat,styleFloat:styleFloat,innerHTML:"innerHTML",className:"className",value:"value",disabled:"disabled",checked:"checked",readonly:"readOnly",selected:"selected",maxlength:"maxLength"}});jQuery.each({parent:"a.parentNode",parents:"jQuery.dir(a,'parentNode')",next:"jQuery.nth(a,2,'nextSibling')",prev:"jQuery.nth(a,2,'previousSibling')",nextAll:"jQuery.dir(a,'nextSibling')",prevAll:"jQuery.dir(a,'previousSibling')",siblings:"jQuery.sibling(a.parentNode.firstChild,a)",children:"jQuery.sibling(a.firstChild)",contents:"jQuery.nodeName(a,'iframe')?a.contentDocument||a.contentWindow.document:jQuery.makeArray(a.childNodes)"},function(i,n){jQuery.fn[i]=function(a){var ret=jQuery.map(this,n);if(a&&typeof a=="string")ret=jQuery.multiFilter(a,ret);return this.pushStack(jQuery.unique(ret));};});jQuery.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(i,n){jQuery.fn[i]=function(){var a=arguments;return this.each(function(){for(var j=0,al=a.length;j<al;j++)jQuery(a[j])[n](this);});};});jQuery.each({removeAttr:function(key){jQuery.attr(this,key,"");this.removeAttribute(key);},addClass:function(c){jQuery.className.add(this,c);},removeClass:function(c){jQuery.className.remove(this,c);},toggleClass:function(c){jQuery.className[jQuery.className.has(this,c)?"remove":"add"](this,c);},remove:function(a){if(!a||jQuery.filter(a,[this]).r.length){jQuery.removeData(this);this.parentNode.removeChild(this);}},empty:function(){jQuery("*",this).each(function(){jQuery.removeData(this);});while(this.firstChild)this.removeChild(this.firstChild);}},function(i,n){jQuery.fn[i]=function(){return this.each(n,arguments);};});jQuery.each(["Height","Width"],function(i,name){var n=name.toLowerCase();jQuery.fn[n]=function(h){return this[0]==window?jQuery.browser.safari&&self["inner"+name]||jQuery.boxModel&&Math.max(document.documentElement["client"+name],document.body["client"+name])||document.body["client"+name]:this[0]==document?Math.max(document.body["scroll"+name],document.body["offset"+name]):h==undefined?(this.length?jQuery.css(this[0],n):null):this.css(n,h.constructor==String?h:h+"px");};});var chars=jQuery.browser.safari&&parseInt(jQuery.browser.version)<417?"(?:[\\w*_-]|\\\\.)":"(?:[\\w\u0128-\uFFFF*_-]|\\\\.)",quickChild=new RegExp("^>\\s*("+chars+"+)"),quickID=new RegExp("^("+chars+"+)(#)("+chars+"+)"),quickClass=new RegExp("^([#.]?)("+chars+"*)");jQuery.extend({expr:{"":"m[2]=='*'||jQuery.nodeName(a,m[2])","#":"a.getAttribute('id')==m[2]",":":{lt:"i<m[3]-0",gt:"i>m[3]-0",nth:"m[3]-0==i",eq:"m[3]-0==i",first:"i==0",last:"i==r.length-1",even:"i%2==0",odd:"i%2","first-child":"a.parentNode.getElementsByTagName('*')[0]==a","last-child":"jQuery.nth(a.parentNode.lastChild,1,'previousSibling')==a","only-child":"!jQuery.nth(a.parentNode.lastChild,2,'previousSibling')",parent:"a.firstChild",empty:"!a.firstChild",contains:"(a.textContent||a.innerText||'').indexOf(m[3])>=0",visible:'"hidden"!=a.type&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden"',hidden:'"hidden"==a.type||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden"',enabled:"!a.disabled",disabled:"a.disabled",checked:"a.checked",selected:"a.selected||jQuery.attr(a,'selected')",text:"'text'==a.type",radio:"'radio'==a.type",checkbox:"'checkbox'==a.type",file:"'file'==a.type",password:"'password'==a.type",submit:"'submit'==a.type",image:"'image'==a.type",reset:"'reset'==a.type",button:'"button"==a.type||jQuery.nodeName(a,"button")',input:"/input|select|textarea|button/i.test(a.nodeName)",has:"jQuery.find(m[3],a).length",header:"/h\\d/i.test(a.nodeName)",animated:"jQuery.grep(jQuery.timers,function(fn){return a==fn.elem;}).length"}},parse:[/^(\[) *@?([\w-]+) *([!*$^~=]*) *('?"?)(.*?)\4 *\]/,/^(:)([\w-]+)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/,new RegExp("^([:.#]*)("+chars+"+)")],multiFilter:function(expr,elems,not){var old,cur=[];while(expr&&expr!=old){old=expr;var f=jQuery.filter(expr,elems,not);expr=f.t.replace(/^\s*,\s*/,"");cur=not?elems=f.r:jQuery.merge(cur,f.r);}return cur;},find:function(t,context){if(typeof t!="string")return[t];if(context&&!context.nodeType)context=null;context=context||document;var ret=[context],done=[],last;while(t&&last!=t){var r=[];last=t;t=jQuery.trim(t);var foundToken=false;var re=quickChild;var m=re.exec(t);if(m){var nodeName=m[1].toUpperCase();for(var i=0;ret[i];i++)for(var c=ret[i].firstChild;c;c=c.nextSibling)if(c.nodeType==1&&(nodeName=="*"||c.nodeName.toUpperCase()==nodeName.toUpperCase()))r.push(c);ret=r;t=t.replace(re,"");if(t.indexOf(" ")==0)continue;foundToken=true;}else{re=/^([>+~])\s*(\w*)/i;if((m=re.exec(t))!=null){r=[];var nodeName=m[2],merge={};m=m[1];for(var j=0,rl=ret.length;j<rl;j++){var n=m=="~"||m=="+"?ret[j].nextSibling:ret[j].firstChild;for(;n;n=n.nextSibling)if(n.nodeType==1){var id=jQuery.data(n);if(m=="~"&&merge[id])break;if(!nodeName||n.nodeName.toUpperCase()==nodeName.toUpperCase()){if(m=="~")merge[id]=true;r.push(n);}if(m=="+")break;}}ret=r;t=jQuery.trim(t.replace(re,""));foundToken=true;}}if(t&&!foundToken){if(!t.indexOf(",")){if(context==ret[0])ret.shift();done=jQuery.merge(done,ret);r=ret=[context];t=" "+t.substr(1,t.length);}else{var re2=quickID;var m=re2.exec(t);if(m){m=[0,m[2],m[3],m[1]];}else{re2=quickClass;m=re2.exec(t);}m[2]=m[2].replace(/\\/g,"");var elem=ret[ret.length-1];if(m[1]=="#"&&elem&&elem.getElementById&&!jQuery.isXMLDoc(elem)){var oid=elem.getElementById(m[2]);if((jQuery.browser.msie||jQuery.browser.opera)&&oid&&typeof oid.id=="string"&&oid.id!=m[2])oid=jQuery('[@id="'+m[2]+'"]',elem)[0];ret=r=oid&&(!m[3]||jQuery.nodeName(oid,m[3]))?[oid]:[];}else{for(var i=0;ret[i];i++){var tag=m[1]=="#"&&m[3]?m[3]:m[1]!=""||m[0]==""?"*":m[2];if(tag=="*"&&ret[i].nodeName.toLowerCase()=="object")tag="param";r=jQuery.merge(r,ret[i].getElementsByTagName(tag));}if(m[1]==".")r=jQuery.classFilter(r,m[2]);if(m[1]=="#"){var tmp=[];for(var i=0;r[i];i++)if(r[i].getAttribute("id")==m[2]){tmp=[r[i]];break;}r=tmp;}ret=r;}t=t.replace(re2,"");}}if(t){var val=jQuery.filter(t,r);ret=r=val.r;t=jQuery.trim(val.t);}}if(t)ret=[];if(ret&&context==ret[0])ret.shift();done=jQuery.merge(done,ret);return done;},classFilter:function(r,m,not){m=" "+m+" ";var tmp=[];for(var i=0;r[i];i++){var pass=(" "+r[i].className+" ").indexOf(m)>=0;if(!not&&pass||not&&!pass)tmp.push(r[i]);}return tmp;},filter:function(t,r,not){var last;while(t&&t!=last){last=t;var p=jQuery.parse,m;for(var i=0;p[i];i++){m=p[i].exec(t);if(m){t=t.substring(m[0].length);m[2]=m[2].replace(/\\/g,"");break;}}if(!m)break;if(m[1]==":"&&m[2]=="not")r=jQuery.filter(m[3],r,true).r;else if(m[1]==".")r=jQuery.classFilter(r,m[2],not);else if(m[1]=="["){var tmp=[],type=m[3];for(var i=0,rl=r.length;i<rl;i++){var a=r[i],z=a[jQuery.props[m[2]]||m[2]];if(z==null||/href|src|selected/.test(m[2]))z=jQuery.attr(a,m[2])||'';if((type==""&&!!z||type=="="&&z==m[5]||type=="!="&&z!=m[5]||type=="^="&&z&&!z.indexOf(m[5])||type=="$="&&z.substr(z.length-m[5].length)==m[5]||(type=="*="||type=="~=")&&z.indexOf(m[5])>=0)^not)tmp.push(a);}r=tmp;}else if(m[1]==":"&&m[2]=="nth-child"){var merge={},tmp=[],test=/(\d*)n\+?(\d*)/.exec(m[3]=="even"&&"2n"||m[3]=="odd"&&"2n+1"||!/\D/.test(m[3])&&"n+"+m[3]||m[3]),first=(test[1]||1)-0,last=test[2]-0;for(var i=0,rl=r.length;i<rl;i++){var node=r[i],parentNode=node.parentNode,id=jQuery.data(parentNode);if(!merge[id]){var c=1;for(var n=parentNode.firstChild;n;n=n.nextSibling)if(n.nodeType==1)n.nodeIndex=c++;merge[id]=true;}var add=false;if(first==1){if(last==0||node.nodeIndex==last)add=true;}else if((node.nodeIndex+last)%first==0)add=true;if(add^not)tmp.push(node);}r=tmp;}else{var f=jQuery.expr[m[1]];if(typeof f!="string")f=jQuery.expr[m[1]][m[2]];f=eval("false||function(a,i){return "+f+"}");r=jQuery.grep(r,f,not);}}return{r:r,t:t};},dir:function(elem,dir){var matched=[];var cur=elem[dir];while(cur&&cur!=document){if(cur.nodeType==1)matched.push(cur);cur=cur[dir];}return matched;},nth:function(cur,result,dir,elem){result=result||1;var num=0;for(;cur;cur=cur[dir])if(cur.nodeType==1&&++num==result)break;return cur;},sibling:function(n,elem){var r=[];for(;n;n=n.nextSibling){if(n.nodeType==1&&(!elem||n!=elem))r.push(n);}return r;}});jQuery.event={add:function(element,type,handler,data){if(jQuery.browser.msie&&element.setInterval!=undefined)element=window;if(!handler.guid)handler.guid=this.guid++;if(data!=undefined){var fn=handler;handler=function(){return fn.apply(this,arguments);};handler.data=data;handler.guid=fn.guid;}var parts=type.split(".");type=parts[0];handler.type=parts[1];var events=jQuery.data(element,"events")||jQuery.data(element,"events",{});var handle=jQuery.data(element,"handle",function(){var val;if(typeof jQuery=="undefined"||jQuery.event.triggered)return val;val=jQuery.event.handle.apply(element,arguments);return val;});var handlers=events[type];if(!handlers){handlers=events[type]={};if(element.addEventListener)element.addEventListener(type,handle,false);else +element.attachEvent("on"+type,handle);}handlers[handler.guid]=handler;this.global[type]=true;},guid:1,global:{},remove:function(element,type,handler){var events=jQuery.data(element,"events"),ret,index;if(typeof type=="string"){var parts=type.split(".");type=parts[0];}if(events){if(type&&type.type){handler=type.handler;type=type.type;}if(!type){for(type in events)this.remove(element,type);}else if(events[type]){if(handler)delete events[type][handler.guid];else +for(handler in events[type])if(!parts[1]||events[type][handler].type==parts[1])delete events[type][handler];for(ret in events[type])break;if(!ret){if(element.removeEventListener)element.removeEventListener(type,jQuery.data(element,"handle"),false);else +element.detachEvent("on"+type,jQuery.data(element,"handle"));ret=null;delete events[type];}}for(ret in events)break;if(!ret){jQuery.removeData(element,"events");jQuery.removeData(element,"handle");}}},trigger:function(type,data,element,donative,extra){data=jQuery.makeArray(data||[]);if(!element){if(this.global[type])jQuery("*").add([window,document]).trigger(type,data);}else{var val,ret,fn=jQuery.isFunction(element[type]||null),evt=!data[0]||!data[0].preventDefault;if(evt)data.unshift(this.fix({type:type,target:element}));if(jQuery.isFunction(jQuery.data(element,"handle")))val=jQuery.data(element,"handle").apply(element,data);if(!fn&&element["on"+type]&&element["on"+type].apply(element,data)===false)val=false;if(evt)data.shift();if(extra&&extra.apply(element,data)===false)val=false;if(fn&&donative!==false&&val!==false&&!(jQuery.nodeName(element,'a')&&type=="click")){this.triggered=true;element[type]();}this.triggered=false;}return val;},handle:function(event){var val;event=jQuery.event.fix(event||window.event||{});var parts=event.type.split(".");event.type=parts[0];var c=jQuery.data(this,"events")&&jQuery.data(this,"events")[event.type],args=Array.prototype.slice.call(arguments,1);args.unshift(event);for(var j in c){args[0].handler=c[j];args[0].data=c[j].data;if(!parts[1]||c[j].type==parts[1]){var tmp=c[j].apply(this,args);if(val!==false)val=tmp;if(tmp===false){event.preventDefault();event.stopPropagation();}}}if(jQuery.browser.msie)event.target=event.preventDefault=event.stopPropagation=event.handler=event.data=null;return val;},fix:function(event){var originalEvent=event;event=jQuery.extend({},originalEvent);event.preventDefault=function(){if(originalEvent.preventDefault)originalEvent.preventDefault();originalEvent.returnValue=false;};event.stopPropagation=function(){if(originalEvent.stopPropagation)originalEvent.stopPropagation();originalEvent.cancelBubble=true;};if(!event.target&&event.srcElement)event.target=event.srcElement;if(jQuery.browser.safari&&event.target.nodeType==3)event.target=originalEvent.target.parentNode;if(!event.relatedTarget&&event.fromElement)event.relatedTarget=event.fromElement==event.target?event.toElement:event.fromElement;if(event.pageX==null&&event.clientX!=null){var e=document.documentElement,b=document.body;event.pageX=event.clientX+(e&&e.scrollLeft||b.scrollLeft||0);event.pageY=event.clientY+(e&&e.scrollTop||b.scrollTop||0);}if(!event.which&&(event.charCode||event.keyCode))event.which=event.charCode||event.keyCode;if(!event.metaKey&&event.ctrlKey)event.metaKey=event.ctrlKey;if(!event.which&&event.button)event.which=(event.button&1?1:(event.button&2?3:(event.button&4?2:0)));return event;}};jQuery.fn.extend({bind:function(type,data,fn){return type=="unload"?this.one(type,data,fn):this.each(function(){jQuery.event.add(this,type,fn||data,fn&&data);});},one:function(type,data,fn){return this.each(function(){jQuery.event.add(this,type,function(event){jQuery(this).unbind(event);return(fn||data).apply(this,arguments);},fn&&data);});},unbind:function(type,fn){return this.each(function(){jQuery.event.remove(this,type,fn);});},trigger:function(type,data,fn){return this.each(function(){jQuery.event.trigger(type,data,this,true,fn);});},triggerHandler:function(type,data,fn){if(this[0])return jQuery.event.trigger(type,data,this[0],false,fn);},toggle:function(){var a=arguments;return this.click(function(e){this.lastToggle=0==this.lastToggle?1:0;e.preventDefault();return a[this.lastToggle].apply(this,[e])||false;});},hover:function(f,g){function handleHover(e){var p=e.relatedTarget;while(p&&p!=this)try{p=p.parentNode;}catch(e){p=this;};if(p==this)return false;return(e.type=="mouseover"?f:g).apply(this,[e]);}return this.mouseover(handleHover).mouseout(handleHover);},ready:function(f){bindReady();if(jQuery.isReady)f.apply(document,[jQuery]);else +jQuery.readyList.push(function(){return f.apply(this,[jQuery]);});return this;}});jQuery.extend({isReady:false,readyList:[],ready:function(){if(!jQuery.isReady){jQuery.isReady=true;if(jQuery.readyList){jQuery.each(jQuery.readyList,function(){this.apply(document);});jQuery.readyList=null;}if(jQuery.browser.mozilla||jQuery.browser.opera)document.removeEventListener("DOMContentLoaded",jQuery.ready,false);if(!window.frames.length)jQuery(window).load(function(){jQuery("#__ie_init").remove();});}}});jQuery.each(("blur,focus,load,resize,scroll,unload,click,dblclick,"+"mousedown,mouseup,mousemove,mouseover,mouseout,change,select,"+"submit,keydown,keypress,keyup,error").split(","),function(i,o){jQuery.fn[o]=function(f){return f?this.bind(o,f):this.trigger(o);};});var readyBound=false;function bindReady(){if(readyBound)return;readyBound=true;if(jQuery.browser.mozilla||jQuery.browser.opera)document.addEventListener("DOMContentLoaded",jQuery.ready,false);else if(jQuery.browser.msie){document.write("<scr"+"ipt id=__ie_init defer=true "+"src=//:><\/script>");var script=document.getElementById("__ie_init");if(script)script.onreadystatechange=function(){if(this.readyState!="complete")return;jQuery.ready();};script=null;}else if(jQuery.browser.safari)jQuery.safariTimer=setInterval(function(){if(document.readyState=="loaded"||document.readyState=="complete"){clearInterval(jQuery.safariTimer);jQuery.safariTimer=null;jQuery.ready();}},10);jQuery.event.add(window,"load",jQuery.ready);}jQuery.fn.extend({load:function(url,params,callback){if(jQuery.isFunction(url))return this.bind("load",url);var off=url.indexOf(" ");if(off>=0){var selector=url.slice(off,url.length);url=url.slice(0,off);}callback=callback||function(){};var type="GET";if(params)if(jQuery.isFunction(params)){callback=params;params=null;}else{params=jQuery.param(params);type="POST";}var self=this;jQuery.ajax({url:url,type:type,data:params,complete:function(res,status){if(status=="success"||status=="notmodified")self.html(selector?jQuery("<div/>").append(res.responseText.replace(/<script(.|\s)*?\/script>/g,"")).find(selector):res.responseText);setTimeout(function(){self.each(callback,[res.responseText,status,res]);},13);}});return this;},serialize:function(){return jQuery.param(this.serializeArray());},serializeArray:function(){return this.map(function(){return jQuery.nodeName(this,"form")?jQuery.makeArray(this.elements):this;}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type));}).map(function(i,elem){var val=jQuery(this).val();return val==null?null:val.constructor==Array?jQuery.map(val,function(i,val){return{name:elem.name,value:val};}):{name:elem.name,value:val};}).get();}});jQuery.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(i,o){jQuery.fn[o]=function(f){return this.bind(o,f);};});var jsc=(new Date).getTime();jQuery.extend({get:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data=null;}return jQuery.ajax({type:"GET",url:url,data:data,success:callback,dataType:type});},getScript:function(url,callback){return jQuery.get(url,null,callback,"script");},getJSON:function(url,data,callback){return jQuery.get(url,data,callback,"json");},post:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data={};}return jQuery.ajax({type:"POST",url:url,data:data,success:callback,dataType:type});},ajaxSetup:function(settings){jQuery.extend(jQuery.ajaxSettings,settings);},ajaxSettings:{global:true,type:"GET",timeout:0,contentType:"application/x-www-form-urlencoded",processData:true,async:true,data:null},lastModified:{},ajax:function(s){var jsonp,jsre=/=(\?|%3F)/g,status,data;s=jQuery.extend(true,s,jQuery.extend(true,{},jQuery.ajaxSettings,s));if(s.data&&s.processData&&typeof s.data!="string")s.data=jQuery.param(s.data);var q=s.url.indexOf("?");if(q>-1){s.data=(s.data?s.data+"&":"")+s.url.slice(q+1);s.url=s.url.slice(0,q);}if(s.dataType=="jsonp"){if(!s.data||!s.data.match(jsre))s.data=(s.data?s.data+"&":"")+(s.jsonp||"callback")+"=?";s.dataType="json";}if(s.dataType=="json"&&s.data&&s.data.match(jsre)){jsonp="jsonp"+jsc++;s.data=s.data.replace(jsre,"="+jsonp);s.dataType="script";window[jsonp]=function(tmp){data=tmp;success();window[jsonp]=undefined;try{delete window[jsonp];}catch(e){}};}if(s.dataType=="script"&&s.cache==null)s.cache=false;if(s.cache===false&&s.type.toLowerCase()=="get")s.data=(s.data?s.data+"&":"")+"_="+(new Date()).getTime();if(s.data&&s.type.toLowerCase()=="get"){s.url+="?"+s.data;s.data=null;}if(s.global&&!jQuery.active++)jQuery.event.trigger("ajaxStart");if(!s.url.indexOf("http")&&s.dataType=="script"){var head=document.getElementsByTagName("head")[0];var script=document.createElement("script");script.src=s.url;if(!jsonp&&(s.success||s.complete)){var done=false;script.onload=script.onreadystatechange=function(){if(!done&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){done=true;success();complete();head.removeChild(script);}};}head.appendChild(script);return;}var requestDone=false;var xml=window.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest();xml.open(s.type,s.url,s.async);if(s.data)xml.setRequestHeader("Content-Type",s.contentType);if(s.ifModified)xml.setRequestHeader("If-Modified-Since",jQuery.lastModified[s.url]||"Thu, 01 Jan 1970 00:00:00 GMT");xml.setRequestHeader("X-Requested-With","XMLHttpRequest");if(s.beforeSend)s.beforeSend(xml);if(s.global)jQuery.event.trigger("ajaxSend",[xml,s]);var onreadystatechange=function(isTimeout){if(!requestDone&&xml&&(xml.readyState==4||isTimeout=="timeout")){requestDone=true;if(ival){clearInterval(ival);ival=null;}status=isTimeout=="timeout"&&"timeout"||!jQuery.httpSuccess(xml)&&"error"||s.ifModified&&jQuery.httpNotModified(xml,s.url)&&"notmodified"||"success";if(status=="success"){try{data=jQuery.httpData(xml,s.dataType);}catch(e){status="parsererror";}}if(status=="success"){var modRes;try{modRes=xml.getResponseHeader("Last-Modified");}catch(e){}if(s.ifModified&&modRes)jQuery.lastModified[s.url]=modRes;if(!jsonp)success();}else +jQuery.handleError(s,xml,status);complete();if(s.async)xml=null;}};if(s.async){var ival=setInterval(onreadystatechange,13);if(s.timeout>0)setTimeout(function(){if(xml){xml.abort();if(!requestDone)onreadystatechange("timeout");}},s.timeout);}try{xml.send(s.data);}catch(e){jQuery.handleError(s,xml,null,e);}if(!s.async)onreadystatechange();return xml;function success(){if(s.success)s.success(data,status);if(s.global)jQuery.event.trigger("ajaxSuccess",[xml,s]);}function complete(){if(s.complete)s.complete(xml,status);if(s.global)jQuery.event.trigger("ajaxComplete",[xml,s]);if(s.global&&!--jQuery.active)jQuery.event.trigger("ajaxStop");}},handleError:function(s,xml,status,e){if(s.error)s.error(xml,status,e);if(s.global)jQuery.event.trigger("ajaxError",[xml,s,e]);},active:0,httpSuccess:function(r){try{return!r.status&&location.protocol=="file:"||(r.status>=200&&r.status<300)||r.status==304||jQuery.browser.safari&&r.status==undefined;}catch(e){}return false;},httpNotModified:function(xml,url){try{var xmlRes=xml.getResponseHeader("Last-Modified");return xml.status==304||xmlRes==jQuery.lastModified[url]||jQuery.browser.safari&&xml.status==undefined;}catch(e){}return false;},httpData:function(r,type){var ct=r.getResponseHeader("content-type");var xml=type=="xml"||!type&&ct&&ct.indexOf("xml")>=0;var data=xml?r.responseXML:r.responseText;if(xml&&data.documentElement.tagName=="parsererror")throw"parsererror";if(type=="script")jQuery.globalEval(data);if(type=="json")data=eval("("+data+")");return data;},param:function(a){var s=[];if(a.constructor==Array||a.jquery)jQuery.each(a,function(){s.push(encodeURIComponent(this.name)+"="+encodeURIComponent(this.value));});else +for(var j in a)if(a[j]&&a[j].constructor==Array)jQuery.each(a[j],function(){s.push(encodeURIComponent(j)+"="+encodeURIComponent(this));});else +s.push(encodeURIComponent(j)+"="+encodeURIComponent(a[j]));return s.join("&").replace(/%20/g,"+");}});jQuery.fn.extend({show:function(speed,callback){return speed?this.animate({height:"show",width:"show",opacity:"show"},speed,callback):this.filter(":hidden").each(function(){this.style.display=this.oldblock?this.oldblock:"";if(jQuery.css(this,"display")=="none")this.style.display="block";}).end();},hide:function(speed,callback){return speed?this.animate({height:"hide",width:"hide",opacity:"hide"},speed,callback):this.filter(":visible").each(function(){this.oldblock=this.oldblock||jQuery.css(this,"display");if(this.oldblock=="none")this.oldblock="block";this.style.display="none";}).end();},_toggle:jQuery.fn.toggle,toggle:function(fn,fn2){return jQuery.isFunction(fn)&&jQuery.isFunction(fn2)?this._toggle(fn,fn2):fn?this.animate({height:"toggle",width:"toggle",opacity:"toggle"},fn,fn2):this.each(function(){jQuery(this)[jQuery(this).is(":hidden")?"show":"hide"]();});},slideDown:function(speed,callback){return this.animate({height:"show"},speed,callback);},slideUp:function(speed,callback){return this.animate({height:"hide"},speed,callback);},slideToggle:function(speed,callback){return this.animate({height:"toggle"},speed,callback);},fadeIn:function(speed,callback){return this.animate({opacity:"show"},speed,callback);},fadeOut:function(speed,callback){return this.animate({opacity:"hide"},speed,callback);},fadeTo:function(speed,to,callback){return this.animate({opacity:to},speed,callback);},animate:function(prop,speed,easing,callback){var opt=jQuery.speed(speed,easing,callback);return this[opt.queue===false?"each":"queue"](function(){opt=jQuery.extend({},opt);var hidden=jQuery(this).is(":hidden"),self=this;for(var p in prop){if(prop[p]=="hide"&&hidden||prop[p]=="show"&&!hidden)return jQuery.isFunction(opt.complete)&&opt.complete.apply(this);if(p=="height"||p=="width"){opt.display=jQuery.css(this,"display");opt.overflow=this.style.overflow;}}if(opt.overflow!=null)this.style.overflow="hidden";opt.curAnim=jQuery.extend({},prop);jQuery.each(prop,function(name,val){var e=new jQuery.fx(self,opt,name);if(/toggle|show|hide/.test(val))e[val=="toggle"?hidden?"show":"hide":val](prop);else{var parts=val.toString().match(/^([+-]?)([\d.]+)(.*)$/),start=e.cur(true)||0;if(parts){end=parseFloat(parts[2]),unit=parts[3]||"px";if(unit!="px"){self.style[name]=end+unit;start=(end/e.cur(true))*start;self.style[name]=start+unit;}if(parts[1])end=((parts[1]=="-"?-1:1)*end)+start;e.custom(start,end,unit);}else +e.custom(start,val,"");}});return true;});},queue:function(type,fn){if(!fn){fn=type;type="fx";}if(!arguments.length)return queue(this[0],type);return this.each(function(){if(fn.constructor==Array)queue(this,type,fn);else{queue(this,type).push(fn);if(queue(this,type).length==1)fn.apply(this);}});},stop:function(){var timers=jQuery.timers;return this.each(function(){for(var i=0;i<timers.length;i++)if(timers[i].elem==this)timers.splice(i--,1);}).dequeue();}});var queue=function(elem,type,array){if(!elem)return;var q=jQuery.data(elem,type+"queue");if(!q||array)q=jQuery.data(elem,type+"queue",array?jQuery.makeArray(array):[]);return q;};jQuery.fn.dequeue=function(type){type=type||"fx";return this.each(function(){var q=queue(this,type);q.shift();if(q.length)q[0].apply(this);});};jQuery.extend({speed:function(speed,easing,fn){var opt=speed&&speed.constructor==Object?speed:{complete:fn||!fn&&easing||jQuery.isFunction(speed)&&speed,duration:speed,easing:fn&&easing||easing&&easing.constructor!=Function&&easing};opt.duration=(opt.duration&&opt.duration.constructor==Number?opt.duration:{slow:600,fast:200}[opt.duration])||400;opt.old=opt.complete;opt.complete=function(){jQuery(this).dequeue();if(jQuery.isFunction(opt.old))opt.old.apply(this);};return opt;},easing:{linear:function(p,n,firstNum,diff){return firstNum+diff*p;},swing:function(p,n,firstNum,diff){return((-Math.cos(p*Math.PI)/2)+0.5)*diff+firstNum;}},timers:[],fx:function(elem,options,prop){this.options=options;this.elem=elem;this.prop=prop;if(!options.orig)options.orig={};}});jQuery.fx.prototype={update:function(){if(this.options.step)this.options.step.apply(this.elem,[this.now,this]);(jQuery.fx.step[this.prop]||jQuery.fx.step._default)(this);if(this.prop=="height"||this.prop=="width")this.elem.style.display="block";},cur:function(force){if(this.elem[this.prop]!=null&&this.elem.style[this.prop]==null)return this.elem[this.prop];var r=parseFloat(jQuery.curCSS(this.elem,this.prop,force));return r&&r>-10000?r:parseFloat(jQuery.css(this.elem,this.prop))||0;},custom:function(from,to,unit){this.startTime=(new Date()).getTime();this.start=from;this.end=to;this.unit=unit||this.unit||"px";this.now=this.start;this.pos=this.state=0;this.update();var self=this;function t(){return self.step();}t.elem=this.elem;jQuery.timers.push(t);if(jQuery.timers.length==1){var timer=setInterval(function(){var timers=jQuery.timers;for(var i=0;i<timers.length;i++)if(!timers[i]())timers.splice(i--,1);if(!timers.length)clearInterval(timer);},13);}},show:function(){this.options.orig[this.prop]=jQuery.attr(this.elem.style,this.prop);this.options.show=true;this.custom(0,this.cur());if(this.prop=="width"||this.prop=="height")this.elem.style[this.prop]="1px";jQuery(this.elem).show();},hide:function(){this.options.orig[this.prop]=jQuery.attr(this.elem.style,this.prop);this.options.hide=true;this.custom(this.cur(),0);},step:function(){var t=(new Date()).getTime();if(t>this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var done=true;for(var i in this.options.curAnim)if(this.options.curAnim[i]!==true)done=false;if(done){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(jQuery.css(this.elem,"display")=="none")this.elem.style.display="block";}if(this.options.hide)this.elem.style.display="none";if(this.options.hide||this.options.show)for(var p in this.options.curAnim)jQuery.attr(this.elem.style,p,this.options.orig[p]);}if(done&&jQuery.isFunction(this.options.complete))this.options.complete.apply(this.elem);return false;}else{var n=t-this.startTime;this.state=n/this.options.duration;this.pos=jQuery.easing[this.options.easing||(jQuery.easing.swing?"swing":"linear")](this.state,n,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update();}return true;}};jQuery.fx.step={scrollLeft:function(fx){fx.elem.scrollLeft=fx.now;},scrollTop:function(fx){fx.elem.scrollTop=fx.now;},opacity:function(fx){jQuery.attr(fx.elem.style,"opacity",fx.now);},_default:function(fx){fx.elem.style[fx.prop]=fx.now+fx.unit;}};jQuery.fn.offset=function(){var left=0,top=0,elem=this[0],results;if(elem)with(jQuery.browser){var absolute=jQuery.css(elem,"position")=="absolute",parent=elem.parentNode,offsetParent=elem.offsetParent,doc=elem.ownerDocument,safari2=safari&&!absolute&&parseInt(version)<522;if(elem.getBoundingClientRect){box=elem.getBoundingClientRect();add(box.left+Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),box.top+Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));if(msie){var border=jQuery("html").css("borderWidth");border=(border=="medium"||jQuery.boxModel&&parseInt(version)>=7)&&2||border;add(-border,-border);}}else{add(elem.offsetLeft,elem.offsetTop);while(offsetParent){add(offsetParent.offsetLeft,offsetParent.offsetTop);if(mozilla&&/^t[d|h]$/i.test(parent.tagName)||!safari2)border(offsetParent);if(safari2&&!absolute&&jQuery.css(offsetParent,"position")=="absolute")absolute=true;offsetParent=offsetParent.offsetParent;}while(parent.tagName&&/^body|html$/i.test(parent.tagName)){if(/^inline|table-row.*$/i.test(jQuery.css(parent,"display")))add(-parent.scrollLeft,-parent.scrollTop);if(mozilla&&jQuery.css(parent,"overflow")!="visible")border(parent);parent=parent.parentNode;}if(safari&&absolute)add(-doc.body.offsetLeft,-doc.body.offsetTop);}results={top:top,left:left};}return results;function border(elem){add(jQuery.css(elem,"borderLeftWidth"),jQuery.css(elem,"borderTopWidth"));}function add(l,t){left+=parseInt(l)||0;top+=parseInt(t)||0;}};})();
\ No newline at end of file diff --git a/devtools/client/inspector/markup/test/lib_jquery_1.3_min.js b/devtools/client/inspector/markup/test/lib_jquery_1.3_min.js new file mode 100644 index 000000000..378f94376 --- /dev/null +++ b/devtools/client/inspector/markup/test/lib_jquery_1.3_min.js @@ -0,0 +1,19 @@ +/* + * jQuery JavaScript Library v1.3 + * http://jquery.com/ + * + * Copyright (c) 2009 John Resig + * Dual licensed under the MIT and GPL licenses. + * http://docs.jquery.com/License + * + * Date: 2009-01-13 12:50:31 -0500 (Tue, 13 Jan 2009) + * Revision: 6104 + */ +(function(){var l=this,g,x=l.jQuery,o=l.$,n=l.jQuery=l.$=function(D,E){return new n.fn.init(D,E)},C=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,f=/^.[^:#\[\.,]*$/;n.fn=n.prototype={init:function(D,G){D=D||document;if(D.nodeType){this[0]=D;this.length=1;this.context=D;return this}if(typeof D==="string"){var F=C.exec(D);if(F&&(F[1]||!G)){if(F[1]){D=n.clean([F[1]],G)}else{var H=document.getElementById(F[3]);if(H){if(H.id!=F[3]){return n().find(D)}var E=n(H);E.context=document;E.selector=D;return E}D=[]}}else{return n(G).find(D)}}else{if(n.isFunction(D)){return n(document).ready(D)}}if(D.selector&&D.context){this.selector=D.selector;this.context=D.context}return this.setArray(n.makeArray(D))},selector:"",jquery:"1.3",size:function(){return this.length},get:function(D){return D===g?n.makeArray(this):this[D]},pushStack:function(E,G,D){var F=n(E);F.prevObject=this;F.context=this.context;if(G==="find"){F.selector=this.selector+(this.selector?" ":"")+D}else{if(G){F.selector=this.selector+"."+G+"("+D+")"}}return F},setArray:function(D){this.length=0;Array.prototype.push.apply(this,D);return this},each:function(E,D){return n.each(this,E,D)},index:function(D){return n.inArray(D&&D.jquery?D[0]:D,this)},attr:function(E,G,F){var D=E;if(typeof E==="string"){if(G===g){return this[0]&&n[F||"attr"](this[0],E)}else{D={};D[E]=G}}return this.each(function(H){for(E in D){n.attr(F?this.style:this,E,n.prop(this,D[E],F,H,E))}})},css:function(D,E){if((D=="width"||D=="height")&&parseFloat(E)<0){E=g}return this.attr(D,E,"curCSS")},text:function(E){if(typeof E!=="object"&&E!=null){return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(E))}var D="";n.each(E||this,function(){n.each(this.childNodes,function(){if(this.nodeType!=8){D+=this.nodeType!=1?this.nodeValue:n.fn.text([this])}})});return D},wrapAll:function(D){if(this[0]){var E=n(D,this[0].ownerDocument).clone();if(this[0].parentNode){E.insertBefore(this[0])}E.map(function(){var F=this;while(F.firstChild){F=F.firstChild}return F}).append(this)}return this},wrapInner:function(D){return this.each(function(){n(this).contents().wrapAll(D)})},wrap:function(D){return this.each(function(){n(this).wrapAll(D)})},append:function(){return this.domManip(arguments,true,function(D){if(this.nodeType==1){this.appendChild(D)}})},prepend:function(){return this.domManip(arguments,true,function(D){if(this.nodeType==1){this.insertBefore(D,this.firstChild)}})},before:function(){return this.domManip(arguments,false,function(D){this.parentNode.insertBefore(D,this)})},after:function(){return this.domManip(arguments,false,function(D){this.parentNode.insertBefore(D,this.nextSibling)})},end:function(){return this.prevObject||n([])},push:[].push,find:function(D){if(this.length===1&&!/,/.test(D)){var F=this.pushStack([],"find",D);F.length=0;n.find(D,this[0],F);return F}else{var E=n.map(this,function(G){return n.find(D,G)});return this.pushStack(/[^+>] [^+>]/.test(D)?n.unique(E):E,"find",D)}},clone:function(E){var D=this.map(function(){if(!n.support.noCloneEvent&&!n.isXMLDoc(this)){var H=this.cloneNode(true),G=document.createElement("div");G.appendChild(H);return n.clean([G.innerHTML])[0]}else{return this.cloneNode(true)}});var F=D.find("*").andSelf().each(function(){if(this[h]!==g){this[h]=null}});if(E===true){this.find("*").andSelf().each(function(H){if(this.nodeType==3){return}var G=n.data(this,"events");for(var J in G){for(var I in G[J]){n.event.add(F[H],J,G[J][I],G[J][I].data)}}})}return D},filter:function(D){return this.pushStack(n.isFunction(D)&&n.grep(this,function(F,E){return D.call(F,E)})||n.multiFilter(D,n.grep(this,function(E){return E.nodeType===1})),"filter",D)},closest:function(D){var E=n.expr.match.POS.test(D)?n(D):null;return this.map(function(){var F=this;while(F&&F.ownerDocument){if(E?E.index(F)>-1:n(F).is(D)){return F}F=F.parentNode}})},not:function(D){if(typeof D==="string"){if(f.test(D)){return this.pushStack(n.multiFilter(D,this,true),"not",D)}else{D=n.multiFilter(D,this)}}var E=D.length&&D[D.length-1]!==g&&!D.nodeType;return this.filter(function(){return E?n.inArray(this,D)<0:this!=D})},add:function(D){return this.pushStack(n.unique(n.merge(this.get(),typeof D==="string"?n(D):n.makeArray(D))))},is:function(D){return !!D&&n.multiFilter(D,this).length>0},hasClass:function(D){return !!D&&this.is("."+D)},val:function(J){if(J===g){var D=this[0];if(D){if(n.nodeName(D,"option")){return(D.attributes.value||{}).specified?D.value:D.text}if(n.nodeName(D,"select")){var H=D.selectedIndex,K=[],L=D.options,G=D.type=="select-one";if(H<0){return null}for(var E=G?H:0,I=G?H+1:L.length;E<I;E++){var F=L[E];if(F.selected){J=n(F).val();if(G){return J}K.push(J)}}return K}return(D.value||"").replace(/\r/g,"")}return g}if(typeof J==="number"){J+=""}return this.each(function(){if(this.nodeType!=1){return}if(n.isArray(J)&&/radio|checkbox/.test(this.type)){this.checked=(n.inArray(this.value,J)>=0||n.inArray(this.name,J)>=0)}else{if(n.nodeName(this,"select")){var M=n.makeArray(J);n("option",this).each(function(){this.selected=(n.inArray(this.value,M)>=0||n.inArray(this.text,M)>=0)});if(!M.length){this.selectedIndex=-1}}else{this.value=J}}})},html:function(D){return D===g?(this[0]?this[0].innerHTML:null):this.empty().append(D)},replaceWith:function(D){return this.after(D).remove()},eq:function(D){return this.slice(D,+D+1)},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments),"slice",Array.prototype.slice.call(arguments).join(","))},map:function(D){return this.pushStack(n.map(this,function(F,E){return D.call(F,E,F)}))},andSelf:function(){return this.add(this.prevObject)},domManip:function(J,M,L){if(this[0]){var I=(this[0].ownerDocument||this[0]).createDocumentFragment(),F=n.clean(J,(this[0].ownerDocument||this[0]),I),H=I.firstChild,D=this.length>1?I.cloneNode(true):I;if(H){for(var G=0,E=this.length;G<E;G++){L.call(K(this[G],H),G>0?D.cloneNode(true):I)}}if(F){n.each(F,y)}}return this;function K(N,O){return M&&n.nodeName(N,"table")&&n.nodeName(O,"tr")?(N.getElementsByTagName("tbody")[0]||N.appendChild(N.ownerDocument.createElement("tbody"))):N}}};n.fn.init.prototype=n.fn;function y(D,E){if(E.src){n.ajax({url:E.src,async:false,dataType:"script"})}else{n.globalEval(E.text||E.textContent||E.innerHTML||"")}if(E.parentNode){E.parentNode.removeChild(E)}}function e(){return +new Date}n.extend=n.fn.extend=function(){var I=arguments[0]||{},G=1,H=arguments.length,D=false,F;if(typeof I==="boolean"){D=I;I=arguments[1]||{};G=2}if(typeof I!=="object"&&!n.isFunction(I)){I={}}if(H==G){I=this;--G}for(;G<H;G++){if((F=arguments[G])!=null){for(var E in F){var J=I[E],K=F[E];if(I===K){continue}if(D&&K&&typeof K==="object"&&!K.nodeType){I[E]=n.extend(D,J||(K.length!=null?[]:{}),K)}else{if(K!==g){I[E]=K}}}}}return I};var b=/z-?index|font-?weight|opacity|zoom|line-?height/i,p=document.defaultView||{},r=Object.prototype.toString;n.extend({noConflict:function(D){l.$=o;if(D){l.jQuery=x}return n},isFunction:function(D){return r.call(D)==="[object Function]"},isArray:function(D){return r.call(D)==="[object Array]"},isXMLDoc:function(D){return D.documentElement&&!D.body||D.tagName&&D.ownerDocument&&!D.ownerDocument.body},globalEval:function(F){F=n.trim(F);if(F){var E=document.getElementsByTagName("head")[0]||document.documentElement,D=document.createElement("script");D.type="text/javascript";if(n.support.scriptEval){D.appendChild(document.createTextNode(F))}else{D.text=F}E.insertBefore(D,E.firstChild);E.removeChild(D)}},nodeName:function(E,D){return E.nodeName&&E.nodeName.toUpperCase()==D.toUpperCase()},each:function(F,J,E){var D,G=0,H=F.length;if(E){if(H===g){for(D in F){if(J.apply(F[D],E)===false){break}}}else{for(;G<H;){if(J.apply(F[G++],E)===false){break}}}}else{if(H===g){for(D in F){if(J.call(F[D],D,F[D])===false){break}}}else{for(var I=F[0];G<H&&J.call(I,G,I)!==false;I=F[++G]){}}}return F},prop:function(G,H,F,E,D){if(n.isFunction(H)){H=H.call(G,E)}return typeof H==="number"&&F=="curCSS"&&!b.test(D)?H+"px":H},className:{add:function(D,E){n.each((E||"").split(/\s+/),function(F,G){if(D.nodeType==1&&!n.className.has(D.className,G)){D.className+=(D.className?" ":"")+G}})},remove:function(D,E){if(D.nodeType==1){D.className=E!==g?n.grep(D.className.split(/\s+/),function(F){return !n.className.has(E,F)}).join(" "):""}},has:function(E,D){return n.inArray(D,(E.className||E).toString().split(/\s+/))>-1}},swap:function(G,F,H){var D={};for(var E in F){D[E]=G.style[E];G.style[E]=F[E]}H.call(G);for(var E in F){G.style[E]=D[E]}},css:function(F,D,H){if(D=="width"||D=="height"){var J,E={position:"absolute",visibility:"hidden",display:"block"},I=D=="width"?["Left","Right"]:["Top","Bottom"];function G(){J=D=="width"?F.offsetWidth:F.offsetHeight;var L=0,K=0;n.each(I,function(){L+=parseFloat(n.curCSS(F,"padding"+this,true))||0;K+=parseFloat(n.curCSS(F,"border"+this+"Width",true))||0});J-=Math.round(L+K)}if(n(F).is(":visible")){G()}else{n.swap(F,E,G)}return Math.max(0,J)}return n.curCSS(F,D,H)},curCSS:function(H,E,F){var K,D=H.style;if(E=="opacity"&&!n.support.opacity){K=n.attr(D,"opacity");return K==""?"1":K}if(E.match(/float/i)){E=v}if(!F&&D&&D[E]){K=D[E]}else{if(p.getComputedStyle){if(E.match(/float/i)){E="float"}E=E.replace(/([A-Z])/g,"-$1").toLowerCase();var L=p.getComputedStyle(H,null);if(L){K=L.getPropertyValue(E)}if(E=="opacity"&&K==""){K="1"}}else{if(H.currentStyle){var I=E.replace(/\-(\w)/g,function(M,N){return N.toUpperCase()});K=H.currentStyle[E]||H.currentStyle[I];if(!/^\d+(px)?$/i.test(K)&&/^\d/.test(K)){var G=D.left,J=H.runtimeStyle.left;H.runtimeStyle.left=H.currentStyle.left;D.left=K||0;K=D.pixelLeft+"px";D.left=G;H.runtimeStyle.left=J}}}}return K},clean:function(E,J,H){J=J||document;if(typeof J.createElement==="undefined"){J=J.ownerDocument||J[0]&&J[0].ownerDocument||document}if(!H&&E.length===1&&typeof E[0]==="string"){var G=/^<(\w+)\s*\/?>$/.exec(E[0]);if(G){return[J.createElement(G[1])]}}var F=[],D=[],K=J.createElement("div");n.each(E,function(O,Q){if(typeof Q==="number"){Q+=""}if(!Q){return}if(typeof Q==="string"){Q=Q.replace(/(<(\w+)[^>]*?)\/>/g,function(S,T,R){return R.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?S:T+"></"+R+">"});var N=n.trim(Q).toLowerCase();var P=!N.indexOf("<opt")&&[1,"<select multiple='multiple'>","</select>"]||!N.indexOf("<leg")&&[1,"<fieldset>","</fieldset>"]||N.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"<table>","</table>"]||!N.indexOf("<tr")&&[2,"<table><tbody>","</tbody></table>"]||(!N.indexOf("<td")||!N.indexOf("<th"))&&[3,"<table><tbody><tr>","</tr></tbody></table>"]||!N.indexOf("<col")&&[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]||!n.support.htmlSerialize&&[1,"div<div>","</div>"]||[0,"",""];K.innerHTML=P[1]+Q+P[2];while(P[0]--){K=K.lastChild}if(!n.support.tbody){var M=!N.indexOf("<table")&&N.indexOf("<tbody")<0?K.firstChild&&K.firstChild.childNodes:P[1]=="<table>"&&N.indexOf("<tbody")<0?K.childNodes:[];for(var L=M.length-1;L>=0;--L){if(n.nodeName(M[L],"tbody")&&!M[L].childNodes.length){M[L].parentNode.removeChild(M[L])}}}if(!n.support.leadingWhitespace&&/^\s/.test(Q)){K.insertBefore(J.createTextNode(Q.match(/^\s*/)[0]),K.firstChild)}Q=n.makeArray(K.childNodes)}if(Q.nodeType){F.push(Q)}else{F=n.merge(F,Q)}});if(H){for(var I=0;F[I];I++){if(n.nodeName(F[I],"script")&&(!F[I].type||F[I].type.toLowerCase()==="text/javascript")){D.push(F[I].parentNode?F[I].parentNode.removeChild(F[I]):F[I])}else{if(F[I].nodeType===1){F.splice.apply(F,[I+1,0].concat(n.makeArray(F[I].getElementsByTagName("script"))))}H.appendChild(F[I])}}return D}return F},attr:function(I,F,J){if(!I||I.nodeType==3||I.nodeType==8){return g}var G=!n.isXMLDoc(I),K=J!==g;F=G&&n.props[F]||F;if(I.tagName){var E=/href|src|style/.test(F);if(F=="selected"&&I.parentNode){I.parentNode.selectedIndex}if(F in I&&G&&!E){if(K){if(F=="type"&&n.nodeName(I,"input")&&I.parentNode){throw"type property can't be changed"}I[F]=J}if(n.nodeName(I,"form")&&I.getAttributeNode(F)){return I.getAttributeNode(F).nodeValue}if(F=="tabIndex"){var H=I.getAttributeNode("tabIndex");return H&&H.specified?H.value:I.nodeName.match(/^(a|area|button|input|object|select|textarea)$/i)?0:g}return I[F]}if(!n.support.style&&G&&F=="style"){return n.attr(I.style,"cssText",J)}if(K){I.setAttribute(F,""+J)}var D=!n.support.hrefNormalized&&G&&E?I.getAttribute(F,2):I.getAttribute(F);return D===null?g:D}if(!n.support.opacity&&F=="opacity"){if(K){I.zoom=1;I.filter=(I.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(J)+""=="NaN"?"":"alpha(opacity="+J*100+")")}return I.filter&&I.filter.indexOf("opacity=")>=0?(parseFloat(I.filter.match(/opacity=([^)]*)/)[1])/100)+"":""}F=F.replace(/-([a-z])/ig,function(L,M){return M.toUpperCase()});if(K){I[F]=J}return I[F]},trim:function(D){return(D||"").replace(/^\s+|\s+$/g,"")},makeArray:function(F){var D=[];if(F!=null){var E=F.length;if(E==null||typeof F==="string"||n.isFunction(F)||F.setInterval){D[0]=F}else{while(E){D[--E]=F[E]}}}return D},inArray:function(F,G){for(var D=0,E=G.length;D<E;D++){if(G[D]===F){return D}}return -1},merge:function(G,D){var E=0,F,H=G.length;if(!n.support.getAll){while((F=D[E++])!=null){if(F.nodeType!=8){G[H++]=F}}}else{while((F=D[E++])!=null){G[H++]=F}}return G},unique:function(J){var E=[],D={};try{for(var F=0,G=J.length;F<G;F++){var I=n.data(J[F]);if(!D[I]){D[I]=true;E.push(J[F])}}}catch(H){E=J}return E},grep:function(E,I,D){var F=[];for(var G=0,H=E.length;G<H;G++){if(!D!=!I(E[G],G)){F.push(E[G])}}return F},map:function(D,I){var E=[];for(var F=0,G=D.length;F<G;F++){var H=I(D[F],F);if(H!=null){E[E.length]=H}}return E.concat.apply([],E)}});var B=navigator.userAgent.toLowerCase();n.browser={version:(B.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[0,"0"])[1],safari:/webkit/.test(B),opera:/opera/.test(B),msie:/msie/.test(B)&&!/opera/.test(B),mozilla:/mozilla/.test(B)&&!/(compatible|webkit)/.test(B)};n.each({parent:function(D){return D.parentNode},parents:function(D){return n.dir(D,"parentNode")},next:function(D){return n.nth(D,2,"nextSibling")},prev:function(D){return n.nth(D,2,"previousSibling")},nextAll:function(D){return n.dir(D,"nextSibling")},prevAll:function(D){return n.dir(D,"previousSibling")},siblings:function(D){return n.sibling(D.parentNode.firstChild,D)},children:function(D){return n.sibling(D.firstChild)},contents:function(D){return n.nodeName(D,"iframe")?D.contentDocument||D.contentWindow.document:n.makeArray(D.childNodes)}},function(D,E){n.fn[D]=function(F){var G=n.map(this,E);if(F&&typeof F=="string"){G=n.multiFilter(F,G)}return this.pushStack(n.unique(G),D,F)}});n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(D,E){n.fn[D]=function(){var F=arguments;return this.each(function(){for(var G=0,H=F.length;G<H;G++){n(F[G])[E](this)}})}});n.each({removeAttr:function(D){n.attr(this,D,"");if(this.nodeType==1){this.removeAttribute(D)}},addClass:function(D){n.className.add(this,D)},removeClass:function(D){n.className.remove(this,D)},toggleClass:function(E,D){if(typeof D!=="boolean"){D=!n.className.has(this,E)}n.className[D?"add":"remove"](this,E)},remove:function(D){if(!D||n.filter(D,[this]).length){n("*",this).add([this]).each(function(){n.event.remove(this);n.removeData(this)});if(this.parentNode){this.parentNode.removeChild(this)}}},empty:function(){n(">*",this).remove();while(this.firstChild){this.removeChild(this.firstChild)}}},function(D,E){n.fn[D]=function(){return this.each(E,arguments)}});function j(D,E){return D[0]&&parseInt(n.curCSS(D[0],E,true),10)||0}var h="jQuery"+e(),u=0,z={};n.extend({cache:{},data:function(E,D,F){E=E==l?z:E;var G=E[h];if(!G){G=E[h]=++u}if(D&&!n.cache[G]){n.cache[G]={}}if(F!==g){n.cache[G][D]=F}return D?n.cache[G][D]:G},removeData:function(E,D){E=E==l?z:E;var G=E[h];if(D){if(n.cache[G]){delete n.cache[G][D];D="";for(D in n.cache[G]){break}if(!D){n.removeData(E)}}}else{try{delete E[h]}catch(F){if(E.removeAttribute){E.removeAttribute(h)}}delete n.cache[G]}},queue:function(E,D,G){if(E){D=(D||"fx")+"queue";var F=n.data(E,D);if(!F||n.isArray(G)){F=n.data(E,D,n.makeArray(G))}else{if(G){F.push(G)}}}return F},dequeue:function(G,F){var D=n.queue(G,F),E=D.shift();if(!F||F==="fx"){E=D[0]}if(E!==g){E.call(G)}}});n.fn.extend({data:function(D,F){var G=D.split(".");G[1]=G[1]?"."+G[1]:"";if(F===g){var E=this.triggerHandler("getData"+G[1]+"!",[G[0]]);if(E===g&&this.length){E=n.data(this[0],D)}return E===g&&G[1]?this.data(G[0]):E}else{return this.trigger("setData"+G[1]+"!",[G[0],F]).each(function(){n.data(this,D,F)})}},removeData:function(D){return this.each(function(){n.removeData(this,D)})},queue:function(D,E){if(typeof D!=="string"){E=D;D="fx"}if(E===g){return n.queue(this[0],D)}return this.each(function(){var F=n.queue(this,D,E);if(D=="fx"&&F.length==1){F[0].call(this)}})},dequeue:function(D){return this.each(function(){n.dequeue(this,D)})}}); +/* + * Sizzle CSS Selector Engine - v0.9.1 + * Copyright 2009, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){var N=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|[^[\]]+)+\]|\\.|[^ >+~,(\[]+)+|[>+~])(\s*,\s*)?/g,I=0,F=Object.prototype.toString;var E=function(ae,S,aa,V){aa=aa||[];S=S||document;if(S.nodeType!==1&&S.nodeType!==9){return[]}if(!ae||typeof ae!=="string"){return aa}var ab=[],ac,Y,ah,ag,Z,R,Q=true;N.lastIndex=0;while((ac=N.exec(ae))!==null){ab.push(ac[1]);if(ac[2]){R=RegExp.rightContext;break}}if(ab.length>1&&G.match.POS.exec(ae)){if(ab.length===2&&G.relative[ab[0]]){var U="",X;while((X=G.match.POS.exec(ae))){U+=X[0];ae=ae.replace(G.match.POS,"")}Y=E.filter(U,E(/\s$/.test(ae)?ae+"*":ae,S))}else{Y=G.relative[ab[0]]?[S]:E(ab.shift(),S);while(ab.length){var P=[];ae=ab.shift();if(G.relative[ae]){ae+=ab.shift()}for(var af=0,ad=Y.length;af<ad;af++){E(ae,Y[af],P)}Y=P}}}else{var ai=V?{expr:ab.pop(),set:D(V)}:E.find(ab.pop(),ab.length===1&&S.parentNode?S.parentNode:S);Y=E.filter(ai.expr,ai.set);if(ab.length>0){ah=D(Y)}else{Q=false}while(ab.length){var T=ab.pop(),W=T;if(!G.relative[T]){T=""}else{W=ab.pop()}if(W==null){W=S}G.relative[T](ah,W,M(S))}}if(!ah){ah=Y}if(!ah){throw"Syntax error, unrecognized expression: "+(T||ae)}if(F.call(ah)==="[object Array]"){if(!Q){aa.push.apply(aa,ah)}else{if(S.nodeType===1){for(var af=0;ah[af]!=null;af++){if(ah[af]&&(ah[af]===true||ah[af].nodeType===1&&H(S,ah[af]))){aa.push(Y[af])}}}else{for(var af=0;ah[af]!=null;af++){if(ah[af]&&ah[af].nodeType===1){aa.push(Y[af])}}}}}else{D(ah,aa)}if(R){E(R,S,aa,V)}return aa};E.matches=function(P,Q){return E(P,null,null,Q)};E.find=function(V,S){var W,Q;if(!V){return[]}for(var R=0,P=G.order.length;R<P;R++){var T=G.order[R],Q;if((Q=G.match[T].exec(V))){var U=RegExp.leftContext;if(U.substr(U.length-1)!=="\\"){Q[1]=(Q[1]||"").replace(/\\/g,"");W=G.find[T](Q,S);if(W!=null){V=V.replace(G.match[T],"");break}}}}if(!W){W=S.getElementsByTagName("*")}return{set:W,expr:V}};E.filter=function(S,ac,ad,T){var Q=S,Y=[],ah=ac,V,ab;while(S&&ac.length){for(var U in G.filter){if((V=G.match[U].exec(S))!=null){var Z=G.filter[U],R=null,X=0,aa,ag;ab=false;if(ah==Y){Y=[]}if(G.preFilter[U]){V=G.preFilter[U](V,ah,ad,Y,T);if(!V){ab=aa=true}else{if(V===true){continue}else{if(V[0]===true){R=[];var W=null,af;for(var ae=0;(af=ah[ae])!==g;ae++){if(af&&W!==af){R.push(af);W=af}}}}}}if(V){for(var ae=0;(ag=ah[ae])!==g;ae++){if(ag){if(R&&ag!=R[X]){X++}aa=Z(ag,V,X,R);var P=T^!!aa;if(ad&&aa!=null){if(P){ab=true}else{ah[ae]=false}}else{if(P){Y.push(ag);ab=true}}}}}if(aa!==g){if(!ad){ah=Y}S=S.replace(G.match[U],"");if(!ab){return[]}break}}}S=S.replace(/\s*,\s*/,"");if(S==Q){if(ab==null){throw"Syntax error, unrecognized expression: "+S}else{break}}Q=S}return ah};var G=E.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF_-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF_-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(P){return P.getAttribute("href")}},relative:{"+":function(T,Q){for(var R=0,P=T.length;R<P;R++){var S=T[R];if(S){var U=S.previousSibling;while(U&&U.nodeType!==1){U=U.previousSibling}T[R]=typeof Q==="string"?U||false:U===Q}}if(typeof Q==="string"){E.filter(Q,T,true)}},">":function(U,Q,V){if(typeof Q==="string"&&!/\W/.test(Q)){Q=V?Q:Q.toUpperCase();for(var R=0,P=U.length;R<P;R++){var T=U[R];if(T){var S=T.parentNode;U[R]=S.nodeName===Q?S:false}}}else{for(var R=0,P=U.length;R<P;R++){var T=U[R];if(T){U[R]=typeof Q==="string"?T.parentNode:T.parentNode===Q}}if(typeof Q==="string"){E.filter(Q,U,true)}}},"":function(S,Q,U){var R="done"+(I++),P=O;if(!Q.match(/\W/)){var T=Q=U?Q:Q.toUpperCase();P=L}P("parentNode",Q,R,S,T,U)},"~":function(S,Q,U){var R="done"+(I++),P=O;if(typeof Q==="string"&&!Q.match(/\W/)){var T=Q=U?Q:Q.toUpperCase();P=L}P("previousSibling",Q,R,S,T,U)}},find:{ID:function(Q,R){if(R.getElementById){var P=R.getElementById(Q[1]);return P?[P]:[]}},NAME:function(P,Q){return Q.getElementsByName?Q.getElementsByName(P[1]):null},TAG:function(P,Q){return Q.getElementsByTagName(P[1])}},preFilter:{CLASS:function(S,Q,R,P,U){S=" "+S[1].replace(/\\/g,"")+" ";for(var T=0;Q[T];T++){if(U^(" "+Q[T].className+" ").indexOf(S)>=0){if(!R){P.push(Q[T])}}else{if(R){Q[T]=false}}}return false},ID:function(P){return P[1].replace(/\\/g,"")},TAG:function(Q,P){for(var R=0;!P[R];R++){}return M(P[R])?Q[1]:Q[1].toUpperCase()},CHILD:function(P){if(P[1]=="nth"){var Q=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(P[2]=="even"&&"2n"||P[2]=="odd"&&"2n+1"||!/\D/.test(P[2])&&"0n+"+P[2]||P[2]);P[2]=(Q[1]+(Q[2]||1))-0;P[3]=Q[3]-0}P[0]="done"+(I++);return P},ATTR:function(Q){var P=Q[1];if(G.attrMap[P]){Q[1]=G.attrMap[P]}if(Q[2]==="~="){Q[4]=" "+Q[4]+" "}return Q},PSEUDO:function(T,Q,R,P,U){if(T[1]==="not"){if(T[3].match(N).length>1){T[3]=E(T[3],null,null,Q)}else{var S=E.filter(T[3],Q,R,true^U);if(!R){P.push.apply(P,S)}return false}}else{if(G.match.POS.test(T[0])){return true}}return T},POS:function(P){P.unshift(true);return P}},filters:{enabled:function(P){return P.disabled===false&&P.type!=="hidden"},disabled:function(P){return P.disabled===true},checked:function(P){return P.checked===true},selected:function(P){P.parentNode.selectedIndex;return P.selected===true},parent:function(P){return !!P.firstChild},empty:function(P){return !P.firstChild},has:function(R,Q,P){return !!E(P[3],R).length},header:function(P){return/h\d/i.test(P.nodeName)},text:function(P){return"text"===P.type},radio:function(P){return"radio"===P.type},checkbox:function(P){return"checkbox"===P.type},file:function(P){return"file"===P.type},password:function(P){return"password"===P.type},submit:function(P){return"submit"===P.type},image:function(P){return"image"===P.type},reset:function(P){return"reset"===P.type},button:function(P){return"button"===P.type||P.nodeName.toUpperCase()==="BUTTON"},input:function(P){return/input|select|textarea|button/i.test(P.nodeName)}},setFilters:{first:function(Q,P){return P===0},last:function(R,Q,P,S){return Q===S.length-1},even:function(Q,P){return P%2===0},odd:function(Q,P){return P%2===1},lt:function(R,Q,P){return Q<P[3]-0},gt:function(R,Q,P){return Q>P[3]-0},nth:function(R,Q,P){return P[3]-0==Q},eq:function(R,Q,P){return P[3]-0==Q}},filter:{CHILD:function(P,S){var V=S[1],W=P.parentNode;var U="child"+W.childNodes.length;if(W&&(!W[U]||!P.nodeIndex)){var T=1;for(var Q=W.firstChild;Q;Q=Q.nextSibling){if(Q.nodeType==1){Q.nodeIndex=T++}}W[U]=T-1}if(V=="first"){return P.nodeIndex==1}else{if(V=="last"){return P.nodeIndex==W[U]}else{if(V=="only"){return W[U]==1}else{if(V=="nth"){var Y=false,R=S[2],X=S[3];if(R==1&&X==0){return true}if(R==0){if(P.nodeIndex==X){Y=true}}else{if((P.nodeIndex-X)%R==0&&(P.nodeIndex-X)/R>=0){Y=true}}return Y}}}}},PSEUDO:function(V,R,S,W){var Q=R[1],T=G.filters[Q];if(T){return T(V,S,R,W)}else{if(Q==="contains"){return(V.textContent||V.innerText||"").indexOf(R[3])>=0}else{if(Q==="not"){var U=R[3];for(var S=0,P=U.length;S<P;S++){if(U[S]===V){return false}}return true}}}},ID:function(Q,P){return Q.nodeType===1&&Q.getAttribute("id")===P},TAG:function(Q,P){return(P==="*"&&Q.nodeType===1)||Q.nodeName===P},CLASS:function(Q,P){return P.test(Q.className)},ATTR:function(T,R){var P=G.attrHandle[R[1]]?G.attrHandle[R[1]](T):T[R[1]]||T.getAttribute(R[1]),U=P+"",S=R[2],Q=R[4];return P==null?false:S==="="?U===Q:S==="*="?U.indexOf(Q)>=0:S==="~="?(" "+U+" ").indexOf(Q)>=0:!R[4]?P:S==="!="?U!=Q:S==="^="?U.indexOf(Q)===0:S==="$="?U.substr(U.length-Q.length)===Q:S==="|="?U===Q||U.substr(0,Q.length+1)===Q+"-":false},POS:function(T,Q,R,U){var P=Q[2],S=G.setFilters[P];if(S){return S(T,R,Q,U)}}}};for(var K in G.match){G.match[K]=RegExp(G.match[K].source+/(?![^\[]*\])(?![^\(]*\))/.source)}var D=function(Q,P){Q=Array.prototype.slice.call(Q);if(P){P.push.apply(P,Q);return P}return Q};try{Array.prototype.slice.call(document.documentElement.childNodes)}catch(J){D=function(T,S){var Q=S||[];if(F.call(T)==="[object Array]"){Array.prototype.push.apply(Q,T)}else{if(typeof T.length==="number"){for(var R=0,P=T.length;R<P;R++){Q.push(T[R])}}else{for(var R=0;T[R];R++){Q.push(T[R])}}}return Q}}(function(){var Q=document.createElement("form"),R="script"+(new Date).getTime();Q.innerHTML="<input name='"+R+"'/>";var P=document.documentElement;P.insertBefore(Q,P.firstChild);if(!!document.getElementById(R)){G.find.ID=function(T,U){if(U.getElementById){var S=U.getElementById(T[1]);return S?S.id===T[1]||S.getAttributeNode&&S.getAttributeNode("id").nodeValue===T[1]?[S]:g:[]}};G.filter.ID=function(U,S){var T=U.getAttributeNode&&U.getAttributeNode("id");return U.nodeType===1&&T&&T.nodeValue===S}}P.removeChild(Q)})();(function(){var P=document.createElement("div");P.appendChild(document.createComment(""));if(P.getElementsByTagName("*").length>0){G.find.TAG=function(Q,U){var T=U.getElementsByTagName(Q[1]);if(Q[1]==="*"){var S=[];for(var R=0;T[R];R++){if(T[R].nodeType===1){S.push(T[R])}}T=S}return T}}P.innerHTML="<a href='#'></a>";if(P.firstChild.getAttribute("href")!=="#"){G.attrHandle.href=function(Q){return Q.getAttribute("href",2)}}})();if(document.querySelectorAll){(function(){var P=E;E=function(T,S,Q,R){S=S||document;if(!R&&S.nodeType===9){try{return D(S.querySelectorAll(T),Q)}catch(U){}}return P(T,S,Q,R)};E.find=P.find;E.filter=P.filter;E.selectors=P.selectors;E.matches=P.matches})()}if(document.documentElement.getElementsByClassName){G.order.splice(1,0,"CLASS");G.find.CLASS=function(P,Q){return Q.getElementsByClassName(P[1])}}function L(Q,W,V,Z,X,Y){for(var T=0,R=Z.length;T<R;T++){var P=Z[T];if(P){P=P[Q];var U=false;while(P&&P.nodeType){var S=P[V];if(S){U=Z[S];break}if(P.nodeType===1&&!Y){P[V]=T}if(P.nodeName===W){U=P;break}P=P[Q]}Z[T]=U}}}function O(Q,V,U,Y,W,X){for(var S=0,R=Y.length;S<R;S++){var P=Y[S];if(P){P=P[Q];var T=false;while(P&&P.nodeType){if(P[U]){T=Y[P[U]];break}if(P.nodeType===1){if(!X){P[U]=S}if(typeof V!=="string"){if(P===V){T=true;break}}else{if(E.filter(V,[P]).length>0){T=P;break}}}P=P[Q]}Y[S]=T}}}var H=document.compareDocumentPosition?function(Q,P){return Q.compareDocumentPosition(P)&16}:function(Q,P){return Q!==P&&(Q.contains?Q.contains(P):true)};var M=function(P){return P.documentElement&&!P.body||P.tagName&&P.ownerDocument&&!P.ownerDocument.body};n.find=E;n.filter=E.filter;n.expr=E.selectors;n.expr[":"]=n.expr.filters;E.selectors.filters.hidden=function(P){return"hidden"===P.type||n.css(P,"display")==="none"||n.css(P,"visibility")==="hidden"};E.selectors.filters.visible=function(P){return"hidden"!==P.type&&n.css(P,"display")!=="none"&&n.css(P,"visibility")!=="hidden"};E.selectors.filters.animated=function(P){return n.grep(n.timers,function(Q){return P===Q.elem}).length};n.multiFilter=function(R,P,Q){if(Q){R=":not("+R+")"}return E.matches(R,P)};n.dir=function(R,Q){var P=[],S=R[Q];while(S&&S!=document){if(S.nodeType==1){P.push(S)}S=S[Q]}return P};n.nth=function(T,P,R,S){P=P||1;var Q=0;for(;T;T=T[R]){if(T.nodeType==1&&++Q==P){break}}return T};n.sibling=function(R,Q){var P=[];for(;R;R=R.nextSibling){if(R.nodeType==1&&R!=Q){P.push(R)}}return P};return;l.Sizzle=E})();n.event={add:function(H,E,G,J){if(H.nodeType==3||H.nodeType==8){return}if(H.setInterval&&H!=l){H=l}if(!G.guid){G.guid=this.guid++}if(J!==g){var F=G;G=this.proxy(F);G.data=J}var D=n.data(H,"events")||n.data(H,"events",{}),I=n.data(H,"handle")||n.data(H,"handle",function(){return typeof n!=="undefined"&&!n.event.triggered?n.event.handle.apply(arguments.callee.elem,arguments):g});I.elem=H;n.each(E.split(/\s+/),function(L,M){var N=M.split(".");M=N.shift();G.type=N.slice().sort().join(".");var K=D[M];if(n.event.specialAll[M]){n.event.specialAll[M].setup.call(H,J,N)}if(!K){K=D[M]={};if(!n.event.special[M]||n.event.special[M].setup.call(H,J,N)===false){if(H.addEventListener){H.addEventListener(M,I,false)}else{if(H.attachEvent){H.attachEvent("on"+M,I)}}}}K[G.guid]=G;n.event.global[M]=true});H=null},guid:1,global:{},remove:function(J,G,I){if(J.nodeType==3||J.nodeType==8){return}var F=n.data(J,"events"),E,D;if(F){if(G===g||(typeof G==="string"&&G.charAt(0)==".")){for(var H in F){this.remove(J,H+(G||""))}}else{if(G.type){I=G.handler;G=G.type}n.each(G.split(/\s+/),function(L,N){var P=N.split(".");N=P.shift();var M=RegExp("(^|\\.)"+P.slice().sort().join(".*\\.")+"(\\.|$)");if(F[N]){if(I){delete F[N][I.guid]}else{for(var O in F[N]){if(M.test(F[N][O].type)){delete F[N][O]}}}if(n.event.specialAll[N]){n.event.specialAll[N].teardown.call(J,P)}for(E in F[N]){break}if(!E){if(!n.event.special[N]||n.event.special[N].teardown.call(J,P)===false){if(J.removeEventListener){J.removeEventListener(N,n.data(J,"handle"),false)}else{if(J.detachEvent){J.detachEvent("on"+N,n.data(J,"handle"))}}}E=null;delete F[N]}}})}for(E in F){break}if(!E){var K=n.data(J,"handle");if(K){K.elem=null}n.removeData(J,"events");n.removeData(J,"handle")}}},trigger:function(H,J,G,D){var F=H.type||H;if(!D){H=typeof H==="object"?H[h]?H:n.extend(n.Event(F),H):n.Event(F);if(F.indexOf("!")>=0){H.type=F=F.slice(0,-1);H.exclusive=true}if(!G){H.stopPropagation();if(this.global[F]){n.each(n.cache,function(){if(this.events&&this.events[F]){n.event.trigger(H,J,this.handle.elem)}})}}if(!G||G.nodeType==3||G.nodeType==8){return g}H.result=g;H.target=G;J=n.makeArray(J);J.unshift(H)}H.currentTarget=G;var I=n.data(G,"handle");if(I){I.apply(G,J)}if((!G[F]||(n.nodeName(G,"a")&&F=="click"))&&G["on"+F]&&G["on"+F].apply(G,J)===false){H.result=false}if(!D&&G[F]&&!H.isDefaultPrevented()&&!(n.nodeName(G,"a")&&F=="click")){this.triggered=true;try{G[F]()}catch(K){}}this.triggered=false;if(!H.isPropagationStopped()){var E=G.parentNode||G.ownerDocument;if(E){n.event.trigger(H,J,E,true)}}},handle:function(J){var I,D;J=arguments[0]=n.event.fix(J||l.event);var K=J.type.split(".");J.type=K.shift();I=!K.length&&!J.exclusive;var H=RegExp("(^|\\.)"+K.slice().sort().join(".*\\.")+"(\\.|$)");D=(n.data(this,"events")||{})[J.type];for(var F in D){var G=D[F];if(I||H.test(G.type)){J.handler=G;J.data=G.data;var E=G.apply(this,arguments);if(E!==g){J.result=E;if(E===false){J.preventDefault();J.stopPropagation()}}if(J.isImmediatePropagationStopped()){break}}}},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(G){if(G[h]){return G}var E=G;G=n.Event(E);for(var F=this.props.length,I;F;){I=this.props[--F];G[I]=E[I]}if(!G.target){G.target=G.srcElement||document}if(G.target.nodeType==3){G.target=G.target.parentNode}if(!G.relatedTarget&&G.fromElement){G.relatedTarget=G.fromElement==G.target?G.toElement:G.fromElement}if(G.pageX==null&&G.clientX!=null){var H=document.documentElement,D=document.body;G.pageX=G.clientX+(H&&H.scrollLeft||D&&D.scrollLeft||0)-(H.clientLeft||0);G.pageY=G.clientY+(H&&H.scrollTop||D&&D.scrollTop||0)-(H.clientTop||0)}if(!G.which&&((G.charCode||G.charCode===0)?G.charCode:G.keyCode)){G.which=G.charCode||G.keyCode}if(!G.metaKey&&G.ctrlKey){G.metaKey=G.ctrlKey}if(!G.which&&G.button){G.which=(G.button&1?1:(G.button&2?3:(G.button&4?2:0)))}return G},proxy:function(E,D){D=D||function(){return E.apply(this,arguments)};D.guid=E.guid=E.guid||D.guid||this.guid++;return D},special:{ready:{setup:A,teardown:function(){}}},specialAll:{live:{setup:function(D,E){n.event.add(this,E[0],c)},teardown:function(F){if(F.length){var D=0,E=RegExp("(^|\\.)"+F[0]+"(\\.|$)");n.each((n.data(this,"events").live||{}),function(){if(E.test(this.type)){D++}});if(D<1){n.event.remove(this,F[0],c)}}}}}};n.Event=function(D){if(!this.preventDefault){return new n.Event(D)}if(D&&D.type){this.originalEvent=D;this.type=D.type;this.timeStamp=D.timeStamp}else{this.type=D}if(!this.timeStamp){this.timeStamp=e()}this[h]=true};function k(){return false}function t(){return true}n.Event.prototype={preventDefault:function(){this.isDefaultPrevented=t;var D=this.originalEvent;if(!D){return}if(D.preventDefault){D.preventDefault()}D.returnValue=false},stopPropagation:function(){this.isPropagationStopped=t;var D=this.originalEvent;if(!D){return}if(D.stopPropagation){D.stopPropagation()}D.cancelBubble=true},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=t;this.stopPropagation()},isDefaultPrevented:k,isPropagationStopped:k,isImmediatePropagationStopped:k};var a=function(E){var D=E.relatedTarget;while(D&&D!=this){try{D=D.parentNode}catch(F){D=this}}if(D!=this){E.type=E.data;n.event.handle.apply(this,arguments)}};n.each({mouseover:"mouseenter",mouseout:"mouseleave"},function(E,D){n.event.special[D]={setup:function(){n.event.add(this,E,a,D)},teardown:function(){n.event.remove(this,E,a)}}});n.fn.extend({bind:function(E,F,D){return E=="unload"?this.one(E,F,D):this.each(function(){n.event.add(this,E,D||F,D&&F)})},one:function(F,G,E){var D=n.event.proxy(E||G,function(H){n(this).unbind(H,D);return(E||G).apply(this,arguments)});return this.each(function(){n.event.add(this,F,D,E&&G)})},unbind:function(E,D){return this.each(function(){n.event.remove(this,E,D)})},trigger:function(D,E){return this.each(function(){n.event.trigger(D,E,this)})},triggerHandler:function(D,F){if(this[0]){var E=n.Event(D);E.preventDefault();E.stopPropagation();n.event.trigger(E,F,this[0]);return E.result}},toggle:function(F){var D=arguments,E=1;while(E<D.length){n.event.proxy(F,D[E++])}return this.click(n.event.proxy(F,function(G){this.lastToggle=(this.lastToggle||0)%E;G.preventDefault();return D[this.lastToggle++].apply(this,arguments)||false}))},hover:function(D,E){return this.mouseenter(D).mouseleave(E)},ready:function(D){A();if(n.isReady){D.call(document,n)}else{n.readyList.push(D)}return this},live:function(F,E){var D=n.event.proxy(E);D.guid+=this.selector+F;n(document).bind(i(F,this.selector),this.selector,D);return this},die:function(E,D){n(document).unbind(i(E,this.selector),D?{guid:D.guid+this.selector+E}:null);return this}});function c(G){var D=RegExp("(^|\\.)"+G.type+"(\\.|$)"),F=true,E=[];n.each(n.data(this,"events").live||[],function(H,I){if(D.test(I.type)){var J=n(G.target).closest(I.data)[0];if(J){E.push({elem:J,fn:I})}}});n.each(E,function(){if(!G.isImmediatePropagationStopped()&&this.fn.call(this.elem,G,this.fn.data)===false){F=false}});return F}function i(E,D){return["live",E,D.replace(/\./g,"`").replace(/ /g,"|")].join(".")}n.extend({isReady:false,readyList:[],ready:function(){if(!n.isReady){n.isReady=true;if(n.readyList){n.each(n.readyList,function(){this.call(document,n)});n.readyList=null}n(document).triggerHandler("ready")}}});var w=false;function A(){if(w){return}w=true;if(document.addEventListener){document.addEventListener("DOMContentLoaded",function(){document.removeEventListener("DOMContentLoaded",arguments.callee,false);n.ready()},false)}else{if(document.attachEvent){document.attachEvent("onreadystatechange",function(){if(document.readyState==="complete"){document.detachEvent("onreadystatechange",arguments.callee);n.ready()}});if(document.documentElement.doScroll&&!l.frameElement){(function(){if(n.isReady){return}try{document.documentElement.doScroll("left")}catch(D){setTimeout(arguments.callee,0);return}n.ready()})()}}}n.event.add(l,"load",n.ready)}n.each(("blur,focus,load,resize,scroll,unload,click,dblclick,mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave,change,select,submit,keydown,keypress,keyup,error").split(","),function(E,D){n.fn[D]=function(F){return F?this.bind(D,F):this.trigger(D)}});n(l).bind("unload",function(){for(var D in n.cache){if(D!=1&&n.cache[D].handle){n.event.remove(n.cache[D].handle.elem)}}});(function(){n.support={};var E=document.documentElement,F=document.createElement("script"),J=document.createElement("div"),I="script"+(new Date).getTime();J.style.display="none";J.innerHTML=' <link/><table></table><a href="/a" style="color:red;float:left;opacity:.5;">a</a><select><option>text</option></select><object><param/></object>';var G=J.getElementsByTagName("*"),D=J.getElementsByTagName("a")[0];if(!G||!G.length||!D){return}n.support={leadingWhitespace:J.firstChild.nodeType==3,tbody:!J.getElementsByTagName("tbody").length,objectAll:!!J.getElementsByTagName("object")[0].getElementsByTagName("*").length,htmlSerialize:!!J.getElementsByTagName("link").length,style:/red/.test(D.getAttribute("style")),hrefNormalized:D.getAttribute("href")==="/a",opacity:D.style.opacity==="0.5",cssFloat:!!D.style.cssFloat,scriptEval:false,noCloneEvent:true,boxModel:null};F.type="text/javascript";try{F.appendChild(document.createTextNode("window."+I+"=1;"))}catch(H){}E.insertBefore(F,E.firstChild);if(l[I]){n.support.scriptEval=true;delete l[I]}E.removeChild(F);if(J.attachEvent&&J.fireEvent){J.attachEvent("onclick",function(){n.support.noCloneEvent=false;J.detachEvent("onclick",arguments.callee)});J.cloneNode(true).fireEvent("onclick")}n(function(){var K=document.createElement("div");K.style.width="1px";K.style.paddingLeft="1px";document.body.appendChild(K);n.boxModel=n.support.boxModel=K.offsetWidth===2;document.body.removeChild(K)})})();var v=n.support.cssFloat?"cssFloat":"styleFloat";n.props={"for":"htmlFor","class":"className","float":v,cssFloat:v,styleFloat:v,readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",tabindex:"tabIndex"};n.fn.extend({_load:n.fn.load,load:function(F,I,J){if(typeof F!=="string"){return this._load(F)}var H=F.indexOf(" ");if(H>=0){var D=F.slice(H,F.length);F=F.slice(0,H)}var G="GET";if(I){if(n.isFunction(I)){J=I;I=null}else{if(typeof I==="object"){I=n.param(I);G="POST"}}}var E=this;n.ajax({url:F,type:G,dataType:"html",data:I,complete:function(L,K){if(K=="success"||K=="notmodified"){E.html(D?n("<div/>").append(L.responseText.replace(/<script(.|\s)*?\/script>/g,"")).find(D):L.responseText)}if(J){E.each(J,[L.responseText,K,L])}}});return this},serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?n.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type))}).map(function(D,E){var F=n(this).val();return F==null?null:n.isArray(F)?n.map(F,function(H,G){return{name:E.name,value:H}}):{name:E.name,value:F}}).get()}});n.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(D,E){n.fn[E]=function(F){return this.bind(E,F)}});var q=e();n.extend({get:function(D,F,G,E){if(n.isFunction(F)){G=F;F=null}return n.ajax({type:"GET",url:D,data:F,success:G,dataType:E})},getScript:function(D,E){return n.get(D,null,E,"script")},getJSON:function(D,E,F){return n.get(D,E,F,"json")},post:function(D,F,G,E){if(n.isFunction(F)){G=F;F={}}return n.ajax({type:"POST",url:D,data:F,success:G,dataType:E})},ajaxSetup:function(D){n.extend(n.ajaxSettings,D)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return l.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest()},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(L){L=n.extend(true,L,n.extend(true,{},n.ajaxSettings,L));var V,E=/=\?(&|$)/g,Q,U,F=L.type.toUpperCase();if(L.data&&L.processData&&typeof L.data!=="string"){L.data=n.param(L.data)}if(L.dataType=="jsonp"){if(F=="GET"){if(!L.url.match(E)){L.url+=(L.url.match(/\?/)?"&":"?")+(L.jsonp||"callback")+"=?"}}else{if(!L.data||!L.data.match(E)){L.data=(L.data?L.data+"&":"")+(L.jsonp||"callback")+"=?"}}L.dataType="json"}if(L.dataType=="json"&&(L.data&&L.data.match(E)||L.url.match(E))){V="jsonp"+q++;if(L.data){L.data=(L.data+"").replace(E,"="+V+"$1")}L.url=L.url.replace(E,"="+V+"$1");L.dataType="script";l[V]=function(W){U=W;H();K();l[V]=g;try{delete l[V]}catch(X){}if(G){G.removeChild(S)}}}if(L.dataType=="script"&&L.cache==null){L.cache=false}if(L.cache===false&&F=="GET"){var D=e();var T=L.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+D+"$2");L.url=T+((T==L.url)?(L.url.match(/\?/)?"&":"?")+"_="+D:"")}if(L.data&&F=="GET"){L.url+=(L.url.match(/\?/)?"&":"?")+L.data;L.data=null}if(L.global&&!n.active++){n.event.trigger("ajaxStart")}var P=/^(\w+:)?\/\/([^\/?#]+)/.exec(L.url);if(L.dataType=="script"&&F=="GET"&&P&&(P[1]&&P[1]!=location.protocol||P[2]!=location.host)){var G=document.getElementsByTagName("head")[0];var S=document.createElement("script");S.src=L.url;if(L.scriptCharset){S.charset=L.scriptCharset}if(!V){var N=false;S.onload=S.onreadystatechange=function(){if(!N&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){N=true;H();K();G.removeChild(S)}}}G.appendChild(S);return g}var J=false;var I=L.xhr();if(L.username){I.open(F,L.url,L.async,L.username,L.password)}else{I.open(F,L.url,L.async)}try{if(L.data){I.setRequestHeader("Content-Type",L.contentType)}if(L.ifModified){I.setRequestHeader("If-Modified-Since",n.lastModified[L.url]||"Thu, 01 Jan 1970 00:00:00 GMT")}I.setRequestHeader("X-Requested-With","XMLHttpRequest");I.setRequestHeader("Accept",L.dataType&&L.accepts[L.dataType]?L.accepts[L.dataType]+", */*":L.accepts._default)}catch(R){}if(L.beforeSend&&L.beforeSend(I,L)===false){if(L.global&&!--n.active){n.event.trigger("ajaxStop")}I.abort();return false}if(L.global){n.event.trigger("ajaxSend",[I,L])}var M=function(W){if(I.readyState==0){if(O){clearInterval(O);O=null;if(L.global&&!--n.active){n.event.trigger("ajaxStop")}}}else{if(!J&&I&&(I.readyState==4||W=="timeout")){J=true;if(O){clearInterval(O);O=null}Q=W=="timeout"?"timeout":!n.httpSuccess(I)?"error":L.ifModified&&n.httpNotModified(I,L.url)?"notmodified":"success";if(Q=="success"){try{U=n.httpData(I,L.dataType,L)}catch(Y){Q="parsererror"}}if(Q=="success"){var X;try{X=I.getResponseHeader("Last-Modified")}catch(Y){}if(L.ifModified&&X){n.lastModified[L.url]=X}if(!V){H()}}else{n.handleError(L,I,Q)}K();if(L.async){I=null}}}};if(L.async){var O=setInterval(M,13);if(L.timeout>0){setTimeout(function(){if(I){if(!J){M("timeout")}if(I){I.abort()}}},L.timeout)}}try{I.send(L.data)}catch(R){n.handleError(L,I,null,R)}if(!L.async){M()}function H(){if(L.success){L.success(U,Q)}if(L.global){n.event.trigger("ajaxSuccess",[I,L])}}function K(){if(L.complete){L.complete(I,Q)}if(L.global){n.event.trigger("ajaxComplete",[I,L])}if(L.global&&!--n.active){n.event.trigger("ajaxStop")}}return I},handleError:function(E,G,D,F){if(E.error){E.error(G,D,F)}if(E.global){n.event.trigger("ajaxError",[G,E,F])}},active:0,httpSuccess:function(E){try{return !E.status&&location.protocol=="file:"||(E.status>=200&&E.status<300)||E.status==304||E.status==1223}catch(D){}return false},httpNotModified:function(F,D){try{var G=F.getResponseHeader("Last-Modified");return F.status==304||G==n.lastModified[D]}catch(E){}return false},httpData:function(I,G,F){var E=I.getResponseHeader("content-type"),D=G=="xml"||!G&&E&&E.indexOf("xml")>=0,H=D?I.responseXML:I.responseText;if(D&&H.documentElement.tagName=="parsererror"){throw"parsererror"}if(F&&F.dataFilter){H=F.dataFilter(H,G)}if(typeof H==="string"){if(G=="script"){n.globalEval(H)}if(G=="json"){H=l["eval"]("("+H+")")}}return H},param:function(D){var F=[];function G(H,I){F[F.length]=encodeURIComponent(H)+"="+encodeURIComponent(I)}if(n.isArray(D)||D.jquery){n.each(D,function(){G(this.name,this.value)})}else{for(var E in D){if(n.isArray(D[E])){n.each(D[E],function(){G(E,this)})}else{G(E,n.isFunction(D[E])?D[E]():D[E])}}}return F.join("&").replace(/%20/g,"+")}});var m={},d=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];function s(E,D){var F={};n.each(d.concat.apply([],d.slice(0,D)),function(){F[this]=E});return F}n.fn.extend({show:function(I,K){if(I){return this.animate(s("show",3),I,K)}else{for(var G=0,E=this.length;G<E;G++){var D=n.data(this[G],"olddisplay");this[G].style.display=D||"";if(n.css(this[G],"display")==="none"){var F=this[G].tagName,J;if(m[F]){J=m[F]}else{var H=n("<"+F+" />").appendTo("body");J=H.css("display");if(J==="none"){J="block"}H.remove();m[F]=J}this[G].style.display=n.data(this[G],"olddisplay",J)}}return this}},hide:function(G,H){if(G){return this.animate(s("hide",3),G,H)}else{for(var F=0,E=this.length;F<E;F++){var D=n.data(this[F],"olddisplay");if(!D&&D!=="none"){n.data(this[F],"olddisplay",n.css(this[F],"display"))}this[F].style.display="none"}return this}},_toggle:n.fn.toggle,toggle:function(F,E){var D=typeof F==="boolean";return n.isFunction(F)&&n.isFunction(E)?this._toggle.apply(this,arguments):F==null||D?this.each(function(){var G=D?F:n(this).is(":hidden");n(this)[G?"show":"hide"]()}):this.animate(s("toggle",3),F,E)},fadeTo:function(D,F,E){return this.animate({opacity:F},D,E)},animate:function(H,E,G,F){var D=n.speed(E,G,F);return this[D.queue===false?"each":"queue"](function(){var J=n.extend({},D),L,K=this.nodeType==1&&n(this).is(":hidden"),I=this;for(L in H){if(H[L]=="hide"&&K||H[L]=="show"&&!K){return J.complete.call(this)}if((L=="height"||L=="width")&&this.style){J.display=n.css(this,"display");J.overflow=this.style.overflow}}if(J.overflow!=null){this.style.overflow="hidden"}J.curAnim=n.extend({},H);n.each(H,function(N,R){var Q=new n.fx(I,J,N);if(/toggle|show|hide/.test(R)){Q[R=="toggle"?K?"show":"hide":R](H)}else{var P=R.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),S=Q.cur(true)||0;if(P){var M=parseFloat(P[2]),O=P[3]||"px";if(O!="px"){I.style[N]=(M||1)+O;S=((M||1)/Q.cur(true))*S;I.style[N]=S+O}if(P[1]){M=((P[1]=="-="?-1:1)*M)+S}Q.custom(S,M,O)}else{Q.custom(S,R,"")}}});return true})},stop:function(E,D){var F=n.timers;if(E){this.queue([])}this.each(function(){for(var G=F.length-1;G>=0;G--){if(F[G].elem==this){if(D){F[G](true)}F.splice(G,1)}}});if(!D){this.dequeue()}return this}});n.each({slideDown:s("show",1),slideUp:s("hide",1),slideToggle:s("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(D,E){n.fn[D]=function(F,G){return this.animate(E,F,G)}});n.extend({speed:function(F,G,E){var D=typeof F==="object"?F:{complete:E||!E&&G||n.isFunction(F)&&F,duration:F,easing:E&&G||G&&!n.isFunction(G)&&G};D.duration=n.fx.off?0:typeof D.duration==="number"?D.duration:n.fx.speeds[D.duration]||n.fx.speeds._default;D.old=D.complete;D.complete=function(){if(D.queue!==false){n(this).dequeue()}if(n.isFunction(D.old)){D.old.call(this)}};return D},easing:{linear:function(F,G,D,E){return D+E*F},swing:function(F,G,D,E){return((-Math.cos(F*Math.PI)/2)+0.5)*E+D}},timers:[],timerId:null,fx:function(E,D,F){this.options=D;this.elem=E;this.prop=F;if(!D.orig){D.orig={}}}});n.fx.prototype={update:function(){if(this.options.step){this.options.step.call(this.elem,this.now,this)}(n.fx.step[this.prop]||n.fx.step._default)(this);if((this.prop=="height"||this.prop=="width")&&this.elem.style){this.elem.style.display="block"}},cur:function(E){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null)){return this.elem[this.prop]}var D=parseFloat(n.css(this.elem,this.prop,E));return D&&D>-10000?D:parseFloat(n.curCSS(this.elem,this.prop))||0},custom:function(H,G,F){this.startTime=e();this.start=H;this.end=G;this.unit=F||this.unit||"px";this.now=this.start;this.pos=this.state=0;var D=this;function E(I){return D.step(I)}E.elem=this.elem;n.timers.push(E);if(E()&&n.timerId==null){n.timerId=setInterval(function(){var J=n.timers;for(var I=0;I<J.length;I++){if(!J[I]()){J.splice(I--,1)}}if(!J.length){clearInterval(n.timerId);n.timerId=null}},13)}},show:function(){this.options.orig[this.prop]=n.attr(this.elem.style,this.prop);this.options.show=true;this.custom(this.prop=="width"||this.prop=="height"?1:0,this.cur());n(this.elem).show()},hide:function(){this.options.orig[this.prop]=n.attr(this.elem.style,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(G){var F=e();if(G||F>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var D=true;for(var E in this.options.curAnim){if(this.options.curAnim[E]!==true){D=false}}if(D){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(n.css(this.elem,"display")=="none"){this.elem.style.display="block"}}if(this.options.hide){n(this.elem).hide()}if(this.options.hide||this.options.show){for(var H in this.options.curAnim){n.attr(this.elem.style,H,this.options.orig[H])}}}if(D){this.options.complete.call(this.elem)}return false}else{var I=F-this.startTime;this.state=I/this.options.duration;this.pos=n.easing[this.options.easing||(n.easing.swing?"swing":"linear")](this.state,I,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update()}return true}};n.extend(n.fx,{speeds:{slow:600,fast:200,_default:400},step:{opacity:function(D){n.attr(D.elem.style,"opacity",D.now)},_default:function(D){if(D.elem.style&&D.elem.style[D.prop]!=null){D.elem.style[D.prop]=D.now+D.unit}else{D.elem[D.prop]=D.now}}}});if(document.documentElement.getBoundingClientRect){n.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return n.offset.bodyOffset(this[0])}var F=this[0].getBoundingClientRect(),I=this[0].ownerDocument,E=I.body,D=I.documentElement,K=D.clientTop||E.clientTop||0,J=D.clientLeft||E.clientLeft||0,H=F.top+(self.pageYOffset||n.boxModel&&D.scrollTop||E.scrollTop)-K,G=F.left+(self.pageXOffset||n.boxModel&&D.scrollLeft||E.scrollLeft)-J;return{top:H,left:G}}}else{n.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return n.offset.bodyOffset(this[0])}n.offset.initialized||n.offset.initialize();var I=this[0],F=I.offsetParent,E=I,N=I.ownerDocument,L,G=N.documentElement,J=N.body,K=N.defaultView,D=K.getComputedStyle(I,null),M=I.offsetTop,H=I.offsetLeft;while((I=I.parentNode)&&I!==J&&I!==G){L=K.getComputedStyle(I,null);M-=I.scrollTop,H-=I.scrollLeft;if(I===F){M+=I.offsetTop,H+=I.offsetLeft;if(n.offset.doesNotAddBorder&&!(n.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(I.tagName))){M+=parseInt(L.borderTopWidth,10)||0,H+=parseInt(L.borderLeftWidth,10)||0}E=F,F=I.offsetParent}if(n.offset.subtractsBorderForOverflowNotVisible&&L.overflow!=="visible"){M+=parseInt(L.borderTopWidth,10)||0,H+=parseInt(L.borderLeftWidth,10)||0}D=L}if(D.position==="relative"||D.position==="static"){M+=J.offsetTop,H+=J.offsetLeft}if(D.position==="fixed"){M+=Math.max(G.scrollTop,J.scrollTop),H+=Math.max(G.scrollLeft,J.scrollLeft)}return{top:M,left:H}}}n.offset={initialize:function(){if(this.initialized){return}var K=document.body,E=document.createElement("div"),G,F,M,H,L,D,I=K.style.marginTop,J='<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"cellpadding="0"cellspacing="0"><tr><td></td></tr></table>';L={position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"};for(D in L){E.style[D]=L[D]}E.innerHTML=J;K.insertBefore(E,K.firstChild);G=E.firstChild,F=G.firstChild,H=G.nextSibling.firstChild.firstChild;this.doesNotAddBorder=(F.offsetTop!==5);this.doesAddBorderForTableAndCells=(H.offsetTop===5);G.style.overflow="hidden",G.style.position="relative";this.subtractsBorderForOverflowNotVisible=(F.offsetTop===-5);K.style.marginTop="1px";this.doesNotIncludeMarginInBodyOffset=(K.offsetTop===0);K.style.marginTop=I;K.removeChild(E);this.initialized=true},bodyOffset:function(D){n.offset.initialized||n.offset.initialize();var F=D.offsetTop,E=D.offsetLeft;if(n.offset.doesNotIncludeMarginInBodyOffset){F+=parseInt(n.curCSS(D,"marginTop",true),10)||0,E+=parseInt(n.curCSS(D,"marginLeft",true),10)||0}return{top:F,left:E}}};n.fn.extend({position:function(){var H=0,G=0,E;if(this[0]){var F=this.offsetParent(),I=this.offset(),D=/^body|html$/i.test(F[0].tagName)?{top:0,left:0}:F.offset();I.top-=j(this,"marginTop");I.left-=j(this,"marginLeft");D.top+=j(F,"borderTopWidth");D.left+=j(F,"borderLeftWidth");E={top:I.top-D.top,left:I.left-D.left}}return E},offsetParent:function(){var D=this[0].offsetParent||document.body;while(D&&(!/^body|html$/i.test(D.tagName)&&n.css(D,"position")=="static")){D=D.offsetParent}return n(D)}});n.each(["Left","Top"],function(E,D){var F="scroll"+D;n.fn[F]=function(G){if(!this[0]){return null}return G!==g?this.each(function(){this==l||this==document?l.scrollTo(!E?G:n(l).scrollLeft(),E?G:n(l).scrollTop()):this[F]=G}):this[0]==l||this[0]==document?self[E?"pageYOffset":"pageXOffset"]||n.boxModel&&document.documentElement[F]||document.body[F]:this[0][F]}});n.each(["Height","Width"],function(G,E){var D=G?"Left":"Top",F=G?"Right":"Bottom";n.fn["inner"+E]=function(){return this[E.toLowerCase()]()+j(this,"padding"+D)+j(this,"padding"+F)};n.fn["outer"+E]=function(I){return this["inner"+E]()+j(this,"border"+D+"Width")+j(this,"border"+F+"Width")+(I?j(this,"margin"+D)+j(this,"margin"+F):0)};var H=E.toLowerCase();n.fn[H]=function(I){return this[0]==l?document.compatMode=="CSS1Compat"&&document.documentElement["client"+E]||document.body["client"+E]:this[0]==document?Math.max(document.documentElement["client"+E],document.body["scroll"+E],document.documentElement["scroll"+E],document.body["offset"+E],document.documentElement["offset"+E]):I===g?(this.length?n.css(this[0],H):null):this.css(H,typeof I==="string"?I:I+"px")}})})(); diff --git a/devtools/client/inspector/markup/test/lib_jquery_1.4_min.js b/devtools/client/inspector/markup/test/lib_jquery_1.4_min.js new file mode 100644 index 000000000..5c70e4c5f --- /dev/null +++ b/devtools/client/inspector/markup/test/lib_jquery_1.4_min.js @@ -0,0 +1,151 @@ +/*! + * jQuery JavaScript Library v1.4 + * http://jquery.com/ + * + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://docs.jquery.com/License + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2010, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Wed Jan 13 15:23:05 2010 -0500 + */ +(function(A,w){function oa(){if(!c.isReady){try{s.documentElement.doScroll("left")}catch(a){setTimeout(oa,1);return}c.ready()}}function La(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function $(a,b,d,f,e,i){var j=a.length;if(typeof b==="object"){for(var o in b)$(a,o,b[o],f,e,d);return a}if(d!==w){f=!i&&f&&c.isFunction(d);for(o=0;o<j;o++)e(a[o],b,f?d.call(a[o],o,e(a[o],b)):d,i);return a}return j? +e(a[0],b):null}function K(){return(new Date).getTime()}function aa(){return false}function ba(){return true}function pa(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function qa(a){var b=true,d=[],f=[],e=arguments,i,j,o,p,n,t=c.extend({},c.data(this,"events").live);for(p in t){j=t[p];if(j.live===a.type||j.altLive&&c.inArray(a.type,j.altLive)>-1){i=j.data;i.beforeFilter&&i.beforeFilter[a.type]&&!i.beforeFilter[a.type](a)||f.push(j.selector)}else delete t[p]}i=c(a.target).closest(f,a.currentTarget); +n=0;for(l=i.length;n<l;n++)for(p in t){j=t[p];o=i[n].elem;f=null;if(i[n].selector===j.selector){if(j.live==="mouseenter"||j.live==="mouseleave")f=c(a.relatedTarget).closest(j.selector)[0];if(!f||f!==o)d.push({elem:o,fn:j})}}n=0;for(l=d.length;n<l;n++){i=d[n];a.currentTarget=i.elem;a.data=i.fn.data;if(i.fn.apply(i.elem,e)===false){b=false;break}}return b}function ra(a,b){return["live",a,b.replace(/\./g,"`").replace(/ /g,"&")].join(".")}function sa(a){return!a||!a.parentNode||a.parentNode.nodeType=== +11}function ta(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var f=c.data(a[d++]),e=c.data(this,f);if(f=f&&f.events){delete e.handle;e.events={};for(var i in f)for(var j in f[i])c.event.add(this,i,f[i][j],f[i][j].data)}}})}function ua(a,b,d){var f,e,i;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&a[0].indexOf("<option")<0){e=true;if(i=c.fragments[a[0]])if(i!==1)f=i}if(!f){b=b&&b[0]?b[0].ownerDocument||b[0]:s;f=b.createDocumentFragment();c.clean(a,b,f,d)}if(e)c.fragments[a[0]]= +i?f:1;return{fragment:f,cacheable:e}}function T(a){for(var b=0,d,f;(d=a[b])!=null;b++)if(!c.noData[d.nodeName.toLowerCase()]&&(f=d[H]))delete c.cache[f]}function L(a,b){var d={};c.each(va.concat.apply([],va.slice(0,b)),function(){d[this]=a});return d}function wa(a){return"scrollTo"in a&&a.document?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var c=function(a,b){return new c.fn.init(a,b)},Ma=A.jQuery,Na=A.$,s=A.document,U,Oa=/^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/,Pa=/^.[^:#\[\.,]*$/,Qa=/\S/, +Ra=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Sa=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,P=navigator.userAgent,xa=false,Q=[],M,ca=Object.prototype.toString,da=Object.prototype.hasOwnProperty,ea=Array.prototype.push,R=Array.prototype.slice,V=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(typeof a==="string")if((d=Oa.exec(a))&&(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:s;if(a=Sa.exec(a))if(c.isPlainObject(b)){a=[s.createElement(a[1])]; +c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=ua([d[1]],[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}}else{if(b=s.getElementById(d[2])){if(b.id!==d[2])return U.find(a);this.length=1;this[0]=b}this.context=s;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=s;a=s.getElementsByTagName(a)}else return!b||b.jquery?(b||U).find(a):c(b).find(a);else if(c.isFunction(a))return U.ready(a);if(a.selector!==w){this.selector=a.selector; +this.context=a.context}return c.isArray(a)?this.setArray(a):c.makeArray(a,this)},selector:"",jquery:"1.4",length:0,size:function(){return this.length},toArray:function(){return R.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){a=c(a||null);a.prevObject=this;a.context=this.context;if(b==="find")a.selector=this.selector+(this.selector?" ":"")+d;else if(b)a.selector=this.selector+"."+b+"("+d+")";return a},setArray:function(a){this.length= +0;ea.apply(this,a);return this},each:function(a,b){return c.each(this,a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(s,c);else Q&&Q.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(R.apply(this,arguments),"slice",R.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this,function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject|| +c(null)},push:ea,sort:[].sort,splice:[].splice};c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,i,j,o;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b<d;b++)if((e=arguments[b])!=null)for(i in e){j=a[i];o=e[i];if(a!==o)if(f&&o&&(c.isPlainObject(o)||c.isArray(o))){j=j&&(c.isPlainObject(j)||c.isArray(j))?j:c.isArray(o)?[]:{};a[i]=c.extend(f,j,o)}else if(o!==w)a[i]= +o}return a};c.extend({noConflict:function(a){A.$=Na;if(a)A.jQuery=Ma;return c},isReady:false,ready:function(){if(!c.isReady){if(!s.body)return setTimeout(c.ready,13);c.isReady=true;if(Q){for(var a,b=0;a=Q[b++];)a.call(s,c);Q=null}c.fn.triggerHandler&&c(s).triggerHandler("ready")}},bindReady:function(){if(!xa){xa=true;if(s.readyState==="complete")return c.ready();if(s.addEventListener){s.addEventListener("DOMContentLoaded",M,false);A.addEventListener("load",c.ready,false)}else if(s.attachEvent){s.attachEvent("onreadystatechange", +M);A.attachEvent("onload",c.ready);var a=false;try{a=A.frameElement==null}catch(b){}s.documentElement.doScroll&&a&&oa()}}},isFunction:function(a){return ca.call(a)==="[object Function]"},isArray:function(a){return ca.call(a)==="[object Array]"},isPlainObject:function(a){if(!a||ca.call(a)!=="[object Object]"||a.nodeType||a.setInterval)return false;if(a.constructor&&!da.call(a,"constructor")&&!da.call(a.constructor.prototype,"isPrototypeOf"))return false;var b;for(b in a);return b===w||da.call(a,b)}, +isEmptyObject:function(a){for(var b in a)return false;return true},noop:function(){},globalEval:function(a){if(a&&Qa.test(a)){var b=s.getElementsByTagName("head")[0]||s.documentElement,d=s.createElement("script");d.type="text/javascript";if(c.support.scriptEval)d.appendChild(s.createTextNode(a));else d.text=a;b.insertBefore(d,b.firstChild);b.removeChild(d)}},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,b,d){var f,e=0,i=a.length,j=i===w||c.isFunction(a); +if(d)if(j)for(f in a){if(b.apply(a[f],d)===false)break}else for(;e<i;){if(b.apply(a[e++],d)===false)break}else if(j)for(f in a){if(b.call(a[f],f,a[f])===false)break}else for(d=a[0];e<i&&b.call(d,e,d)!==false;d=a[++e]);return a},trim:function(a){return(a||"").replace(Ra,"")},makeArray:function(a,b){b=b||[];if(a!=null)a.length==null||typeof a==="string"||c.isFunction(a)||typeof a!=="function"&&a.setInterval?ea.call(b,a):c.merge(b,a);return b},inArray:function(a,b){if(b.indexOf)return b.indexOf(a);for(var d= +0,f=b.length;d<f;d++)if(b[d]===a)return d;return-1},merge:function(a,b){var d=a.length,f=0;if(typeof b.length==="number")for(var e=b.length;f<e;f++)a[d++]=b[f];else for(;b[f]!==w;)a[d++]=b[f++];a.length=d;return a},grep:function(a,b,d){for(var f=[],e=0,i=a.length;e<i;e++)!d!==!b(a[e],e)&&f.push(a[e]);return f},map:function(a,b,d){for(var f=[],e,i=0,j=a.length;i<j;i++){e=b(a[i],i,d);if(e!=null)f[f.length]=e}return f.concat.apply([],f)},guid:1,proxy:function(a,b,d){if(arguments.length===2)if(typeof b=== +"string"){d=a;a=d[b];b=w}else if(b&&!c.isFunction(b)){d=b;b=w}if(!b&&a)b=function(){return a.apply(d||this,arguments)};if(a)b.guid=a.guid=a.guid||b.guid||c.guid++;return b},uaMatch:function(a){var b={browser:""};a=a.toLowerCase();if(/webkit/.test(a))b={browser:"webkit",version:/webkit[\/ ]([\w.]+)/};else if(/opera/.test(a))b={browser:"opera",version:/version/.test(a)?/version[\/ ]([\w.]+)/:/opera[\/ ]([\w.]+)/};else if(/msie/.test(a))b={browser:"msie",version:/msie ([\w.]+)/};else if(/mozilla/.test(a)&& +!/compatible/.test(a))b={browser:"mozilla",version:/rv:([\w.]+)/};b.version=(b.version&&b.version.exec(a)||[0,"0"])[1];return b},browser:{}});P=c.uaMatch(P);if(P.browser){c.browser[P.browser]=true;c.browser.version=P.version}if(c.browser.webkit)c.browser.safari=true;if(V)c.inArray=function(a,b){return V.call(b,a)};U=c(s);if(s.addEventListener)M=function(){s.removeEventListener("DOMContentLoaded",M,false);c.ready()};else if(s.attachEvent)M=function(){if(s.readyState==="complete"){s.detachEvent("onreadystatechange", +M);c.ready()}};if(V)c.inArray=function(a,b){return V.call(b,a)};(function(){c.support={};var a=s.documentElement,b=s.createElement("script"),d=s.createElement("div"),f="script"+K();d.style.display="none";d.innerHTML=" <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";var e=d.getElementsByTagName("*"),i=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!i)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length, +htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(i.getAttribute("style")),hrefNormalized:i.getAttribute("href")==="/a",opacity:/^0.55$/.test(i.style.opacity),cssFloat:!!i.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:s.createElement("select").appendChild(s.createElement("option")).selected,scriptEval:false,noCloneEvent:true,boxModel:null};b.type="text/javascript";try{b.appendChild(s.createTextNode("window."+f+"=1;"))}catch(j){}a.insertBefore(b, +a.firstChild);if(A[f]){c.support.scriptEval=true;delete A[f]}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function o(){c.support.noCloneEvent=false;d.detachEvent("onclick",o)});d.cloneNode(true).fireEvent("onclick")}c(function(){var o=s.createElement("div");o.style.width=o.style.paddingLeft="1px";s.body.appendChild(o);c.boxModel=c.support.boxModel=o.offsetWidth===2;s.body.removeChild(o).style.display="none"});a=function(o){var p=s.createElement("div");o="on"+o;var n=o in +p;if(!n){p.setAttribute(o,"return;");n=typeof p[o]==="function"}return n};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=i=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var H="jQuery"+K(),Ta=0,ya={},Ua={};c.extend({cache:{},expando:H,noData:{embed:true,object:true,applet:true},data:function(a, +b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?ya:a;var f=a[H],e=c.cache;if(!b&&!f)return null;f||(f=++Ta);if(typeof b==="object"){a[H]=f;e=e[f]=c.extend(true,{},b)}else e=e[f]?e[f]:typeof d==="undefined"?Ua:(e[f]={});if(d!==w){a[H]=f;e[b]=d}return typeof b==="string"?e[b]:e}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?ya:a;var d=a[H],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{try{delete a[H]}catch(i){a.removeAttribute&& +a.removeAttribute(H)}delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===w){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===w&&this.length)f=c.data(this[0],a);return f===w&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this,a,b)})},removeData:function(a){return this.each(function(){c.removeData(this, +a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b===w)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this, +a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var za=/[\n\t]/g,fa=/\s+/,Va=/\r/g,Wa=/href|src|style/,Xa=/(button|input)/i,Ya=/(button|input|object|select|textarea)/i,Za=/^(a|area)$/i,Aa=/radio|checkbox/;c.fn.extend({attr:function(a, +b){return $(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(p){var n=c(this);n.addClass(a.call(this,p,n.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(fa),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1)if(e.className)for(var i=" "+e.className+" ",j=0,o=b.length;j<o;j++){if(i.indexOf(" "+b[j]+" ")<0)e.className+= +" "+b[j]}else e.className=a}return this},removeClass:function(a){if(c.isFunction(a))return this.each(function(p){var n=c(this);n.removeClass(a.call(this,p,n.attr("class")))});if(a&&typeof a==="string"||a===w)for(var b=(a||"").split(fa),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1&&e.className)if(a){for(var i=(" "+e.className+" ").replace(za," "),j=0,o=b.length;j<o;j++)i=i.replace(" "+b[j]+" "," ");e.className=i.substring(1,i.length-1)}else e.className=""}return this},toggleClass:function(a, +b){var d=typeof a,f=typeof b==="boolean";if(c.isFunction(a))return this.each(function(e){var i=c(this);i.toggleClass(a.call(this,e,i.attr("class"),b),b)});return this.each(function(){if(d==="string")for(var e,i=0,j=c(this),o=b,p=a.split(fa);e=p[i++];){o=f?o:!j.hasClass(e);j[o?"addClass":"removeClass"](e)}else if(d==="undefined"||d==="boolean"){this.className&&c.data(this,"__className__",this.className);this.className=this.className||a===false?"":c.data(this,"__className__")||""}})},hasClass:function(a){a= +" "+a+" ";for(var b=0,d=this.length;b<d;b++)if((" "+this[b].className+" ").replace(za," ").indexOf(a)>-1)return true;return false},val:function(a){if(a===w){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var i=b?d:0;for(d=b?d+1:e.length;i<d;i++){var j=e[i];if(j.selected){a=c(j).val();if(b)return a;f.push(a)}}return f}if(Aa.test(b.type)&& +!c.support.checkOn)return b.getAttribute("value")===null?"on":b.value;return(b.value||"").replace(Va,"")}return w}var o=c.isFunction(a);return this.each(function(p){var n=c(this),t=a;if(this.nodeType===1){if(o)t=a.call(this,p,n.val());if(typeof t==="number")t+="";if(c.isArray(t)&&Aa.test(this.type))this.checked=c.inArray(n.val(),t)>=0;else if(c.nodeName(this,"select")){var z=c.makeArray(t);c("option",this).each(function(){this.selected=c.inArray(c(this).val(),z)>=0});if(!z.length)this.selectedIndex= +-1}else this.value=t}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return w;if(f&&b in c.attrFn)return c(a)[b](d);f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==w;b=f&&c.props[b]||b;if(a.nodeType===1){var i=Wa.test(b);if(b in a&&f&&!i){if(e){if(b==="type"&&Xa.test(a.nodeName)&&a.parentNode)throw"type property can't be changed";a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue; +if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:Ya.test(a.nodeName)||Za.test(a.nodeName)&&a.href?0:w;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&i?a.getAttribute(b,2):a.getAttribute(b);return a===null?w:a}return c.style(a,b,d)}});var $a=function(a){return a.replace(/[^\w\s\.\|`]/g,function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType=== +3||a.nodeType===8)){if(a.setInterval&&a!==A&&!a.frameElement)a=A;if(!d.guid)d.guid=c.guid++;if(f!==w){d=c.proxy(d);d.data=f}var e=c.data(a,"events")||c.data(a,"events",{}),i=c.data(a,"handle"),j;if(!i){j=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(j.elem,arguments):w};i=c.data(a,"handle",j)}if(i){i.elem=a;b=b.split(/\s+/);for(var o,p=0;o=b[p++];){var n=o.split(".");o=n.shift();d.type=n.slice(0).sort().join(".");var t=e[o],z=this.special[o]||{};if(!t){t=e[o]={}; +if(!z.setup||z.setup.call(a,f,n,d)===false)if(a.addEventListener)a.addEventListener(o,i,false);else a.attachEvent&&a.attachEvent("on"+o,i)}if(z.add)if((n=z.add.call(a,d,f,n,t))&&c.isFunction(n)){n.guid=n.guid||d.guid;d=n}t[d.guid]=d;this.global[o]=true}a=null}}},global:{},remove:function(a,b,d){if(!(a.nodeType===3||a.nodeType===8)){var f=c.data(a,"events"),e,i,j;if(f){if(b===w||typeof b==="string"&&b.charAt(0)===".")for(i in f)this.remove(a,i+(b||""));else{if(b.type){d=b.handler;b=b.type}b=b.split(/\s+/); +for(var o=0;i=b[o++];){var p=i.split(".");i=p.shift();var n=!p.length,t=c.map(p.slice(0).sort(),$a);t=new RegExp("(^|\\.)"+t.join("\\.(?:.*\\.)?")+"(\\.|$)");var z=this.special[i]||{};if(f[i]){if(d){j=f[i][d.guid];delete f[i][d.guid]}else for(var B in f[i])if(n||t.test(f[i][B].type))delete f[i][B];z.remove&&z.remove.call(a,p,j);for(e in f[i])break;if(!e){if(!z.teardown||z.teardown.call(a,p)===false)if(a.removeEventListener)a.removeEventListener(i,c.data(a,"handle"),false);else a.detachEvent&&a.detachEvent("on"+ +i,c.data(a,"handle"));e=null;delete f[i]}}}}for(e in f)break;if(!e){if(B=c.data(a,"handle"))B.elem=null;c.removeData(a,"events");c.removeData(a,"handle")}}}},trigger:function(a,b,d,f){var e=a.type||a;if(!f){a=typeof a==="object"?a[H]?a:c.extend(c.Event(e),a):c.Event(e);if(e.indexOf("!")>=0){a.type=e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();this.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType=== +8)return w;a.result=w;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;var i=c.data(d,"handle");i&&i.apply(d,b);var j,o;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()])){j=d[e];o=d["on"+e]}}catch(p){}i=c.nodeName(d,"a")&&e==="click";if(!f&&j&&!a.isDefaultPrevented()&&!i){this.triggered=true;try{d[e]()}catch(n){}}else if(o&&d["on"+e].apply(d,b)===false)a.result=false;this.triggered=false;if(!a.isPropagationStopped())(d=d.parentNode||d.ownerDocument)&&c.event.trigger(a,b,d,true)}, +handle:function(a){var b,d;a=arguments[0]=c.event.fix(a||A.event);a.currentTarget=this;d=a.type.split(".");a.type=d.shift();b=!d.length&&!a.exclusive;var f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)");d=(c.data(this,"events")||{})[a.type];for(var e in d){var i=d[e];if(b||f.test(i.type)){a.handler=i;a.data=i.data;i=i.apply(this,arguments);if(i!==w){a.result=i;if(i===false){a.preventDefault();a.stopPropagation()}}if(a.isImmediatePropagationStopped())break}}return a.result}, +props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(a){if(a[H])return a;var b=a;a=c.Event(b);for(var d=this.props.length,f;d;){f=this.props[--d];a[f]=b[f]}if(!a.target)a.target=a.srcElement|| +s;if(a.target.nodeType===3)a.target=a.target.parentNode;if(!a.relatedTarget&&a.fromElement)a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement;if(a.pageX==null&&a.clientX!=null){b=s.documentElement;d=s.body;a.pageX=a.clientX+(b&&b.scrollLeft||d&&d.scrollLeft||0)-(b&&b.clientLeft||d&&d.clientLeft||0);a.pageY=a.clientY+(b&&b.scrollTop||d&&d.scrollTop||0)-(b&&b.clientTop||d&&d.clientTop||0)}if(!a.which&&(a.charCode||a.charCode===0?a.charCode:a.keyCode))a.which=a.charCode||a.keyCode;if(!a.metaKey&& +a.ctrlKey)a.metaKey=a.ctrlKey;if(!a.which&&a.button!==w)a.which=a.button&1?1:a.button&2?3:a.button&4?2:0;return a},guid:1E8,proxy:c.proxy,special:{ready:{setup:c.bindReady,teardown:c.noop},live:{add:function(a,b){c.extend(a,b||{});a.guid+=b.selector+b.live;c.event.add(this,b.live,qa,b)},remove:function(a){if(a.length){var b=0,d=new RegExp("(^|\\.)"+a[0]+"(\\.|$)");c.each(c.data(this,"events").live||{},function(){d.test(this.type)&&b++});b<1&&c.event.remove(this,a[0],qa)}},special:{}},beforeunload:{setup:function(a, +b,d){if(this.setInterval)this.onbeforeunload=d;return false},teardown:function(a,b){if(this.onbeforeunload===b)this.onbeforeunload=null}}}};c.Event=function(a){if(!this.preventDefault)return new c.Event(a);if(a&&a.type){this.originalEvent=a;this.type=a.type}else this.type=a;this.timeStamp=K();this[H]=true};c.Event.prototype={preventDefault:function(){this.isDefaultPrevented=ba;var a=this.originalEvent;if(a){a.preventDefault&&a.preventDefault();a.returnValue=false}},stopPropagation:function(){this.isPropagationStopped= +ba;var a=this.originalEvent;if(a){a.stopPropagation&&a.stopPropagation();a.cancelBubble=true}},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=ba;this.stopPropagation()},isDefaultPrevented:aa,isPropagationStopped:aa,isImmediatePropagationStopped:aa};var Ba=function(a){for(var b=a.relatedTarget;b&&b!==this;)try{b=b.parentNode}catch(d){break}if(b!==this){a.type=a.data;c.event.handle.apply(this,arguments)}},Ca=function(a){a.type=a.data;c.event.handle.apply(this,arguments)};c.each({mouseenter:"mouseover", +mouseleave:"mouseout"},function(a,b){c.event.special[a]={setup:function(d){c.event.add(this,b,d&&d.selector?Ca:Ba,a)},teardown:function(d){c.event.remove(this,b,d&&d.selector?Ca:Ba)}}});if(!c.support.submitBubbles)c.event.special.submit={setup:function(a,b,d){if(this.nodeName.toLowerCase()!=="form"){c.event.add(this,"click.specialSubmit."+d.guid,function(f){var e=f.target,i=e.type;if((i==="submit"||i==="image")&&c(e).closest("form").length)return pa("submit",this,arguments)});c.event.add(this,"keypress.specialSubmit."+ +d.guid,function(f){var e=f.target,i=e.type;if((i==="text"||i==="password")&&c(e).closest("form").length&&f.keyCode===13)return pa("submit",this,arguments)})}else return false},remove:function(a,b){c.event.remove(this,"click.specialSubmit"+(b?"."+b.guid:""));c.event.remove(this,"keypress.specialSubmit"+(b?"."+b.guid:""))}};if(!c.support.changeBubbles){var ga=/textarea|input|select/i;function Da(a){var b=a.type,d=a.value;if(b==="radio"||b==="checkbox")d=a.checked;else if(b==="select-multiple")d=a.selectedIndex> +-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d}function ha(a,b){var d=a.target,f,e;if(!(!ga.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Da(d);if(e!==f){if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data",e);if(d.type!=="select"&&(f!=null||e)){a.type="change";return c.event.trigger(a,b,this)}}}}c.event.special.change={filters:{focusout:ha,click:function(a){var b=a.target,d=b.type;if(d=== +"radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return ha.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return ha.call(this,a)},beforeactivate:function(a){a=a.target;a.nodeName.toLowerCase()==="input"&&a.type==="radio"&&c.data(a,"_change_data",Da(a))}},setup:function(a,b,d){for(var f in W)c.event.add(this,f+".specialChange."+d.guid,W[f]);return ga.test(this.nodeName)}, +remove:function(a,b){for(var d in W)c.event.remove(this,d+".specialChange"+(b?"."+b.guid:""),W[d]);return ga.test(this.nodeName)}};var W=c.event.special.change.filters}s.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}c.event.special[b]={setup:function(){this.addEventListener(a,d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d, +f,e){if(typeof d==="object"){for(var i in d)this[b](i,f,d[i],e);return this}if(c.isFunction(f)){thisObject=e;e=f;f=w}var j=b==="one"?c.proxy(e,function(o){c(this).unbind(o,j);return e.apply(this,arguments)}):e;return d==="unload"&&b!=="one"?this.one(d,f,e,thisObject):this.each(function(){c.event.add(this,d,j,f)})}});c.fn.extend({unbind:function(a,b){if(typeof a==="object"&&!a.preventDefault){for(var d in a)this.unbind(d,a[d]);return this}return this.each(function(){c.event.remove(this,a,b)})},trigger:function(a, +b){return this.each(function(){c.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0]){a=c.Event(a);a.preventDefault();a.stopPropagation();c.event.trigger(a,b,this[0]);return a.result}},toggle:function(a){for(var b=arguments,d=1;d<b.length;)c.proxy(a,b[d++]);return this.click(c.proxy(a,function(f){var e=(c.data(this,"lastToggle"+a.guid)||0)%d;c.data(this,"lastToggle"+a.guid,e+1);f.preventDefault();return b[e].apply(this,arguments)||false}))},hover:function(a,b){return this.mouseenter(a).mouseleave(b|| +a)},live:function(a,b,d){if(c.isFunction(b)){d=b;b=w}c(this.context).bind(ra(a,this.selector),{data:b,selector:this.selector,live:a},d);return this},die:function(a,b){c(this.context).unbind(ra(a,this.selector),b?{guid:b.guid+this.selector+a}:null);return this}});c.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),function(a,b){c.fn[b]=function(d){return d? +this.bind(b,d):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});A.attachEvent&&!A.addEventListener&&A.attachEvent("onunload",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}});(function(){function a(g){for(var h="",k,m=0;g[m];m++){k=g[m];if(k.nodeType===3||k.nodeType===4)h+=k.nodeValue;else if(k.nodeType!==8)h+=a(k.childNodes)}return h}function b(g,h,k,m,r,q){r=0;for(var v=m.length;r<v;r++){var u=m[r];if(u){u=u[g];for(var y=false;u;){if(u.sizcache=== +k){y=m[u.sizset];break}if(u.nodeType===1&&!q){u.sizcache=k;u.sizset=r}if(u.nodeName.toLowerCase()===h){y=u;break}u=u[g]}m[r]=y}}}function d(g,h,k,m,r,q){r=0;for(var v=m.length;r<v;r++){var u=m[r];if(u){u=u[g];for(var y=false;u;){if(u.sizcache===k){y=m[u.sizset];break}if(u.nodeType===1){if(!q){u.sizcache=k;u.sizset=r}if(typeof h!=="string"){if(u===h){y=true;break}}else if(p.filter(h,[u]).length>0){y=u;break}}u=u[g]}m[r]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, +e=0,i=Object.prototype.toString,j=false,o=true;[0,0].sort(function(){o=false;return 0});var p=function(g,h,k,m){k=k||[];var r=h=h||s;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||typeof g!=="string")return k;for(var q=[],v,u,y,S,I=true,N=x(h),J=g;(f.exec(""),v=f.exec(J))!==null;){J=v[3];q.push(v[1]);if(v[2]){S=v[3];break}}if(q.length>1&&t.exec(g))if(q.length===2&&n.relative[q[0]])u=ia(q[0]+q[1],h);else for(u=n.relative[q[0]]?[h]:p(q.shift(),h);q.length;){g=q.shift();if(n.relative[g])g+=q.shift(); +u=ia(g,u)}else{if(!m&&q.length>1&&h.nodeType===9&&!N&&n.match.ID.test(q[0])&&!n.match.ID.test(q[q.length-1])){v=p.find(q.shift(),h,N);h=v.expr?p.filter(v.expr,v.set)[0]:v.set[0]}if(h){v=m?{expr:q.pop(),set:B(m)}:p.find(q.pop(),q.length===1&&(q[0]==="~"||q[0]==="+")&&h.parentNode?h.parentNode:h,N);u=v.expr?p.filter(v.expr,v.set):v.set;if(q.length>0)y=B(u);else I=false;for(;q.length;){var E=q.pop();v=E;if(n.relative[E])v=q.pop();else E="";if(v==null)v=h;n.relative[E](y,v,N)}}else y=[]}y||(y=u);if(!y)throw"Syntax error, unrecognized expression: "+ +(E||g);if(i.call(y)==="[object Array]")if(I)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&F(h,y[g])))k.push(u[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&k.push(u[g]);else k.push.apply(k,y);else B(y,k);if(S){p(S,r,k,m);p.uniqueSort(k)}return k};p.uniqueSort=function(g){if(D){j=o;g.sort(D);if(j)for(var h=1;h<g.length;h++)g[h]===g[h-1]&&g.splice(h--,1)}return g};p.matches=function(g,h){return p(g,null,null,h)};p.find=function(g,h,k){var m,r;if(!g)return[]; +for(var q=0,v=n.order.length;q<v;q++){var u=n.order[q];if(r=n.leftMatch[u].exec(g)){var y=r[1];r.splice(1,1);if(y.substr(y.length-1)!=="\\"){r[1]=(r[1]||"").replace(/\\/g,"");m=n.find[u](r,h,k);if(m!=null){g=g.replace(n.match[u],"");break}}}}m||(m=h.getElementsByTagName("*"));return{set:m,expr:g}};p.filter=function(g,h,k,m){for(var r=g,q=[],v=h,u,y,S=h&&h[0]&&x(h[0]);g&&h.length;){for(var I in n.filter)if((u=n.leftMatch[I].exec(g))!=null&&u[2]){var N=n.filter[I],J,E;E=u[1];y=false;u.splice(1,1);if(E.substr(E.length- +1)!=="\\"){if(v===q)q=[];if(n.preFilter[I])if(u=n.preFilter[I](u,v,k,q,m,S)){if(u===true)continue}else y=J=true;if(u)for(var X=0;(E=v[X])!=null;X++)if(E){J=N(E,u,X,v);var Ea=m^!!J;if(k&&J!=null)if(Ea)y=true;else v[X]=false;else if(Ea){q.push(E);y=true}}if(J!==w){k||(v=q);g=g.replace(n.match[I],"");if(!y)return[];break}}}if(g===r)if(y==null)throw"Syntax error, unrecognized expression: "+g;else break;r=g}return v};var n=p.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF-]|\\.)+)/, +CLASS:/\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(g){return g.getAttribute("href")}}, +relative:{"+":function(g,h){var k=typeof h==="string",m=k&&!/\W/.test(h);k=k&&!m;if(m)h=h.toLowerCase();m=0;for(var r=g.length,q;m<r;m++)if(q=g[m]){for(;(q=q.previousSibling)&&q.nodeType!==1;);g[m]=k||q&&q.nodeName.toLowerCase()===h?q||false:q===h}k&&p.filter(h,g,true)},">":function(g,h){var k=typeof h==="string";if(k&&!/\W/.test(h)){h=h.toLowerCase();for(var m=0,r=g.length;m<r;m++){var q=g[m];if(q){k=q.parentNode;g[m]=k.nodeName.toLowerCase()===h?k:false}}}else{m=0;for(r=g.length;m<r;m++)if(q=g[m])g[m]= +k?q.parentNode:q.parentNode===h;k&&p.filter(h,g,true)}},"":function(g,h,k){var m=e++,r=d;if(typeof h==="string"&&!/\W/.test(h)){var q=h=h.toLowerCase();r=b}r("parentNode",h,m,g,q,k)},"~":function(g,h,k){var m=e++,r=d;if(typeof h==="string"&&!/\W/.test(h)){var q=h=h.toLowerCase();r=b}r("previousSibling",h,m,g,q,k)}},find:{ID:function(g,h,k){if(typeof h.getElementById!=="undefined"&&!k)return(g=h.getElementById(g[1]))?[g]:[]},NAME:function(g,h){if(typeof h.getElementsByName!=="undefined"){var k=[]; +h=h.getElementsByName(g[1]);for(var m=0,r=h.length;m<r;m++)h[m].getAttribute("name")===g[1]&&k.push(h[m]);return k.length===0?null:k}},TAG:function(g,h){return h.getElementsByTagName(g[1])}},preFilter:{CLASS:function(g,h,k,m,r,q){g=" "+g[1].replace(/\\/g,"")+" ";if(q)return g;q=0;for(var v;(v=h[q])!=null;q++)if(v)if(r^(v.className&&(" "+v.className+" ").replace(/[\t\n]/g," ").indexOf(g)>=0))k||m.push(v);else if(k)h[q]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()}, +CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,k,m,r,q){h=g[1].replace(/\\/g,"");if(!q&&n.attrMap[h])g[1]=n.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,k,m,r){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=p(g[3],null,null,h);else{g=p.filter(g[3],h,k,true^r);k||m.push.apply(m, +g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,k){return!!p(k[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)}, +text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}}, +setFilters:{first:function(g,h){return h===0},last:function(g,h,k,m){return h===m.length-1},even:function(g,h){return h%2===0},odd:function(g,h){return h%2===1},lt:function(g,h,k){return h<k[3]-0},gt:function(g,h,k){return h>k[3]-0},nth:function(g,h,k){return k[3]-0===h},eq:function(g,h,k){return k[3]-0===h}},filter:{PSEUDO:function(g,h,k,m){var r=h[1],q=n.filters[r];if(q)return q(g,k,h,m);else if(r==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(r==="not"){h= +h[3];k=0;for(m=h.length;k<m;k++)if(h[k]===g)return false;return true}else throw"Syntax error, unrecognized expression: "+r;},CHILD:function(g,h){var k=h[1],m=g;switch(k){case "only":case "first":for(;m=m.previousSibling;)if(m.nodeType===1)return false;if(k==="first")return true;m=g;case "last":for(;m=m.nextSibling;)if(m.nodeType===1)return false;return true;case "nth":k=h[2];var r=h[3];if(k===1&&r===0)return true;h=h[0];var q=g.parentNode;if(q&&(q.sizcache!==h||!g.nodeIndex)){var v=0;for(m=q.firstChild;m;m= +m.nextSibling)if(m.nodeType===1)m.nodeIndex=++v;q.sizcache=h}g=g.nodeIndex-r;return k===0?g===0:g%k===0&&g/k>=0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var k=h[1];g=n.attrHandle[k]?n.attrHandle[k](g):g[k]!=null?g[k]:g.getAttribute(k);k=g+"";var m=h[2];h=h[4];return g==null?m==="!=":m=== +"="?k===h:m==="*="?k.indexOf(h)>=0:m==="~="?(" "+k+" ").indexOf(h)>=0:!h?k&&g!==false:m==="!="?k!==h:m==="^="?k.indexOf(h)===0:m==="$="?k.substr(k.length-h.length)===h:m==="|="?k===h||k.substr(0,h.length+1)===h+"-":false},POS:function(g,h,k,m){var r=n.setFilters[h[2]];if(r)return r(g,k,h,m)}}},t=n.match.POS;for(var z in n.match){n.match[z]=new RegExp(n.match[z].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[z]=new RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[z].source.replace(/\\(\d+)/g,function(g, +h){return"\\"+(h-0+1)}))}var B=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};try{Array.prototype.slice.call(s.documentElement.childNodes,0)}catch(C){B=function(g,h){h=h||[];if(i.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var k=0,m=g.length;k<m;k++)h.push(g[k]);else for(k=0;g[k];k++)h.push(g[k]);return h}}var D;if(s.documentElement.compareDocumentPosition)D=function(g,h){if(!g.compareDocumentPosition|| +!h.compareDocumentPosition){if(g==h)j=true;return g.compareDocumentPosition?-1:1}g=g.compareDocumentPosition(h)&4?-1:g===h?0:1;if(g===0)j=true;return g};else if("sourceIndex"in s.documentElement)D=function(g,h){if(!g.sourceIndex||!h.sourceIndex){if(g==h)j=true;return g.sourceIndex?-1:1}g=g.sourceIndex-h.sourceIndex;if(g===0)j=true;return g};else if(s.createRange)D=function(g,h){if(!g.ownerDocument||!h.ownerDocument){if(g==h)j=true;return g.ownerDocument?-1:1}var k=g.ownerDocument.createRange(),m= +h.ownerDocument.createRange();k.setStart(g,0);k.setEnd(g,0);m.setStart(h,0);m.setEnd(h,0);g=k.compareBoundaryPoints(Range.START_TO_END,m);if(g===0)j=true;return g};(function(){var g=s.createElement("div"),h="script"+(new Date).getTime();g.innerHTML="<a name='"+h+"'/>";var k=s.documentElement;k.insertBefore(g,k.firstChild);if(s.getElementById(h)){n.find.ID=function(m,r,q){if(typeof r.getElementById!=="undefined"&&!q)return(r=r.getElementById(m[1]))?r.id===m[1]||typeof r.getAttributeNode!=="undefined"&& +r.getAttributeNode("id").nodeValue===m[1]?[r]:w:[]};n.filter.ID=function(m,r){var q=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&q&&q.nodeValue===r}}k.removeChild(g);k=g=null})();(function(){var g=s.createElement("div");g.appendChild(s.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(h,k){k=k.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var m=0;k[m];m++)k[m].nodeType===1&&h.push(k[m]);k=h}return k};g.innerHTML="<a href='#'></a>"; +if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(h){return h.getAttribute("href",2)};g=null})();s.querySelectorAll&&function(){var g=p,h=s.createElement("div");h.innerHTML="<p class='TEST'></p>";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){p=function(m,r,q,v){r=r||s;if(!v&&r.nodeType===9&&!x(r))try{return B(r.querySelectorAll(m),q)}catch(u){}return g(m,r,q,v)};for(var k in g)p[k]=g[k];h=null}}(); +(function(){var g=s.createElement("div");g.innerHTML="<div class='test e'></div><div class='test'></div>";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(h,k,m){if(typeof k.getElementsByClassName!=="undefined"&&!m)return k.getElementsByClassName(h[1])};g=null}}})();var F=s.compareDocumentPosition?function(g,h){return g.compareDocumentPosition(h)&16}:function(g, +h){return g!==h&&(g.contains?g.contains(h):true)},x=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},ia=function(g,h){var k=[],m="",r;for(h=h.nodeType?[h]:h;r=n.match.PSEUDO.exec(g);){m+=r[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;r=0;for(var q=h.length;r<q;r++)p(g,h[r],k);return p.filter(m,k)};c.find=p;c.expr=p.selectors;c.expr[":"]=c.expr.filters;c.unique=p.uniqueSort;c.getText=a;c.isXMLDoc=x;c.contains=F})();var ab=/Until$/,bb=/^(?:parents|prevUntil|prevAll)/, +cb=/,/;R=Array.prototype.slice;var Fa=function(a,b,d){if(c.isFunction(b))return c.grep(a,function(e,i){return!!b.call(e,i,e)===d});else if(b.nodeType)return c.grep(a,function(e){return e===b===d});else if(typeof b==="string"){var f=c.grep(a,function(e){return e.nodeType===1});if(Pa.test(b))return c.filter(b,f,!d);else b=c.filter(b,a)}return c.grep(a,function(e){return c.inArray(e,b)>=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f<e;f++){d=b.length; +c.find(a,this[f],b);if(f>0)for(var i=d;i<b.length;i++)for(var j=0;j<d;j++)if(b[j]===b[i]){b.splice(i--,1);break}}return b},has:function(a){var b=c(a);return this.filter(function(){for(var d=0,f=b.length;d<f;d++)if(c.contains(this,b[d]))return true})},not:function(a){return this.pushStack(Fa(this,a,false),"not",a)},filter:function(a){return this.pushStack(Fa(this,a,true),"filter",a)},is:function(a){return!!a&&c.filter(a,this).length>0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,i= +{},j;if(f&&a.length){e=0;for(var o=a.length;e<o;e++){j=a[e];i[j]||(i[j]=c.expr.match.POS.test(j)?c(j,b||this.context):j)}for(;f&&f.ownerDocument&&f!==b;){for(j in i){e=i[j];if(e.jquery?e.index(f)>-1:c(f).is(e)){d.push({selector:j,elem:f});delete i[j]}}f=f.parentNode}}return d}var p=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(n,t){for(;t&&t.ownerDocument&&t!==b;){if(p?p.index(t)>-1:c(t).is(a))return t;t=t.parentNode}return null})},index:function(a){if(!a||typeof a=== +"string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),a);return this.pushStack(sa(a[0])||sa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode", +d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")? +a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);ab.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):e;if((this.length>1||cb.test(f))&&bb.test(a))e=e.reverse();return this.pushStack(e,a,R.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===w||!c(a).is(d));){a.nodeType=== +1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var Ga=/ jQuery\d+="(?:\d+|null)"/g,Y=/^\s+/,db=/(<([\w:]+)[^>]*?)\/>/g,eb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,Ha=/<([\w:]+)/,fb=/<tbody/i,gb=/<|&\w+;/,hb=function(a,b,d){return eb.test(d)?a:b+"></"+d+">"},G={option:[1,"<select multiple='multiple'>","</select>"], +legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};G.optgroup=G.option;G.tbody=G.tfoot=G.colgroup=G.caption=G.thead;G.th=G.td;if(!c.support.htmlSerialize)G._default=[1,"div<div>","</div>"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d=c(this); +return d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==w)return this.empty().append((this[0]&&this[0].ownerDocument||s).createTextNode(a));return c.getText(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this}, +wrapInner:function(a){return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&& +this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this.nextSibling)});else if(arguments.length){var a=this.pushStack(this, +"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Ga,"").replace(Y,"")],f)[0]}else return this.cloneNode(true)});if(a===true){ta(this,b);ta(this.find("*"),b.find("*"))}return b},html:function(a){if(a===w)return this[0]&&this[0].nodeType=== +1?this[0].innerHTML.replace(Ga,""):null;else if(typeof a==="string"&&!/<script/i.test(a)&&(c.support.leadingWhitespace||!Y.test(a))&&!G[(Ha.exec(a)||["",""])[1].toLowerCase()])try{for(var b=0,d=this.length;b<d;b++)if(this[b].nodeType===1){T(this[b].getElementsByTagName("*"));this[b].innerHTML=a}}catch(f){this.empty().append(a)}else c.isFunction(a)?this.each(function(e){var i=c(this),j=i.html();i.empty().append(function(){return a.call(this,e,j)})}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&& +this[0].parentNode){c.isFunction(a)||(a=c(a).detach());return this.each(function(){var b=this.nextSibling,d=this.parentNode;c(this).remove();b?c(b).before(a):c(d).append(a)})}else return this.pushStack(c(c.isFunction(a)?a():a),"replaceWith",a)},detach:function(a){return this.remove(a,true)},domManip:function(a,b,d){function f(t){return c.nodeName(t,"table")?t.getElementsByTagName("tbody")[0]||t.appendChild(t.ownerDocument.createElement("tbody")):t}var e,i,j=a[0],o=[];if(c.isFunction(j))return this.each(function(t){var z= +c(this);a[0]=j.call(this,t,b?z.html():w);return z.domManip(a,b,d)});if(this[0]){e=a[0]&&a[0].parentNode&&a[0].parentNode.nodeType===11?{fragment:a[0].parentNode}:ua(a,this,o);if(i=e.fragment.firstChild){b=b&&c.nodeName(i,"tr");for(var p=0,n=this.length;p<n;p++)d.call(b?f(this[p],i):this[p],e.cacheable||this.length>1||p>0?e.fragment.cloneNode(true):e.fragment)}o&&c.each(o,La)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"}, +function(a,b){c.fn[a]=function(d){var f=[];d=c(d);for(var e=0,i=d.length;e<i;e++){var j=(e>0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),j);f=f.concat(j)}return this.pushStack(f,a,d.selector)}});c.each({remove:function(a,b){if(!a||c.filter(a,[this]).length){if(!b&&this.nodeType===1){T(this.getElementsByTagName("*"));T([this])}this.parentNode&&this.parentNode.removeChild(this)}},empty:function(){for(this.nodeType===1&&T(this.getElementsByTagName("*"));this.firstChild;)this.removeChild(this.firstChild)}}, +function(a,b){c.fn[a]=function(){return this.each(b,arguments)}});c.extend({clean:function(a,b,d,f){b=b||s;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||s;var e=[];c.each(a,function(i,j){if(typeof j==="number")j+="";if(j){if(typeof j==="string"&&!gb.test(j))j=b.createTextNode(j);else if(typeof j==="string"){j=j.replace(db,hb);var o=(Ha.exec(j)||["",""])[1].toLowerCase(),p=G[o]||G._default,n=p[0];i=b.createElement("div");for(i.innerHTML=p[1]+j+p[2];n--;)i=i.lastChild; +if(!c.support.tbody){n=fb.test(j);o=o==="table"&&!n?i.firstChild&&i.firstChild.childNodes:p[1]==="<table>"&&!n?i.childNodes:[];for(p=o.length-1;p>=0;--p)c.nodeName(o[p],"tbody")&&!o[p].childNodes.length&&o[p].parentNode.removeChild(o[p])}!c.support.leadingWhitespace&&Y.test(j)&&i.insertBefore(b.createTextNode(Y.exec(j)[0]),i.firstChild);j=c.makeArray(i.childNodes)}if(j.nodeType)e.push(j);else e=c.merge(e,j)}});if(d)for(a=0;e[a];a++)if(f&&c.nodeName(e[a],"script")&&(!e[a].type||e[a].type.toLowerCase()=== +"text/javascript"))f.push(e[a].parentNode?e[a].parentNode.removeChild(e[a]):e[a]);else{e[a].nodeType===1&&e.splice.apply(e,[a+1,0].concat(c.makeArray(e[a].getElementsByTagName("script"))));d.appendChild(e[a])}return e}});var ib=/z-?index|font-?weight|opacity|zoom|line-?height/i,Ia=/alpha\([^)]*\)/,Ja=/opacity=([^)]*)/,ja=/float/i,ka=/-([a-z])/ig,jb=/([A-Z])/g,kb=/^-?\d+(?:px)?$/i,lb=/^-?\d/,mb={position:"absolute",visibility:"hidden",display:"block"},nb=["Left","Right"],ob=["Top","Bottom"],pb=s.defaultView&& +s.defaultView.getComputedStyle,Ka=c.support.cssFloat?"cssFloat":"styleFloat",la=function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return $(this,a,b,true,function(d,f,e){if(e===w)return c.curCSS(d,f);if(typeof e==="number"&&!ib.test(f))e+="px";c.style(d,f,e)})};c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return w;if((b==="width"||b==="height")&&parseFloat(d)<0)d=w;var f=a.style||a,e=d!==w;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""=== +"NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter=Ia.test(a)?a.replace(Ia,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Ja.exec(f.filter)[1])/100+"":""}if(ja.test(b))b=Ka;b=b.replace(ka,la);if(e)f[b]=d;return f[b]},css:function(a,b,d,f){if(b==="width"||b==="height"){var e,i=b==="width"?nb:ob;function j(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(i,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+= +parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a,"border"+this+"Width",true))||0})}a.offsetWidth!==0?j():c.swap(a,mb,j);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&a.currentStyle){f=Ja.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ja.test(b))b=Ka;if(!d&&e&&e[b])f=e[b];else if(pb){if(ja.test(b))b="float";b=b.replace(jb,"-$1").toLowerCase();e= +a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f=a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ka,la);f=a.currentStyle[b]||a.currentStyle[d];if(!kb.test(f)&&lb.test(f)){b=e.left;var i=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=i}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]= +f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b=a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var qb=K(),rb=/<script(.|\s)*?\/script>/gi,sb=/select|textarea/i,tb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,O=/=\?(&|$)/,ma=/\?/,ub=/(\?|&)_=.*?(&|$)/,vb=/^(\w+:)?\/\/([^\/?#]+)/, +wb=/%20/g;c.fn.extend({_load:c.fn.load,load:function(a,b,d){if(typeof a!=="string")return this._load(a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);f="POST"}c.ajax({url:a,type:f,dataType:"html",data:b,context:this,complete:function(i,j){if(j==="success"||j==="notmodified")this.html(e?c("<div />").append(i.responseText.replace(rb, +"")).find(e):i.responseText);d&&this.each(d,[i.responseText,j,i])}});return this},serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||sb.test(this.nodeName)||tb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}}); +c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})}, +ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:A.XMLHttpRequest&&(A.location.protocol!=="file:"||!A.ActiveXObject)?function(){return new A.XMLHttpRequest}:function(){try{return new A.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript", +text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&&e.success.call(p,o,j,x);e.global&&f("ajaxSuccess",[x,e])}function d(){e.complete&&e.complete.call(p,x,j);e.global&&f("ajaxComplete",[x,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}function f(r,q){(e.context?c(e.context):c.event).trigger(r,q)}var e=c.extend(true,{},c.ajaxSettings,a),i,j,o,p=e.context||e,n=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data, +e.traditional);if(e.dataType==="jsonp"){if(n==="GET")O.test(e.url)||(e.url+=(ma.test(e.url)?"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!O.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&O.test(e.data)||O.test(e.url))){i=e.jsonpCallback||"jsonp"+qb++;if(e.data)e.data=(e.data+"").replace(O,"="+i+"$1");e.url=e.url.replace(O,"="+i+"$1");e.dataType="script";A[i]=A[i]||function(r){o=r;b();d();A[i]=w;try{delete A[i]}catch(q){}B&& +B.removeChild(C)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache===false&&n==="GET"){var t=K(),z=e.url.replace(ub,"$1_="+t+"$2");e.url=z+(z===e.url?(ma.test(e.url)?"&":"?")+"_="+t:"")}if(e.data&&n==="GET")e.url+=(ma.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&c.event.trigger("ajaxStart");t=(t=vb.exec(e.url))&&(t[1]&&t[1]!==location.protocol||t[2]!==location.host);if(e.dataType==="script"&&n==="GET"&&t){var B=s.getElementsByTagName("head")[0]||s.documentElement,C=s.createElement("script"); +C.src=e.url;if(e.scriptCharset)C.charset=e.scriptCharset;if(!i){var D=false;C.onload=C.onreadystatechange=function(){if(!D&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){D=true;b();d();C.onload=C.onreadystatechange=null;B&&C.parentNode&&B.removeChild(C)}}}B.insertBefore(C,B.firstChild);return w}var F=false,x=e.xhr();if(x){e.username?x.open(n,e.url,e.async,e.username,e.password):x.open(n,e.url,e.async);try{if(e.data||a&&a.contentType)x.setRequestHeader("Content-Type", +e.contentType);if(e.ifModified){c.lastModified[e.url]&&x.setRequestHeader("If-Modified-Since",c.lastModified[e.url]);c.etag[e.url]&&x.setRequestHeader("If-None-Match",c.etag[e.url])}t||x.setRequestHeader("X-Requested-With","XMLHttpRequest");x.setRequestHeader("Accept",e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(ia){}if(e.beforeSend&&e.beforeSend.call(p,x,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");x.abort();return false}e.global&&f("ajaxSend", +[x,e]);var g=x.onreadystatechange=function(r){if(!x||x.readyState===0){F||d();F=true;if(x)x.onreadystatechange=c.noop}else if(!F&&x&&(x.readyState===4||r==="timeout")){F=true;x.onreadystatechange=c.noop;j=r==="timeout"?"timeout":!c.httpSuccess(x)?"error":e.ifModified&&c.httpNotModified(x,e.url)?"notmodified":"success";if(j==="success")try{o=c.httpData(x,e.dataType,e)}catch(q){j="parsererror"}if(j==="success"||j==="notmodified")i||b();else c.handleError(e,x,j);d();r==="timeout"&&x.abort();if(e.async)x= +null}};try{var h=x.abort;x.abort=function(){if(x){h.call(x);if(x)x.readyState=0}g()}}catch(k){}e.async&&e.timeout>0&&setTimeout(function(){x&&!F&&g("timeout")},e.timeout);try{x.send(n==="POST"||n==="PUT"||n==="DELETE"?e.data:null)}catch(m){c.handleError(e,x,null,m);d()}e.async||g();return x}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||A,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol=== +"file:"||a.status>=200&&a.status<300||a.status===304||a.status===1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;if(e&&a.documentElement.nodeName==="parsererror")throw"parsererror";if(d&& +d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b==="json"||!b&&f.indexOf("json")>=0)if(/^[\],:{}\s]*$/.test(a.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))a=A.JSON&&A.JSON.parse?A.JSON.parse(a):(new Function("return "+a))();else throw"Invalid JSON: "+a;else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(e,i){i= +c.isFunction(i)?i():i;f[f.length]=encodeURIComponent(e)+"="+encodeURIComponent(i)}var f=[];if(b===w)b=c.ajaxSettings.traditional;c.isArray(a)||a.jquery?c.each(a,function(){d(this.name,this.value)}):c.each(a,function e(i,j){if(c.isArray(j))c.each(j,function(o,p){b?d(i,p):e(i+"["+(typeof p==="object"||c.isArray(p)?o:"")+"]",p)});else!b&&j!=null&&typeof j==="object"?c.each(j,function(o,p){e(i+"["+o+"]",p)}):d(i,j)});return f.join("&").replace(wb,"+")}});var na={},xb=/toggle|show|hide/,yb=/^([+-]=)?([\d+-.]+)(.*)$/, +Z,va=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a!=null)return this.animate(L("show",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");this[a].style.display=d||"";if(c.css(this[a],"display")==="none"){d=this[a].nodeName;var f;if(na[d])f=na[d];else{var e=c("<"+d+" />").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove(); +na[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a<b;a++)this[a].style.display=c.data(this[a],"olddisplay")||"";return this}},hide:function(a,b){if(a!=null)return this.animate(L("hide",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");!d&&d!=="none"&&c.data(this[a],"olddisplay",c.css(this[a],"display"))}a=0;for(b=this.length;a<b;a++)this[a].style.display="none";return this}},_toggle:c.fn.toggle,toggle:function(a,b){var d=typeof a==="boolean";if(c.isFunction(a)&& +c.isFunction(b))this._toggle.apply(this,arguments);else a==null||d?this.each(function(){var f=d?a:c(this).is(":hidden");c(this)[f?"show":"hide"]()}):this.animate(L("toggle",3),a,b);return this},fadeTo:function(a,b,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,d)},animate:function(a,b,d,f){var e=c.speed(b,d,f);if(c.isEmptyObject(a))return this.each(e.complete);return this[e.queue===false?"each":"queue"](function(){var i=c.extend({},e),j,o=this.nodeType===1&&c(this).is(":hidden"), +p=this;for(j in a){var n=j.replace(ka,la);if(j!==n){a[n]=a[j];delete a[j];j=n}if(a[j]==="hide"&&o||a[j]==="show"&&!o)return i.complete.call(this);if((j==="height"||j==="width")&&this.style){i.display=c.css(this,"display");i.overflow=this.style.overflow}if(c.isArray(a[j])){(i.specialEasing=i.specialEasing||{})[j]=a[j][1];a[j]=a[j][0]}}if(i.overflow!=null)this.style.overflow="hidden";i.curAnim=c.extend({},a);c.each(a,function(t,z){var B=new c.fx(p,i,t);if(xb.test(z))B[z==="toggle"?o?"show":"hide":z](a); +else{var C=yb.exec(z),D=B.cur(true)||0;if(C){z=parseFloat(C[2]);var F=C[3]||"px";if(F!=="px"){p.style[t]=(z||1)+F;D=(z||1)/B.cur(true)*D;p.style[t]=D+F}if(C[1])z=(C[1]==="-="?-1:1)*z+D;B.custom(D,z,F)}else B.custom(D,z,"")}});return true})},stop:function(a,b){var d=c.timers;a&&this.queue([]);this.each(function(){for(var f=d.length-1;f>=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:L("show",1),slideUp:L("hide",1),slideToggle:L("toggle", +1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration==="number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a, +b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]||c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]== +null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(i){return e.step(i)}this.startTime=K();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start;this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!Z)Z=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop=== +"width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=K(),d=true;if(a||b>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow= +this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem,e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos= +c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b<a.length;b++)a[b]()||a.splice(b--,1);a.length||c.fx.stop()},stop:function(){clearInterval(Z);Z=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){c.style(a.elem,"opacity",a.now)},_default:function(a){if(a.elem.style&&a.elem.style[a.prop]!= +null)a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit;else a.elem[a.prop]=a.now}}});if(c.expr&&c.expr.filters)c.expr.filters.animated=function(a){return c.grep(c.timers,function(b){return a===b.elem}).length};c.fn.offset="getBoundingClientRect"in s.documentElement?function(a){var b=this[0];if(!b||!b.ownerDocument)return null;if(a)return this.each(function(e){c.offset.setOffset(this,a,e)});if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);var d=b.getBoundingClientRect(), +f=b.ownerDocument;b=f.body;f=f.documentElement;return{top:d.top+(self.pageYOffset||c.support.boxModel&&f.scrollTop||b.scrollTop)-(f.clientTop||b.clientTop||0),left:d.left+(self.pageXOffset||c.support.boxModel&&f.scrollLeft||b.scrollLeft)-(f.clientLeft||b.clientLeft||0)}}:function(a){var b=this[0];if(!b||!b.ownerDocument)return null;if(a)return this.each(function(t){c.offset.setOffset(this,a,t)});if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);c.offset.initialize();var d=b.offsetParent,f= +b,e=b.ownerDocument,i,j=e.documentElement,o=e.body;f=(e=e.defaultView)?e.getComputedStyle(b,null):b.currentStyle;for(var p=b.offsetTop,n=b.offsetLeft;(b=b.parentNode)&&b!==o&&b!==j;){if(c.offset.supportsFixedPosition&&f.position==="fixed")break;i=e?e.getComputedStyle(b,null):b.currentStyle;p-=b.scrollTop;n-=b.scrollLeft;if(b===d){p+=b.offsetTop;n+=b.offsetLeft;if(c.offset.doesNotAddBorder&&!(c.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(b.nodeName))){p+=parseFloat(i.borderTopWidth)|| +0;n+=parseFloat(i.borderLeftWidth)||0}f=d;d=b.offsetParent}if(c.offset.subtractsBorderForOverflowNotVisible&&i.overflow!=="visible"){p+=parseFloat(i.borderTopWidth)||0;n+=parseFloat(i.borderLeftWidth)||0}f=i}if(f.position==="relative"||f.position==="static"){p+=o.offsetTop;n+=o.offsetLeft}if(c.offset.supportsFixedPosition&&f.position==="fixed"){p+=Math.max(j.scrollTop,o.scrollTop);n+=Math.max(j.scrollLeft,o.scrollLeft)}return{top:p,left:n}};c.offset={initialize:function(){var a=s.body,b=s.createElement("div"), +d,f,e,i=parseFloat(c.curCSS(a,"marginTop",true))||0;c.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"});b.innerHTML="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";a.insertBefore(b,a.firstChild); +d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i;a.removeChild(b);c.offset.initialize=c.noop}, +bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),i=parseInt(c.curCSS(a,"top",true),10)||0,j=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a,d,e);d={top:b.top-e.top+i,left:b.left- +e.left+j};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top-f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a= +this.offsetParent||s.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],i;if(!e)return null;if(f!==w)return this.each(function(){if(i=wa(this))i.scrollTo(!a?f:c(i).scrollLeft(),a?f:c(i).scrollTop());else this[d]=f});else return(i=wa(e))?"pageXOffset"in i?i[a?"pageYOffset":"pageXOffset"]:c.support.boxModel&&i.document.documentElement[d]||i.document.body[d]:e[d]}}); +c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;return"scrollTo"in e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+ +b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===w?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});A.jQuery=A.$=c})(window); diff --git a/devtools/client/inspector/markup/test/lib_jquery_1.6_min.js b/devtools/client/inspector/markup/test/lib_jquery_1.6_min.js new file mode 100644 index 000000000..c72011dfa --- /dev/null +++ b/devtools/client/inspector/markup/test/lib_jquery_1.6_min.js @@ -0,0 +1,16 @@ +/*! + * jQuery JavaScript Library v1.6 + * http://jquery.com/ + * + * Copyright 2011, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Mon May 2 13:50:00 2011 -0400 + */ +(function(a,b){function cw(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function ct(a){if(!ch[a]){var b=f("<"+a+">").appendTo("body"),d=b.css("display");b.remove();if(d==="none"||d===""){ci||(ci=c.createElement("iframe"),ci.frameBorder=ci.width=ci.height=0),c.body.appendChild(ci);if(!cj||!ci.createElement)cj=(ci.contentWindow||ci.contentDocument).document,cj.write("<!doctype><html><body></body></html>");b=cj.createElement(a),cj.body.appendChild(b),d=f.css(b,"display"),c.body.removeChild(ci)}ch[a]=d}return ch[a]}function cs(a,b){var c={};f.each(cn.concat.apply([],cn.slice(0,b)),function(){c[this]=a});return c}function cr(){co=b}function cq(){setTimeout(cr,0);return co=f.now()}function cg(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function cf(){try{return new a.XMLHttpRequest}catch(b){}}function b_(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function b$(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function bZ(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bD.test(a)?d(a,e):bZ(a+"["+(typeof e=="object"||f.isArray(e)?b:"")+"]",e,c,d)});else if(!c&&b!=null&&typeof b=="object")for(var e in b)bZ(a+"["+e+"]",b[e],c,d);else d(a,b)}function bY(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bS,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=bY(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=bY(a,c,d,e,"*",g));return l}function bX(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bO),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bB(a,b,c){var d=b==="width"?bv:bw,e=b==="width"?a.offsetWidth:a.offsetHeight;if(c==="border")return e;f.each(d,function(){c||(e-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?e+=parseFloat(f.css(a,"margin"+this))||0:e-=parseFloat(f.css(a,"border"+this+"Width"))||0});return e}function bl(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval(b.text||b.textContent||b.innerHTML||""),b.parentNode&&b.parentNode.removeChild(b)}function bk(a){f.nodeName(a,"input")?bj(a):a.getElementsByTagName&&f.grep(a.getElementsByTagName("input"),bj)}function bj(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bi(a){return"getElementsByTagName"in a?a.getElementsByTagName("*"):"querySelectorAll"in a?a.querySelectorAll("*"):[]}function bh(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bg(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c=f.expando,d=f.data(a),e=f.data(b,d);if(d=d[c]){var g=d.events;e=e[c]=f.extend({},d);if(g){delete e.handle,e.events={};for(var h in g)for(var i=0,j=g[h].length;i<j;i++)f.event.add(b,h+(g[h][i].namespace?".":"")+g[h][i].namespace,g[h][i],g[h][i].data)}}}}function bf(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function W(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(R.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function V(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function N(a,b){return(a&&a!=="*"?a+".":"")+b.replace(z,"`").replace(A,"&")}function M(a){var b,c,d,e,g,h,i,j,k,l,m,n,o,p=[],q=[],r=f._data(this,"events");if(!(a.liveFired===this||!r||!r.live||a.target.disabled||a.button&&a.type==="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var s=r.live.slice(0);for(i=0;i<s.length;i++)g=s[i],g.origType.replace(x,"")===a.type?q.push(g.selector):s.splice(i--,1);e=f(a.target).closest(q,a.currentTarget);for(j=0,k=e.length;j<k;j++){m=e[j];for(i=0;i<s.length;i++){g=s[i];if(m.selector===g.selector&&(!n||n.test(g.namespace))&&!m.elem.disabled){h=m.elem,d=null;if(g.preType==="mouseenter"||g.preType==="mouseleave")a.type=g.preType,d=f(a.relatedTarget).closest(g.selector)[0],d&&f.contains(h,d)&&(d=h);(!d||d!==h)&&p.push({elem:h,handleObj:g,level:m.level})}}}for(j=0,k=p.length;j<k;j++){e=p[j];if(c&&e.level>c)break;a.currentTarget=e.elem,a.data=e.handleObj.data,a.handleObj=e.handleObj,o=e.handleObj.origHandler.apply(e.elem,arguments);if(o===!1||a.isPropagationStopped()){c=e.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function K(a,c,d){var e=f.extend({},d[0]);e.type=a,e.originalEvent={},e.liveFired=b,f.event.handle.call(c,e),e.isDefaultPrevented()&&d[0].preventDefault()}function E(){return!0}function D(){return!1}function m(a,c,d){var e=c+"defer",g=c+"queue",h=c+"mark",i=f.data(a,e,b,!0);i&&(d==="queue"||!f.data(a,g,b,!0))&&(d==="mark"||!f.data(a,h,b,!0))&&setTimeout(function(){!f.data(a,g,b,!0)&&!f.data(a,h,b,!0)&&(f.removeData(a,e,!0),i.resolve())},0)}function l(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function k(a,c,d){if(d===b&&a.nodeType===1){name="data-"+c.replace(j,"$1-$2").toLowerCase(),d=a.getAttribute(name);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNaN(d)?i.test(d)?f.parseJSON(d):d:parseFloat(d)}catch(e){}f.data(a,c,d)}else d=b}return d}var c=a.document,d=a.navigator,e=a.location,f=function(){function H(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(H,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=d.userAgent,x,y,z,A=Object.prototype.toString,B=Object.prototype.hasOwnProperty,C=Array.prototype.push,D=Array.prototype.slice,E=String.prototype.trim,F=Array.prototype.indexOf,G={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)==="<"&&a.charAt(a.length-1)===">"&&a.length>=3?g=[null,a,null]:g=i.exec(a);if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.6",length:0,size:function(){return this.length},toArray:function(){return D.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?C.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),y.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(D.apply(this,arguments),"slice",D.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:C,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;y.resolveWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!y){y=e._Deferred();if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",z,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",z),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&H()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNaN:function(a){return a==null||!m.test(a)||isNaN(a)},type:function(a){return a==null?String(a):G[A.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;if(a.constructor&&!B.call(a,"constructor")&&!B.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a);return c===b||B.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(b,c,d){a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),d=c.documentElement,(!d||!d.nodeName||d.nodeName==="parsererror")&&e.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:E?function(a){return a==null?"":E.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?C.call(c,a):e.merge(c,a)}return c},inArray:function(a,b){if(F)return F.call(b,a);for(var c=0,d=b.length;c<d;c++)if(b[c]===a)return c;return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=D.call(arguments,2),g=function(){return a.apply(c,f.concat(D.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h){var i=a.length;if(typeof c=="object"){for(var j in c)e.access(a,j,c[j],f,g,d);return a}if(d!==b){f=!h&&f&&e.isFunction(d);for(var k=0;k<i;k++)g(a[k],c,f?d.call(a[k],k,g(a[k],c)):d,h);return a}return i?g(a[0],c):b},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=s.exec(a)||t.exec(a)||u.exec(a)||a.indexOf("compatible")<0&&v.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(c,d){d&&d instanceof e&&!(d instanceof a)&&(d=a(d));return e.fn.init.call(this,c,d,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){G["[object "+b+"]"]=b.toLowerCase()}),x=e.uaMatch(w),x.browser&&(e.browser[x.browser]=!0,e.browser.version=x.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?z=function(){c.removeEventListener("DOMContentLoaded",z,!1),e.ready()}:c.attachEvent&&(z=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",z),e.ready())});return e}(),g="done fail isResolved isRejected promise then always pipe".split(" "),h=[].slice;f.extend({_Deferred:function(){var a=[],b,c,d,e={done:function(){if(!d){var c=arguments,g,h,i,j,k;b&&(k=b,b=0);for(g=0,h=c.length;g<h;g++)i=c[g],j=f.type(i),j==="array"?e.done.apply(e,i):j==="function"&&a.push(i);k&&e.resolveWith(k[0],k[1])}return this},resolveWith:function(e,f){if(!d&&!b&&!c){f=f||[],c=1;try{while(a[0])a.shift().apply(e,f)}finally{b=[e,f],c=0}}return this},resolve:function(){e.resolveWith(this,arguments);return this},isResolved:function(){return!!c||!!b},cancel:function(){d=1,a=[];return this}};return e},Deferred:function(a){var b=f._Deferred(),c=f._Deferred(),d;f.extend(b,{then:function(a,c){b.done(a).fail(c);return this},always:function(){return b.done.apply(b,arguments).fail.apply(this,arguments)},fail:c.done,rejectWith:c.resolveWith,reject:c.resolve,isRejected:c.isResolved,pipe:function(a,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[c,"reject"]},function(a,c){var e=c[0],g=c[1],h;f.isFunction(e)?b[a](function(){h=e.apply(this,arguments),f.isFunction(h.promise)?h.promise().then(d.resolve,d.reject):d[g](h)}):b[a](d[g])})}).promise()},promise:function(a){if(a==null){if(d)return d;d=a={}}var c=g.length;while(c--)a[g[c]]=b[g[c]];return a}}),b.done(c.cancel).fail(b.cancel),delete b.cancel,a&&a.call(b,b);return b},when:function(a){function i(a){return function(c){b[a]=arguments.length>1?h.call(arguments,0):c,--e||g.resolveWith(g,h.call(b,0))}}var b=arguments,c=0,d=b.length,e=d,g=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred();if(d>1){for(;c<d;c++)b[c]&&f.isFunction(b[c].promise)?b[c].promise().then(i(c),g.reject):--e;e||g.resolveWith(g,b)}else g!==a&&g.resolveWith(g,d?[a]:[]);return g.promise()}}),f.support=function(){var a=c.createElement("div"),b,d,e,f,g,h,i,j,k,l,m,n,o,p,q;a.setAttribute("className","t"),a.innerHTML=" <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>",b=a.getElementsByTagName("*"),d=a.getElementsByTagName("a")[0];if(!b||!b.length||!d)return{};e=c.createElement("select"),f=e.appendChild(c.createElement("option")),g=a.getElementsByTagName("input")[0],i={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(d.getAttribute("style")),hrefNormalized:d.getAttribute("href")==="/a",opacity:/^0.55$/.test(d.style.opacity),cssFloat:!!d.style.cssFloat,checkOn:g.value==="on",optSelected:f.selected,getSetAttribute:a.className!=="t",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},g.checked=!0,i.noCloneChecked=g.cloneNode(!0).checked,e.disabled=!0,i.optDisabled=!f.disabled;try{delete a.test}catch(r){i.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function click(){i.noCloneEvent=!1,a.detachEvent("onclick",click)}),a.cloneNode(!0).fireEvent("onclick")),g=c.createElement("input"),g.value="t",g.setAttribute("type","radio"),i.radioValue=g.value==="t",g.setAttribute("checked","checked"),a.appendChild(g),j=c.createDocumentFragment(),j.appendChild(a.firstChild),i.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",k=c.createElement("body"),l={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"};for(p in l)k.style[p]=l[p];k.appendChild(a),c.documentElement.appendChild(k),i.appendChecked=g.checked,i.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,i.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="<div style='width:4px;'></div>",i.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>",m=a.getElementsByTagName("td"),q=m[0].offsetHeight===0,m[0].style.display="",m[1].style.display="none",i.reliableHiddenOffsets=q&&m[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(h=c.createElement("div"),h.style.width="0",h.style.marginRight="0",a.appendChild(h),i.reliableMarginRight=(parseInt(c.defaultView.getComputedStyle(h,null).marginRight,10)||0)===0),k.innerHTML="",c.documentElement.removeChild(k);if(a.attachEvent)for(p in{submit:1,change:1,focusin:1})o="on"+p,q=o in a,q||(a.setAttribute(o,"return;"),q=typeof a[o]=="function"),i[p+"Bubbles"]=q;return i}(),f.boxModel=f.support.boxModel;var i=/^(?:\{.*\}|\[.*\])$/,j=/([a-z])([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!l(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g=f.expando,h=typeof c=="string",i,j=a.nodeType,k=j?f.cache:a,l=j?a[f.expando]:a[f.expando]&&f.expando;if((!l||e&&l&&!k[l][g])&&h&&d===b)return;l||(j?a[f.expando]=l=++f.uuid:l=f.expando),k[l]||(k[l]={},j||(k[l].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?k[l][g]=f.extend(k[l][g],c):k[l]=f.extend(k[l],c);i=k[l],e&&(i[g]||(i[g]={}),i=i[g]),d!==b&&(i[c]=d);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[c]:i}},removeData:function(b,c,d){if(!!f.acceptData(b)){var e=f.expando,g=b.nodeType,h=g?f.cache:b,i=g?b[f.expando]:f.expando;if(!h[i])return;if(c){var j=d?h[i][e]:h[i];if(j){delete j[c];if(!l(j))return}}if(d){delete h[i][e];if(!l(h[i]))return}var k=h[i][e];f.support.deleteExpando||h!=a?delete h[i]:h[i]=null,k?(h[i]={},g||(h[i].toJSON=f.noop),h[i][e]=k):g&&(f.support.deleteExpando?delete b[f.expando]:b.removeAttribute?b.removeAttribute(f.expando):b[f.expando]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d=null;if(typeof a=="undefined"){if(this.length){d=f.data(this[0]);if(this[0].nodeType===1){var e=this[0].attributes,g;for(var h=0,i=e.length;h<i;h++)g=e[h].name,g.indexOf("data-")===0&&(g=f.camelCase(g.substring(5)),k(this[0],g,d[g]))}}return d}if(typeof a=="object")return this.each(function(){f.data(this,a)});var j=a.split(".");j[1]=j[1]?"."+j[1]:"";if(c===b){d=this.triggerHandler("getData"+j[1]+"!",[j[0]]),d===b&&this.length&&(d=f.data(this[0],a),d=k(this[0],a,d));return d===b&&j[1]?this.data(j[0]):d}return this.each(function(){var b=f(this),d=[j[0],c];b.triggerHandler("setData"+j[1]+"!",d),f.data(this,a,c),b.triggerHandler("changeData"+j[1]+"!",d)})},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,c){a&&(c=(c||"fx")+"mark",f.data(a,c,(f.data(a,c,b,!0)||0)+1,!0))},_unmark:function(a,c,d){a!==!0&&(d=c,c=a,a=!1);if(c){d=d||"fx";var e=d+"mark",g=a?0:(f.data(c,e,b,!0)||1)-1;g?f.data(c,e,g,!0):(f.removeData(c,e,!0),m(c,d,"mark"))}},queue:function(a,c,d){if(a){c=(c||"fx")+"queue";var e=f.data(a,c,b,!0);d&&(!e||f.isArray(d)?e=f.data(a,c,f.makeArray(d),!0):e.push(d));return e||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e;d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),d.call(a,function(){f.dequeue(a,b)})),c.length||(f.removeData(a,b+"queue",!0),m(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){typeof a!="string"&&(c=a,a="fx");if(c===b)return f.queue(this[0],a);return this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(){var c=this;setTimeout(function(){f.dequeue(c,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function l(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark";while(g--)if(tmp=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f._Deferred(),!0))h++,tmp.done(l);l();return d.promise()}});var n=/[\n\t\r]/g,o=/\s+/,p=/\r/g,q=/^(?:button|input)$/i,r=/^(?:button|input|object|select|textarea)$/i,s=/^a(?:rea)?$/i,t=/^(?:data-|aria-)/,u=/\:/,v;f.fn.extend({attr:function(a,b){return f.access(this,a,b,!0,f.attr)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,a,b,!0,f.prop)},removeProp:function(a){return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.addClass(a.call(this,b,c.attr("class")||""))});if(a&&typeof a=="string"){var b=(a||"").split(o);for(var c=0,d=this.length;c<d;c++){var e=this[c];if(e.nodeType===1)if(!e.className)e.className=a;else{var g=" "+e.className+" ",h=e.className;for(var i=0,j=b.length;i<j;i++)g.indexOf(" "+b[i]+" ")<0&&(h+=" "+b[i]);e.className=f.trim(h)}}}return this},removeClass:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.removeClass(a.call(this,b,c.attr("class")))});if(a&&typeof a=="string"||a===b){var c=(a||"").split(o);for(var d=0,e=this.length;d<e;d++){var g=this[d];if(g.nodeType===1&&g.className)if(a){var h=(" "+g.className+" ").replace(n," ");for(var i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){var d=f(this);d.toggleClass(a.call(this,c,d.attr("class"),b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(o);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ";for(var c=0,d=this.length;c<d;c++)if((" "+this[c].className+" ").replace(n," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e=this[0];if(!arguments.length){if(e){c=f.valHooks[e.nodeName.toLowerCase()]||f.valHooks[e.type];if(c&&"get"in c&&(d=c.get(e,"value"))!==b)return d;return(e.value||"").replace(p,"")}return b}var g=f.isFunction(a);return this.each(function(d){var e=f(this),h;if(this.nodeType===1){g?h=a.call(this,d,e.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||"set"in c&&c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b=a.selectedIndex,c=[],d=a.options,e=a.type==="select-one";if(b<0)return null;for(var g=e?b:0,h=e?b+1:d.length;g<h;g++){var i=d[g];if(i.selected&&(f.support.optDisabled?!i.disabled:i.getAttribute("disabled")===null)&&(!i.parentNode.disabled||!f.nodeName(i.parentNode,"optgroup"))){value=f(i).val();if(e)return value;c.push(value)}}if(e&&!c.length&&d.length)return f(d[b]).val();return c},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attrFix:{tabindex:"tabIndex",readonly:"readOnly"},attr:function(a,c,d,e){var g=a.nodeType;if(!a||g===3||g===8||g===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);var h,i,j=g!==1||!f.isXMLDoc(a);c=j&&f.attrFix[c]||c,i=f.attrHooks[c]||(v&&(f.nodeName(a,"form")||u.test(c))?v:b);if(d!==b){if(d===null||d===!1&&!t.test(c)){f.removeAttr(a,c);return b}if(i&&"set"in i&&j&&(h=i.set(a,d,c))!==b)return h;d===!0&&!t.test(c)&&(d=c),a.setAttribute(c,""+d);return d}if(i&&"get"in i&&j)return i.get(a,c);h=a.getAttribute(c);return h===null?b:h},removeAttr:function(a,b){a.nodeType===1&&(b=f.attrFix[b]||b,f.support.getSetAttribute?a.removeAttribute(b):(f.attr(a,b,""),a.removeAttributeNode(a.getAttributeNode(b))))},attrHooks:{type:{set:function(a,b){if(q.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.getAttribute("value");a.setAttribute("type",b),c&&(a.value=c);return b}}},tabIndex:{get:function(a){var c=a.getAttributeNode("tabIndex");return c&&c.specified?parseInt(c.value,10):r.test(a.nodeName)||s.test(a.nodeName)&&a.href?0:b}}},propFix:{},prop:function(a,c,d){var e=a.nodeType;if(!a||e===3||e===8||e===2)return b;var g,h,i=e!==1||!f.isXMLDoc(a);c=i&&f.propFix[c]||c,h=f.propHooks[c];return d!==b?h&&"set"in h&&(g=h.set(a,d,c))!==b?g:a[c]=d:h&&"get"in h&&(g=h.get(a,c))!==b?g:a[c]},propHooks:{}}),f.support.getSetAttribute||(f.attrFix=f.extend(f.attrFix,{"for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder"}),v=f.attrHooks.name=f.attrHooks.value=f.valHooks.button={get:function(a,c){var d;if(c==="value"&&!f.nodeName(a,"button"))return a.getAttribute(c);d=a.getAttributeNode(c);return d&&d.specified?d.nodeValue:b},set:function(a,b,c){var d=a.getAttributeNode(c);if(d){d.nodeValue=b;return b}}},f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})})),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}})),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var w=Object.prototype.hasOwnProperty,x=/\.(.*)$/,y=/^(?:textarea|input|select)$/i,z=/\./g,A=/ /g,B=/[^\w\s.|`]/g,C=function(a){return a.replace(B,"\\$&")};f.event={add:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){if(d===!1)d=D;else if(!d)return;var g,h;d.handler&&(g=d,d=g.handler),d.guid||(d.guid=f.guid++);var i=f._data(a);if(!i)return;var j=i.events,k=i.handle;j||(i.events=j={}),k||(i.handle=k=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.handle.apply(k.elem,arguments):b}),k.elem=a,c=c.split(" ");var l,m=0,n;while(l=c[m++]){h=g?f.extend({},g):{handler:d,data:e},l.indexOf(".")>-1?(n=l.split("."),l=n.shift(),h.namespace=n.slice(0).sort().join(".")):(n=[],h.namespace=""),h.type=l,h.guid||(h.guid=d.guid);var o=j[l],p=f.event.special[l]||{};if(!o){o=j[l]=[];if(!p.setup||p.setup.call(a,e,n,k)===!1)a.addEventListener?a.addEventListener(l,k,!1):a.attachEvent&&a.attachEvent("on"+l,k)}p.add&&(p.add.call(a,h),h.handler.guid||(h.handler.guid=d.guid)),o.push(h),f.event.global[l]=!0}a=null}},global:{},remove:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){d===!1&&(d=D);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=f.hasData(a)&&f._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(d=c.handler,c=c.type);if(!c||typeof c=="string"&&c.charAt(0)==="."){c=c||"";for(h in t)f.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+f.map(m.slice(0).sort(),C).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!d){for(j=0;j<p.length;j++){q=p[j];if(l||n.test(q.namespace))f.event.remove(a,r,q.handler,j),p.splice(j--,1)}continue}o=f.event.special[h]||{};for(j=e||0;j<p.length;j++){q=p[j];if(d.guid===q.guid){if(l||n.test(q.namespace))e==null&&p.splice(j--,1),o.remove&&o.remove.call(a,q);if(e!=null)break}}if(p.length===0||e!=null&&p.length===1)(!o.teardown||o.teardown.call(a,m)===!1)&&f.removeEvent(a,h,s.handle),g=null,delete t[h]}if(f.isEmptyObject(t)){var u=s.handle;u&&(u.elem=null),delete s.events,delete s.handle,f.isEmptyObject(s)&&f.removeData(a,b,!0)}}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){var h=c.type||c,i=[],j;h.indexOf("!")>=0&&(h=h.slice(0,-1),j=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if(!!e&&!f.event.customEvent[h]||!!f.event.global[h]){c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.exclusive=j,c.namespace=i.join("."),c.namespace_re=new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)");if(g||!e)c.preventDefault(),c.stopPropagation();if(!e){f.each(f.cache,function(){var a=f.expando,b=this[a];b&&b.events&&b.events[h]&&f.event.trigger(c,d,b.handle.elem)});return}if(e.nodeType===3||e.nodeType===8)return;c.result=b,c.target=e,d=d?f.makeArray(d):[],d.unshift(c);var k=e,l=h.indexOf(":")<0?"on"+h:"";do{var m=f._data(k,"handle");c.currentTarget=k,m&&m.apply(k,d),l&&f.acceptData(k)&&k[l]&&k[l].apply(k,d)===!1&&(c.result=!1,c.preventDefault()),k=k.parentNode||k.ownerDocument||k===c.target.ownerDocument&&a}while(k&&!c.isPropagationStopped());if(!c.isDefaultPrevented()){var n,o=f.event.special[h]||{};if((!o._default||o._default.call(e.ownerDocument,c)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)){try{l&&e[h]&&(n=e[l],n&&(e[l]=null),f.event.triggered=h,e[h]())}catch(p){}n&&(e[l]=n),f.event.triggered=b}}return c.result}},handle:function(c){c=f.event.fix(c||a.event);var d=((f._data(this,"events")||{})[c.type]||[]).slice(0),e=!c.exclusive&&!c.namespace,g=Array.prototype.slice.call(arguments,0);g[0]=c,c.currentTarget=this;for(var h=0,i=d.length;h<i;h++){var j=d[h];if(e||c.namespace_re.test(j.namespace)){c.handler=j.handler,c.data=j.data,c.handleObj=j;var k=j.handler.apply(this,g);k!==b&&(c.result=k,k===!1&&(c.preventDefault(),c.stopPropagation()));if(c.isImmediatePropagationStopped())break}}return c.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(a){if(a[f.expando])return a;var d=a;a=f.Event(d);for(var e=this.props.length,g;e;)g=this.props[--e],a[g]=d[g];a.target||(a.target=a.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),!a.relatedTarget&&a.fromElement&&(a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement);if(a.pageX==null&&a.clientX!=null){var h=a.target.ownerDocument||c,i=h.documentElement,j=h.body;a.pageX=a.clientX+(i&&i.scrollLeft||j&&j.scrollLeft||0)-(i&&i.clientLeft||j&&j.clientLeft||0),a.pageY=a.clientY+(i&&i.scrollTop||j&&j.scrollTop||0)-(i&&i.clientTop||j&&j.clientTop||0)}a.which==null&&(a.charCode!=null||a.keyCode!=null)&&(a.which=a.charCode!=null?a.charCode:a.keyCode),!a.metaKey&&a.ctrlKey&&(a.metaKey=a.ctrlKey),!a.which&&a.button!==b&&(a.which=a.button&1?1:a.button&2?3:a.button&4?2:0);return a},guid:1e8,proxy:f.proxy,special:{ready:{setup:f.bindReady,teardown:f.noop},live:{add:function(a){f.event.add(this,N(a.origType,a.selector),f.extend({},a,{handler:M,guid:a.handler.guid}))},remove:function(a){f.event.remove(this,N(a.origType,a.selector),a)}},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}}},f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!this.preventDefault)return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?E:D):this.type=a,b&&f.extend(this,b),this.timeStamp=f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=E;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=E;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=E,this.stopPropagation()},isDefaultPrevented:D,isPropagationStopped:D,isImmediatePropagationStopped:D};var F=function(a){var b=a.relatedTarget;try{if(b&&b!==c&&!b.parentNode)return;while(b&&b!==this)b=b.parentNode;b!==this&&(a.type=a.data,f.event.handle.apply(this,arguments))}catch(d){}},G=function(a){a.type=a.data,f.event.handle.apply(this,arguments)};f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]={setup:function(c){f.event.add(this,b,c&&c.selector?G:F,a)},teardown:function(a){f.event.remove(this,b,a&&a.selector?G:F)}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(a,b){if(!f.nodeName(this,"form"))f.event.add(this,"click.specialSubmit",function(a){var b=a.target,c=b.type;(c==="submit"||c==="image")&&f(b).closest("form").length&&K("submit",this,arguments)}),f.event.add(this,"keypress.specialSubmit",function(a){var b=a.target,c=b.type;(c==="text"||c==="password")&&f(b).closest("form").length&&a.keyCode===13&&K("submit",this,arguments)});else return!1},teardown:function(a){f.event.remove(this,".specialSubmit")}});if(!f.support.changeBubbles){var H,I=function(a){var b=a.type,c=a.value;b==="radio"||b==="checkbox"?c=a.checked:b==="select-multiple"?c=a.selectedIndex>-1?f.map(a.options,function(a){return a.selected}).join("-"):"":f.nodeName(a,"select")&&(c=a.selectedIndex);return c},J=function J(a){var c=a.target,d,e;if(!!y.test(c.nodeName)&&!c.readOnly){d=f._data(c,"_change_data"),e=I(c),(a.type!=="focusout"||c.type!=="radio")&&f._data(c,"_change_data",e);if(d===b||e===d)return;if(d!=null||e)a.type="change",a.liveFired=b,f.event.trigger(a,arguments[1],c)}};f.event.special.change={filters:{focusout:J,beforedeactivate:J,click:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(c==="radio"||c==="checkbox"||f.nodeName(b,"select"))&&J.call(this,a)},keydown:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(a.keyCode===13&&!f.nodeName(b,"textarea")||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&J.call(this,a)},beforeactivate:function(a){var b=a.target;f._data(b,"_change_data",I(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in H)f.event.add(this,c+".specialChange",H[c]);return y.test(this.nodeName)},teardown:function(a){f.event.remove(this,".specialChange");return y.test(this.nodeName)}},H=f.event.special.change.filters,H.focus=H.beforeactivate}f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){function e(a){var c=f.event.fix(a);c.type=b,c.originalEvent={},f.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var d=0;f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.each(["bind","one"],function(a,c){f.fn[c]=function(a,d,e){var g;if(typeof a=="object"){for(var h in a)this[c](h,d,a[h],e);return this}if(arguments.length===2||d===!1)e=d,d=b;c==="one"?(g=function(a){f(this).unbind(a,g);return e.apply(this,arguments)},g.guid=e.guid||f.guid++):g=e;if(a==="unload"&&c!=="one")this.one(a,d,e);else for(var i=0,j=this.length;i<j;i++)f.event.add(this[i],a,g,d);return this}}),f.fn.extend({unbind:function(a,b){if(typeof a=="object"&&!a.preventDefault)for(var c in a)this.unbind(c,a[c]);else for(var d=0,e=this.length;d<e;d++)f.event.remove(this[d],a,b);return this},delegate:function(a,b,c,d){return this.live(b,c,d,a)},undelegate:function(a,b,c){return arguments.length===0?this.unbind("live"):this.die(b,null,c,a)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f.data(this,"lastToggle"+a.guid)||0)%d;f.data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var L={focus:"focusin",blur:"focusout",mouseenter:"mouseover",mouseleave:"mouseout"};f.each(["live","die"],function(a,c){f.fn[c]=function(a,d,e,g){var h,i=0,j,k,l,m=g||this.selector,n=g?this:f(this.context);if(typeof a=="object"&&!a.preventDefault){for(var o in a)n[c](o,d,a[o],m);return this}if(c==="die"&&!a&&g&&g.charAt(0)==="."){n.unbind(g);return this}if(d===!1||f.isFunction(d))e=d||D,d=b;a=(a||"").split(" ");while((h=a[i++])!=null){j=x.exec(h),k="",j&&(k=j[0],h=h.replace(x,""));if(h==="hover"){a.push("mouseenter"+k,"mouseleave"+k);continue}l=h,L[h]?(a.push(L[h]+k),h=h+k):h=(L[h]||h)+k;if(c==="live")for(var p=0,q=n.length;p<q;p++)f.event.add(n[p],"live."+N(h,m),{data:d,selector:m,handler:e,origType:h,origHandler:e,preType:l});else n.unbind("live."+N(h,m),e)}return this}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}if(i.nodeType===1){f||(i.sizcache=c,i.sizset=g);if(typeof b!="string"){if(i===b){j=!0;break}}else if(k.filter(b,[i]).length>0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}i.nodeType===1&&!f&&(i.sizcache=c,i.sizset=g);if(i.nodeName.toLowerCase()===b){j=i;break}i=i[a]}d[g]=j}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d=0,e=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,f,g){f=f||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return f;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(e.call(n)==="[object Array]")if(!u)f.push.apply(f,n);else if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&f.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&f.push(j[t]);else p(n,f);o&&(k(o,h,f,g),k.uniqueSort(f));return f};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},k.matches=function(a,b){return k(a,null,null,b)},k.matchesSelector=function(a,b){return k(b,null,null,[a]).length>0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e<f;e++){var g,h=l.order[e];if(g=l.leftMatch[h].exec(a)){var j=g[1];g.splice(1,1);if(j.substr(j.length-1)!=="\\"){g[1]=(g[1]||"").replace(i,""),d=l.find[h](g,b,c);if(d!=null){a=a.replace(l.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},k.filter=function(a,c,d,e){var f,g,h=a,i=[],j=c,m=c&&c[0]&&k.isXML(c[0]);while(a&&c.length){for(var n in l.filter)if((f=l.leftMatch[n].exec(a))!=null&&f[2]){var o,p,q=l.filter[n],r=f[1];g=!1,f.splice(1,1);if(r.substr(r.length-1)==="\\")continue;j===i&&(i=[]);if(l.preFilter[n]){f=l.preFilter[n](f,j,d,i,e,m);if(!f)g=o=!0;else if(f===!0)continue}if(f)for(var s=0;(p=j[s])!=null;s++)if(p){o=q(p,f,s,j);var t=e^!!o;d&&o!=null?t?g=!0:j[s]=!1:t&&(i.push(p),g=!0)}if(o!==b){d||(j=i),a=a.replace(l.match[n],"");if(!g)return[];break}}if(a===h)if(g==null)k.error(a);else break;h=a}return j},k.error=function(a){throw"Syntax error, unrecognized expression: "+a};var l=k.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!j.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&k.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&k.filter(b,a,!0)}},"":function(a,b,c){var e,f=d++,g=u;typeof b=="string"&&!j.test(b)&&(b=b.toLowerCase(),e=b,g=t),g("parentNode",b,f,a,e,c)},"~":function(a,b,c){var e,f=d++,g=u;typeof b=="string"&&!j.test(b)&&(b=b.toLowerCase(),e=b,g=t),g("previousSibling",b,f,a,e,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(i,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=d++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){return a.nodeName.toLowerCase()==="input"&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}k.error(e)},CHILD:function(a,b){var c=b[1],d=a;switch(c){case"only":case"first":while(d=d.previousSibling)if(d.nodeType===1)return!1;if(c==="first")return!0;d=a;case"last":while(d=d.nextSibling)if(d.nodeType===1)return!1;return!0;case"nth":var e=b[2],f=b[3];if(e===1&&f===0)return!0;var g=b[0],h=a.parentNode;if(h&&(h.sizcache!==g||!a.nodeIndex)){var i=0;for(d=h.firstChild;d;d=d.nextSibling)d.nodeType===1&&(d.nodeIndex=++i);h.sizcache=g}var j=a.nodeIndex-f;return e===0?j===0:j%e===0&&j/e>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(e.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var f=a.length;c<f;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var r,s;c.documentElement.compareDocumentPosition?r=function(a,b){if(a===b){g=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(r=function(a,b){var c,d,e=[],f=[],h=a.parentNode,i=b.parentNode,j=h;if(a===b){g=!0;return 0}if(h===i)return s(a,b);if(!h)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return s(e[k],f[k]);return k===c?s(a,f[k],-1):s(e[k],b,1)},s=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),k.getText=function(a){var b="",c;for(var d=0;a[d];d++)c=a[d],c.nodeType===3||c.nodeType===4?b+=c.nodeValue:c.nodeType!==8&&(b+=k.getText(c.childNodes));return b},function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g<h;g++)k(a,f[g],d);return k.filter(e,d)};f.find=k,f.expr=k.selectors,f.expr[":"]=f.expr.filters,f.unique=k.uniqueSort,f.text=k.getText,f.isXMLDoc=k.isXML,f.contains=k.contains}();var O=/Until$/,P=/^(?:parents|prevUntil|prevAll)/,Q=/,/,R=/^.[^:#\[\.,]*$/,S=Array.prototype.slice,T=f.expr.match.POS,U={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(W(this,a,!1),"not",a)},filter:function(a){return this.pushStack(W(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(d=0,e=a.length;d<e;d++)i=a[d],j[i]||(j[i]=T.test(i)?f(i,b||this.context):i);while(g&&g.ownerDocument&&g!==b){for(i in j)h=j[i],(h.jquery?h.index(g)>-1:f(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=T.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(l?l.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a=="string")return f.inArray(this[0],a?f(a):this.parent().children());return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(V(c[0])||V(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=S.call(arguments);O.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!U[a]?f.unique(e):e,(this.length>1||Q.test(d))&&P.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var X=/ jQuery\d+="(?:\d+|null)"/g,Y=/^\s+/,Z=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,$=/<([\w:]+)/,_=/<tbody/i,ba=/<|&#?\w+;/,bb=/<(?:script|object|embed|option|style)/i,bc=/checked\s*(?:[^=]|=\s*.checked.)/i,bd=/\/(java|ecma)script/i,be={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};be.optgroup=be.option,be.tbody=be.tfoot=be.colgroup=be.caption=be.thead,be.th=be.td,f.support.htmlSerialize||(be._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(X,""):null;if(typeof a=="string"&&!bb.test(a)&&(f.support.leadingWhitespace||!Y.test(a))&&!be[($.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Z,"<$1></$2>");try{for(var c=0,d=this.length;c<d;c++)this[c].nodeType===1&&(f.cleanData(this[c].getElementsByTagName("*")),this[c].innerHTML=a)}catch(e){this.empty().append(a)}}else f.isFunction(a)?this.each(function(b){var c=f(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bc.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bf(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,bl)}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i=b&&b[0]?b[0].ownerDocument||b[0]:c;a.length===1&&typeof a[0]=="string"&&a[0].length<512&&i===c&&a[0].charAt(0)==="<"&&!bb.test(a[0])&&(f.support.checkClone||!bc.test(a[0]))&&(g=!0,h=f.fragments[a[0]],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[a[0]]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bh(a,d),e=bi(a),g=bi(d);for(h=0;e[h];++h)bh(e[h],g[h])}if(b){bg(a,d);if(c){e=bi(a),g=bi(d);for(h=0;e[h];++h)bg(e[h],g[h])}}return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[];for(var i=0,j;(j=a[i])!=null;i++){typeof j=="number"&&(j+="");if(!j)continue;if(typeof j=="string")if(!ba.test(j))j=b.createTextNode(j);else{j=j.replace(Z,"<$1></$2>");var k=($.exec(j)||["",""])[1].toLowerCase(),l=be[k]||be._default,m=l[0],n=b.createElement("div");n.innerHTML=l[1]+j+l[2];while(m--)n=n.lastChild;if(!f.support.tbody){var o=_.test(j),p=k==="table"&&!o?n.firstChild&&n.firstChild.childNodes:l[1]==="<table>"&&!o?n.childNodes:[];for(var q=p.length-1;q>=0;--q)f.nodeName(p[q],"tbody")&&!p[q].childNodes.length&&p[q].parentNode.removeChild(p[q])}!f.support.leadingWhitespace&&Y.test(j)&&n.insertBefore(b.createTextNode(Y.exec(j)[0]),n.firstChild),j=n.childNodes}var r;if(!f.support.appendChecked)if(j[0]&&typeof (r=j.length)=="number")for(i=0;i<r;i++)bk(j[i]);else bk(j);j.nodeType?h.push(j):h=f.merge(h,j)}if(d){g=function(a){return!a.type||bd.test(a.type)};for(i=0;h[i];i++)if(e&&f.nodeName(h[i],"script")&&(!h[i].type||h[i].type.toLowerCase()==="text/javascript"))e.push(h[i].parentNode?h[i].parentNode.removeChild(h[i]):h[i]);else{if(h[i].nodeType===1){var s=f.grep(h[i].getElementsByTagName("script"),g);h.splice.apply(h,[i+1,0].concat(s))}d.appendChild(h[i])}}return h},cleanData:function(a){var b,c,d=f.cache,e=f.expando,g=f.event.special,h=f.support.deleteExpando;for(var i=0,j;(j=a[i])!=null;i++){if(j.nodeName&&f.noData[j.nodeName.toLowerCase()])continue;c=j[f.expando];if(c){b=d[c]&&d[c][e];if(b&&b.events){for(var k in b.events)g[k]?f.event.remove(j,k):f.removeEvent(j,k,b.handle);b.handle&&(b.handle.elem=null)}h?delete j[f.expando]:j.removeAttribute&&j.removeAttribute(f.expando),delete d[c]}}}});var bm=/alpha\([^)]*\)/i,bn=/opacity=([^)]*)/,bo=/-([a-z])/ig,bp=/([A-Z]|^ms)/g,bq=/^-?\d+(?:px)?$/i,br=/^-?\d/,bs=/^[+\-]=/,bt=/[^+\-\.\de]+/g,bu={position:"absolute",visibility:"hidden",display:"block"},bv=["Left","Right"],bw=["Top","Bottom"],bx,by,bz,bA=function(a,b){return b.toUpperCase()};f.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return f.access(this,a,c,!0,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)})},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bx(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{zIndex:!0,fontWeight:!0,opacity:!0,zoom:!0,lineHeight:!0,widows:!0,orphans:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d;if(h==="number"&&isNaN(d)||d==null)return;h==="string"&&bs.test(d)&&(d=+d.replace(bt,"")+parseFloat(f.css(a,c))),h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(bx)return bx(a,c)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]},camelCase:function(a){return a.replace(bo,bA)}}),f.curCSS=f.css,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){var e;if(c){a.offsetWidth!==0?e=bB(a,b,d):f.swap(a,bu,function(){e=bB(a,b,d)});if(e<=0){e=bx(a,b,b),e==="0px"&&bz&&(e=bz(a,b,b));if(e!=null)return e===""||e==="auto"?"0px":e}if(e<0||e==null){e=a.style[b];return e===""||e==="auto"?"0px":e}return typeof e=="string"?e:e+"px"}},set:function(a,b){if(!bq.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bn.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle;c.zoom=1;var e=f.isNaN(b)?"":"alpha(opacity="+b*100+")",g=d&&d.filter||c.filter||"";c.filter=bm.test(g)?g.replace(bm,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bx(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(by=function(a,c){var d,e,g;c=c.replace(bp,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bz=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bq.test(d)&&br.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bx=by||bz,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV;try{bU=e.href}catch(bW){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?f.extend(!0,a,f.ajaxSettings,b):(b=a,a=f.extend(!0,f.ajaxSettings,b));for(var c in{context:1,url:1})c in b?a[c]=b[c]:c in f.ajaxSettings&&(a[c]=f.ajaxSettings[c]);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":"*/*"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML}},ajaxPrefilter:bX(bS),ajaxTransport:bX(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a?4:0;var o,r,u,w=l?b$(d,v,l):b,x,y;if(a>=200&&a<300||a===304){if(d.ifModified){if(x=v.getResponseHeader("Last-Modified"))f.lastModified[k]=x;if(y=v.getResponseHeader("Etag"))f.etag[k]=y}if(a===304)c="notmodified",o=!0;else try{r=b_(d,w),c="success",o=!0}catch(z){c="parsererror",u=z}}else{u=c;if(!c||a)c="error",a<0&&(a=0)}v.status=a,v.statusText=c,o?h.resolveWith(e,[r,c,v]):h.rejectWith(e,[v,c,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.resolveWith(e,[v,c]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f._Deferred(),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.done,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bY(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", */*; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bY(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){status<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)bZ(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var ca=f.now(),cb=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+ca++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cb.test(b.url)||e&&cb.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cb,l),b.url===j&&(e&&(k=k.replace(cb,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cc=a.ActiveXObject?function(){for(var a in ce)ce[a](0,1)}:!1,cd=0,ce;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&cf()||cg()}:cf,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cc&&delete ce[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cd,cc&&(ce||(ce={},f(a).unload(cc)),ce[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ch={},ci,cj,ck=/^(?:toggle|show|hide)$/,cl=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cm,cn=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],co,cp=a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cs("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),e===""&&f.css(d,"display")==="none"&&f._data(d,"olddisplay",ct(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(cs("hide",3),a,b,c);for(var d=0,e=this.length;d<e;d++)if(this[d].style){var g=f.css(this[d],"display");g!=="none"&&!f._data(this[d],"olddisplay")&&f._data(this[d],"olddisplay",g)}for(d=0;d<e;d++)this[d].style&&(this[d].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(cs("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);return this[e.queue===!1?"each":"queue"](function(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]),h=a[g];if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(f.support.inlineBlockNeedsLayout?(j=ct(this.nodeName),j==="inline"?this.style.display="inline-block":(this.style.display="inline",this.style.zoom=1)):this.style.display="inline-block")),b.animatedProperties[g]=f.isArray(h)?h[1]:b.specialEasing&&b.specialEasing[g]||b.easing||"swing"}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)k=new f.fx(this,b,i),h=a[i],ck.test(h)?k[h==="toggle"?d?"show":"hide":h]():(l=cl.exec(h),m=k.cur(),l?(n=parseFloat(l[2]),o=l[3]||(f.cssNumber[g]?"":"px"),o!=="px"&&(f.style(this,i,(n||1)+o),m=(n||1)/k.cur()*m,f.style(this,i,m+o)),l[1]&&(n=(l[1]==="-="?-1:1)*n+m),k.custom(m,n,o)):k.custom(m,h,""));return!0})},stop:function(a,b){a&&this.queue([]),this.each(function(){var a=f.timers,c=a.length;b||f._unmark(!0,this);while(c--)a[c].elem===this&&(b&&a[c](!0),a.splice(c,1))}),b||this.dequeue();return this}}),f.each({slideDown:cs("show",1),slideUp:cs("hide",1),slideToggle:cs("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default,d.old=d.complete,d.complete=function(a){d.queue!==!1?f.dequeue(this):a!==!1&&f._unmark(this),f.isFunction(d.old)&&d.old.call(this)};return d},easing:{linear:function(a,b,c,d){return c+d*a},swing:function(a,b,c,d){return(-Math.cos(a*Math.PI)/2+.5)*d+c}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,b,c){function h(a){return d.step(a)}var d=this,e=f.fx,g;this.startTime=co||cq(),this.start=a,this.end=b,this.unit=c||this.unit||(f.cssNumber[this.prop]?"":"px"),this.now=this.start,this.pos=this.state=0,h.elem=this.elem,h()&&f.timers.push(h)&&!cm&&(cp?(cm=1,g=function(){cm&&(cp(g),e.tick())},cp(g)):cm=setInterval(e.tick,e.interval))},show:function(){this.options.orig[this.prop]=f.style(this.elem,this.prop),this.options.show=!0,this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b=co||cq(),c=!0,d=this.elem,e=this.options,g,h;if(a||b>=e.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),e.animatedProperties[this.prop]=!0;for(g in e.animatedProperties)e.animatedProperties[g]!==!0&&(c=!1);if(c){e.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){d.style["overflow"+b]=e.overflow[a]}),e.hide&&f(d).hide();if(e.hide||e.show)for(var i in e.animatedProperties)f.style(d,i,e.orig[i]);e.complete.call(d)}return!1}e.duration==Infinity?this.now=b:(h=b-this.startTime,this.state=h/e.duration,this.pos=f.easing[e.animatedProperties[this.prop]](this.state,h,0,1,e.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a=f.timers,b=a.length;while(b--)a[b]()||a.splice(b,1);a.length||f.fx.stop()},interval:13,stop:function(){clearInterval(cm),cm=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit:a.elem[a.prop]=a.now}}}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var cu=/^t(?:able|d|h)$/i,cv=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?f.fn.offset=function(a){var b=this[0],c;if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);try{c=b.getBoundingClientRect()}catch(d){}var e=b.ownerDocument,g=e.documentElement;if(!c||!f.contains(g,b))return c?{top:c.top,left:c.left}:{top:0,left:0};var h=e.body,i=cw(e),j=g.clientTop||h.clientTop||0,k=g.clientLeft||h.clientLeft||0,l=i.pageYOffset||f.support.boxModel&&g.scrollTop||h.scrollTop,m=i.pageXOffset||f.support.boxModel&&g.scrollLeft||h.scrollLeft,n=c.top+l-j,o=c.left+m-k;return{top:n,left:o}}:f.fn.offset=function(a){var b=this[0];if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);f.offset.initialize();var c,d=b.offsetParent,e=b,g=b.ownerDocument,h=g.documentElement,i=g.body,j=g.defaultView,k=j?j.getComputedStyle(b,null):b.currentStyle,l=b.offsetTop,m=b.offsetLeft;while((b=b.parentNode)&&b!==i&&b!==h){if(f.offset.supportsFixedPosition&&k.position==="fixed")break;c=j?j.getComputedStyle(b,null):b.currentStyle,l-=b.scrollTop,m-=b.scrollLeft,b===d&&(l+=b.offsetTop,m+=b.offsetLeft,f.offset.doesNotAddBorder&&(!f.offset.doesAddBorderForTableAndCells||!cu.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),e=d,d=b.offsetParent),f.offset.subtractsBorderForOverflowNotVisible&&c.overflow!=="visible"&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),k=c}if(k.position==="relative"||k.position==="static")l+=i.offsetTop,m+=i.offsetLeft;f.offset.supportsFixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},f.offset={initialize:function(){var a=c.body,b=c.createElement("div"),d,e,g,h,i=parseFloat(f.css(a,"marginTop"))||0,j="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";f.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),d=b.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,this.doesNotAddBorder=e.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,e.style.position="fixed",e.style.top="20px",this.supportsFixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",this.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),f.offset.initialize=f.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.offset.initialize(),f.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cv.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cv.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cw(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cw(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){return this[0]?parseFloat(f.css(this[0],d,"padding")):null},f.fn["outer"+c]=function(a){return this[0]?parseFloat(f.css(this[0],d,a?"margin":"border")):null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c];return e.document.compatMode==="CSS1Compat"&&g||e.document.body["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var h=f.css(e,d),i=parseFloat(h);return f.isNaN(i)?h:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window);
\ No newline at end of file diff --git a/devtools/client/inspector/markup/test/lib_jquery_1.7_min.js b/devtools/client/inspector/markup/test/lib_jquery_1.7_min.js new file mode 100644 index 000000000..3ca5e0f5d --- /dev/null +++ b/devtools/client/inspector/markup/test/lib_jquery_1.7_min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.7 jquery.com | jquery.org/license */ +(function(a,b){function cA(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cx(a){if(!cm[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cn||(cn=c.createElement("iframe"),cn.frameBorder=cn.width=cn.height=0),b.appendChild(cn);if(!co||!cn.createElement)co=(cn.contentWindow||cn.contentDocument).document,co.write((c.compatMode==="CSS1Compat"?"<!doctype html>":"")+"<html><body>"),co.close();d=co.createElement(a),co.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cn)}cm[a]=e}return cm[a]}function cw(a,b){var c={};f.each(cs.concat.apply([],cs.slice(0,b)),function(){c[this]=a});return c}function cv(){ct=b}function cu(){setTimeout(cv,0);return ct=f.now()}function cl(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ck(){try{return new a.XMLHttpRequest}catch(b){}}function ce(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function cd(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function cc(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bG.test(a)?d(a,e):cc(a+"["+(typeof e=="object"||f.isArray(e)?b:"")+"]",e,c,d)});else if(!c&&b!=null&&typeof b=="object")for(var e in b)cc(a+"["+e+"]",b[e],c,d);else d(a,b)}function cb(a,c){var d,e,g=f.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((g[d]?a:e||(e={}))[d]=c[d]);e&&f.extend(!0,a,e)}function ca(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bV,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=ca(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=ca(a,c,d,e,"*",g));return l}function b_(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bR),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bE(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=b==="width"?bz:bA;if(d>0){c!=="border"&&f.each(e,function(){c||(d-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?d+=parseFloat(f.css(a,c+this))||0:d-=parseFloat(f.css(a,"border"+this+"Width"))||0});return d+"px"}d=bB(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0,c&&f.each(e,function(){d+=parseFloat(f.css(a,"padding"+this))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+this+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+this))||0)});return d+"px"}function br(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bi,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bq(a){var b=(a.nodeName||"").toLowerCase();b==="input"?bp(a):b!=="script"&&typeof a.getElementsByTagName!="undefined"&&f.grep(a.getElementsByTagName("input"),bp)}function bp(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bo(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bn(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bm(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c,d,e,g=f._data(a),h=f._data(b,g),i=g.events;if(i){delete h.handle,h.events={};for(c in i)for(d=0,e=i[c].length;d<e;d++)f.event.add(b,c+(i[c][d].namespace?".":"")+i[c][d].namespace,i[c][d],i[c][d].data)}h.data&&(h.data=f.extend({},h.data))}}function bl(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function X(a){var b=Y.split(" "),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function W(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(R.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function V(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function N(){return!0}function M(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c<d;c++)b[a[c]]=!0;return b}var c=a.document,d=a.navigator,e=a.location,f=function(){function K(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(K,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=/-([a-z]|[0-9])/ig,x=/^-ms-/,y=function(a,b){return(b+"").toUpperCase()},z=d.userAgent,A,B,C,D=Object.prototype.toString,E=Object.prototype.hasOwnProperty,F=Array.prototype.push,G=Array.prototype.slice,H=String.prototype.trim,I=Array.prototype.indexOf,J={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7",length:0,size:function(){return this.length},toArray:function(){return G.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?F.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),B.add(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(G.apply(this,arguments),"slice",G.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:F,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;B.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!B){B=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",C,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",C),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&K()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return a!=null&&m.test(a)&&!isNaN(a)},type:function(a){return a==null?String(a):J[D.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!E.call(a,"constructor")&&!E.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||E.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(x,"ms-").replace(w,y)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:H?function(a){return a==null?"":H.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?F.call(c,a):e.merge(c,a)}return c},inArray:function(a,b,c){var d;if(b){if(I)return I.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=G.call(arguments,2),g=function(){return a.apply(c,f.concat(G.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h){var i=a.length;if(typeof c=="object"){for(var j in c)e.access(a,j,c[j],f,g,d);return a}if(d!==b){f=!h&&f&&e.isFunction(d);for(var k=0;k<i;k++)g(a[k],c,f?d.call(a[k],k,g(a[k],c)):d,h);return a}return i?g(a[0],c):b},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=s.exec(a)||t.exec(a)||u.exec(a)||a.indexOf("compatible")<0&&v.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(d,f){f&&f instanceof e&&!(f instanceof a)&&(f=a(f));return e.fn.init.call(this,d,f,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){J["[object "+b+"]"]=b.toLowerCase()}),A=e.uaMatch(z),A.browser&&(e.browser[A.browser]=!0,e.browser.version=A.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?C=function(){c.removeEventListener("DOMContentLoaded",C,!1),e.ready()}:c.attachEvent&&(C=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",C),e.ready())}),typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return e});return e}(),g={};f.Callbacks=function(a){a=a?g[a]||h(a):{};var c=[],d=[],e,i,j,k,l,m=function(b){var d,e,g,h,i;for(d=0,e=b.length;d<e;d++)g=b[d],h=f.type(g),h==="array"?m(g):h==="function"&&(!a.unique||!o.has(g))&&c.push(g)},n=function(b,f){f=f||[],e=!a.memory||[b,f],i=!0,l=j||0,j=0,k=c.length;for(;c&&l<k;l++)if(c[l].apply(b,f)===!1&&a.stopOnFalse){e=!0;break}i=!1,c&&(a.once?e===!0?o.disable():c=[]:d&&d.length&&(e=d.shift(),o.fireWith(e[0],e[1])))},o={add:function(){if(c){var a=c.length;m(arguments),i?k=c.length:e&&e!==!0&&(j=a,n(e[0],e[1]))}return this},remove:function(){if(c){var b=arguments,d=0,e=b.length;for(;d<e;d++)for(var f=0;f<c.length;f++)if(b[d]===c[f]){i&&f<=k&&(k--,f<=l&&l--),c.splice(f--,1);if(a.unique)break}}return this},has:function(a){if(c){var b=0,d=c.length;for(;b<d;b++)if(a===c[b])return!0}return!1},empty:function(){c=[];return this},disable:function(){c=d=e=b;return this},disabled:function(){return!c},lock:function(){d=b,(!e||e===!0)&&o.disable();return this},locked:function(){return!d},fireWith:function(b,c){d&&(i?a.once||d.push([b,c]):(!a.once||!e)&&n(b,c));return this},fire:function(){o.fireWith(this,arguments);return this},fired:function(){return!!e}};return o};var i=[].slice;f.extend({Deferred:function(a){var b=f.Callbacks("once memory"),c=f.Callbacks("once memory"),d=f.Callbacks("memory"),e="pending",g={resolve:b,reject:c,notify:d},h={done:b.add,fail:c.add,progress:d.add,state:function(){return e},isResolved:b.fired,isRejected:c.fired,then:function(a,b,c){i.done(a).fail(b).progress(c);return this},always:function(){return i.done.apply(i,arguments).fail.apply(i,arguments)},pipe:function(a,b,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[b,"reject"],progress:[c,"notify"]},function(a,b){var c=b[0],e=b[1],g;f.isFunction(c)?i[a](function(){g=c.apply(this,arguments),g&&f.isFunction(g.promise)?g.promise().then(d.resolve,d.reject,d.notify):d[e+"With"](this===i?d:this,[g])}):i[a](d[e])})}).promise()},promise:function(a){if(a==null)a=h;else for(var b in h)a[b]=h[b];return a}},i=h.promise({}),j;for(j in g)i[j]=g[j].fire,i[j+"With"]=g[j].fireWith;i.done(function(){e="resolved"},c.disable,d.lock).fail(function(){e="rejected"},b.disable,d.lock),a&&a.call(i,i);return i},when:function(a){function m(a){return function(b){e[a]=arguments.length>1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c<d;c++)b[c]&&b[c].promise&&f.isFunction(b[c].promise)?b[c].promise().then(l(c),j.reject,m(c)):--g;g||j.resolveWith(j,b)}else j!==a&&j.resolveWith(j,d?[a]:[]);return k}}),f.support=function(){var a=c.createElement("div"),b=c.documentElement,d,e,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u;a.setAttribute("className","t"),a.innerHTML=" <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/><nav></nav>",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=a.getElementsByTagName("input")[0],k={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,unknownElems:!!a.getElementsByTagName("nav").length,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:a.className!=="t",enctype:!!c.createElement("form").enctype,submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,k.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,k.optDisabled=!h.disabled;try{delete a.test}catch(v){k.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function(){k.noCloneEvent=!1}),a.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),k.radioValue=i.value==="t",i.setAttribute("checked","checked"),a.appendChild(i),l=c.createDocumentFragment(),l.appendChild(a.lastChild),k.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",m=c.getElementsByTagName("body")[0],o=c.createElement(m?"div":"body"),p={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},m&&f.extend(p,{position:"absolute",left:"-999px",top:"-999px"});for(t in p)o.style[t]=p[t];o.appendChild(a),n=m||b,n.insertBefore(o,n.firstChild),k.appendChecked=i.checked,k.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,k.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="<div style='width:4px;'></div>",k.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>",q=a.getElementsByTagName("td"),u=q[0].offsetHeight===0,q[0].style.display="",q[1].style.display="none",k.reliableHiddenOffsets=u&&q[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",a.appendChild(j),k.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(a.attachEvent)for(t in{submit:1,change:1,focusin:1})s="on"+t,u=s in a,u||(a.setAttribute(s,"return;"),u=typeof a[s]=="function"),k[t+"Bubbles"]=u;f(function(){var a,b,d,e,g,h,i=1,j="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",l="visibility:hidden;border:0;",n="style='"+j+"border:5px solid #000;padding:0;'",p="<div "+n+"><div></div></div>"+"<table "+n+" cellpadding='0' cellspacing='0'>"+"<tr><td></td></tr></table>";m=c.getElementsByTagName("body")[0];!m||(a=c.createElement("div"),a.style.cssText=l+"width:0;height:0;position:static;top:0;margin-top:"+i+"px",m.insertBefore(a,m.firstChild),o=c.createElement("div"),o.style.cssText=j+l,o.innerHTML=p,a.appendChild(o),b=o.firstChild,d=b.firstChild,g=b.nextSibling.firstChild.firstChild,h={doesNotAddBorder:d.offsetTop!==5,doesAddBorderForTableAndCells:g.offsetTop===5},d.style.position="fixed",d.style.top="20px",h.fixedPosition=d.offsetTop===20||d.offsetTop===15,d.style.position=d.style.top="",b.style.overflow="hidden",b.style.position="relative",h.subtractsBorderForOverflowNotVisible=d.offsetTop===-5,h.doesNotIncludeMarginInBodyOffset=m.offsetTop!==i,m.removeChild(a),o=a=null,f.extend(k,h))}),o.innerHTML="",n.removeChild(o),o=l=g=h=m=j=a=i=null;return k}(),f.boxModel=f.support.boxModel;var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[f.expando]:a[f.expando]&&f.expando,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[f.expando]=n=++f.uuid:n=f.expando),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[f.expando]:f.expando;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)?b=b:b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" "));for(e=0,g=b.length;e<g;e++)delete d[b[e]];if(!(c?m:f.isEmptyObject)(d))return}}if(!c){delete j[k].data;if(!m(j[k]))return}f.support.deleteExpando||!j.setInterval?delete j[k]:j[k]=null,i&&(f.support.deleteExpando?delete a[f.expando]:a.removeAttribute?a.removeAttribute(f.expando):a[f.expando]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d,e,g,h=null;if(typeof a=="undefined"){if(this.length){h=f.data(this[0]);if(this[0].nodeType===1&&!f._data(this[0],"parsedAttrs")){e=this[0].attributes;for(var i=0,j=e.length;i<j;i++)g=e[i].name,g.indexOf("data-")===0&&(g=f.camelCase(g.substring(5)),l(this[0],g,h[g]));f._data(this[0],"parsedAttrs",!0)}}return h}if(typeof a=="object")return this.each(function(){f.data(this,a)});d=a.split("."),d[1]=d[1]?"."+d[1]:"";if(c===b){h=this.triggerHandler("getData"+d[1]+"!",[d[0]]),h===b&&this.length&&(h=f.data(this[0],a),h=l(this[0],a,h));return h===b&&d[1]?this.data(d[0]):h}return this.each(function(){var b=f(this),e=[d[0],c];b.triggerHandler("setData"+d[1]+"!",e),f.data(this,a,c),b.triggerHandler("changeData"+d[1]+"!",e)})},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){typeof a!="string"&&(c=a,a="fx");if(c===b)return f.queue(this[0],a);return this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function m(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark",l;while(g--)if(l=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f.Callbacks("once memory"),!0))h++,l.add(m);m();return d.promise()}});var o=/[\n\t\r]/g,p=/\s+/,q=/\r/g,r=/^(?:button|input)$/i,s=/^(?:button|input|object|select|textarea)$/i,t=/^a(?:rea)?$/i,u=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,v=f.support.getSetAttribute,w,x,y;f.fn.extend({attr:function(a,b){return f.access(this,a,b,!0,f.attr)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,a,b,!0,f.prop)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{g=" "+e.className+" ";for(h=0,i=b.length;h<i;h++)~g.indexOf(" "+b[h]+" ")||(g+=b[h]+" ");e.className=f.trim(g)}}}return this},removeClass:function(a){var c,d,e,g,h,i,j;if(f.isFunction(a))return this.each(function(b){f(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(p);for(d=0,e=this.length;d<e;d++){g=this[d];if(g.nodeType===1&&g.className)if(a){h=(" "+g.className+" ").replace(o," ");for(i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){f(this).toggleClass(a.call(this,c,this.className,b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(p);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(o," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];if(!arguments.length){if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}return b}e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c<d;c++){e=i[c];if(e.selected&&(f.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!f.nodeName(e.parentNode,"optgroup"))){b=f(e).val();if(j)return b;h.push(b)}}if(j&&!h.length&&i.length)return f(i[g]).val();return h},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!a||j===3||j===8||j===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);if(!("getAttribute"in a))return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return b}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g},removeAttr:function(a,b){var c,d,e,g,h=0;if(a.nodeType===1){d=(b||"").split(p),g=d.length;for(;h<g;h++)e=d[h].toLowerCase(),c=f.propFix[e]||e,f.attr(a,e,""),a.removeAttribute(v?e:c),u.test(e)&&c in a&&(a[c]=!1)}},attrHooks:{type:{set:function(a,b){if(r.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},value:{get:function(a,b){if(w&&f.nodeName(a,"button"))return w.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(w&&f.nodeName(a,"button"))return w.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,g,h,i=a.nodeType;if(!a||i===3||i===8||i===2)return b;h=i!==1||!f.isXMLDoc(a),h&&(c=f.propFix[c]||c,g=f.propHooks[c]);return d!==b?g&&"set"in g&&(e=g.set(a,d,c))!==b?e:a[c]=d:g&&"get"in g&&(e=g.get(a,c))!==null?e:a[c]},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):s.test(a.nodeName)||t.test(a.nodeName)&&a.href?0:b}}}}),f.attrHooks.tabindex=f.propHooks.tabIndex,x={get:function(a,c){var d,e=f.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},v||(y={name:!0,id:!0},w=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&(y[c]?d.nodeValue!=="":d.specified)?d.nodeValue:b},set:function(a,b,d){var e=a.getAttributeNode(d);e||(e=c.createAttribute(d),a.setAttributeNode(e));return e.nodeValue=b+""}},f.attrHooks.tabindex.set=w.set,f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})}),f.attrHooks.contenteditable={get:w.get,set:function(a,b,c){b===""&&(b="false"),w.set(a,b,c)}}),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex);return null}})),f.support.enctype||(f.propFix.enctype="encoding"),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var z=/\.(.*)$/,A=/^(?:textarea|input|select)$/i,B=/\./g,C=/ /g,D=/[^\w\s.|`]/g,E=/^([^\.]*)?(?:\.(.+))?$/,F=/\bhover(\.\S+)?/,G=/^key/,H=/^(?:mouse|contextmenu)|click/,I=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,J=function(a){var b=I.exec(a);b&& +(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},K=function(a,b){return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||a.id===b[2])&&(!b[3]||b[3].test(a.className))},L=function(a){return f.event.special.hover?a:a.replace(F,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=L(c).split(" ");for(k=0;k<c.length;k++){l=E.exec(c[k])||[],m=l[1],n=(l[2]||"").split(".").sort(),s=f.event.special[m]||{},m=(g?s.delegateType:s.bindType)||m,s=f.event.special[m]||{},o=f.extend({type:m,origType:l[1],data:e,handler:d,guid:d.guid,selector:g,namespace:n.join(".")},p),g&&(o.quick=J(g),!o.quick&&f.expr.match.POS.test(g)&&(o.isPositional=!0)),r=j[m];if(!r){r=j[m]=[],r.delegateCount=0;if(!s.setup||s.setup.call(a,e,n,i)===!1)a.addEventListener?a.addEventListener(m,i,!1):a.attachEvent&&a.attachEvent("on"+m,i)}s.add&&(s.add.call(a,o),o.handler.guid||(o.handler.guid=d.guid)),g?r.splice(r.delegateCount++,0,o):r.push(o),f.event.global[m]=!0}a=null}},global:{},remove:function(a,b,c,d){var e=f.hasData(a)&&f._data(a),g,h,i,j,k,l,m,n,o,p,q;if(!!e&&!!(m=e.events)){b=L(b||"").split(" ");for(g=0;g<b.length;g++){h=E.exec(b[g])||[],i=h[1],j=h[2];if(!i){j=j?"."+j:"";for(l in m)f.event.remove(a,l+j,c,d);return}n=f.event.special[i]||{},i=(d?n.delegateType:n.bindType)||i,p=m[i]||[],k=p.length,j=j?new RegExp("(^|\\.)"+j.split(".").sort().join("\\.(?:.*\\.)?")+"(\\.|$)"):null;if(c||j||d||n.remove)for(l=0;l<p.length;l++){q=p[l];if(!c||c.guid===q.guid)if(!j||j.test(q.namespace))if(!d||d===q.selector||d==="**"&&q.selector)p.splice(l--,1),q.selector&&p.delegateCount--,n.remove&&n.remove.call(a,q)}else p.length=0;p.length===0&&k!==p.length&&((!n.teardown||n.teardown.call(a,j)===!1)&&f.removeEvent(a,i,e.handle),delete m[i])}f.isEmptyObject(m)&&(o=e.handle,o&&(o.elem=null),f.removeData(a,["events","handle"],!0))}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){if(!e||e.nodeType!==3&&e.nodeType!==8){var h=c.type||c,i=[],j,k,l,m,n,o,p,q,r,s;h.indexOf("!")>=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"",(g||!e)&&c.preventDefault();if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,n=null;for(m=e.parentNode;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;l<r.length;l++){m=r[l][0],c.type=r[l][1],q=(f._data(m,"events")||{})[c.type]&&f._data(m,"handle"),q&&q.apply(m,d),q=o&&m[o],q&&f.acceptData(m)&&q.apply(m,d);if(c.isPropagationStopped())break}c.type=h,c.isDefaultPrevented()||(!p._default||p._default.apply(e.ownerDocument,d)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)&&o&&e[h]&&(h!=="focus"&&h!=="blur"||c.target.offsetWidth!==0)&&!f.isWindow(e)&&(n=e[o],n&&(e[o]=null),f.event.triggered=h,e[h](),f.event.triggered=b,n&&(e[o]=n));return c.result}},dispatch:function(c){c=f.event.fix(c||a.event);var d=(f._data(this,"events")||{})[c.type]||[],e=d.delegateCount,g=[].slice.call(arguments,0),h=!c.exclusive&&!c.namespace,i=(f.event.special[c.type]||{}).handle,j=[],k,l,m,n,o,p,q,r,s,t,u;g[0]=c,c.delegateTarget=this;if(e&&!c.target.disabled&&(!c.button||c.type!=="click"))for(m=c.target;m!=this;m=m.parentNode||this){o={},q=[];for(k=0;k<e;k++)r=d[k],s=r.selector,t=o[s],r.isPositional?t=(t||(o[s]=f(s))).index(m)>=0:t===b&&(t=o[s]=r.quick?K(m,r.quick):f(m).is(s)),t&&q.push(r);q.length&&j.push({elem:m,matches:q})}d.length>e&&j.push({elem:this,matches:d.slice(e)});for(k=0;k<j.length&&!c.isPropagationStopped();k++){p=j[k],c.currentTarget=p.elem;for(l=0;l<p.matches.length&&!c.isImmediatePropagationStopped();l++){r=p.matches[l];if(h||!c.namespace&&!r.namespace||c.namespace_re&&c.namespace_re.test(r.namespace))c.data=r.data,c.handleObj=r,n=(i||r.handler).apply(p.elem,g),n!==b&&(c.result=n,n===!1&&(c.preventDefault(),c.stopPropagation()))}}return c.result},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode);return a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement wheelDelta".split(" "),filter:function(a,d){var e,f,g,h=d.button,i=d.fromElement;a.pageX==null&&d.clientX!=null&&(e=a.target.ownerDocument||c,f=e.documentElement,g=e.body,a.pageX=d.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=d.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?d.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0);return a}},fix:function(a){if(a[f.expando])return a;var d,e,g=a,h=f.event.fixHooks[a.type]||{},i=h.props?this.props.concat(h.props):this.props;a=f.Event(g);for(d=i.length;d;)e=i[--d],a[e]=g[e];a.target||(a.target=g.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey===b&&(a.metaKey=a.ctrlKey);return h.filter?h.filter(a,g):a},special:{ready:{setup:f.bindReady},focus:{delegateType:"focusin",noBubble:!0},blur:{delegateType:"focusout",noBubble:!0},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=f.extend(new f.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?f.event.trigger(e,null,b):f.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},f.event.handle=f.event.dispatch,f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!(this instanceof f.Event))return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?N:M):this.type=a,b&&f.extend(this,b),this.timeStamp=a&&a.timeStamp||f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=N;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=N;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=N,this.stopPropagation()},isDefaultPrevented:M,isPropagationStopped:M,isImmediatePropagationStopped:M},f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]=f.event.special[b]={delegateType:b,bindType:b,handle:function(a){var b=this,c=a.relatedTarget,d=a.handleObj,e=d.selector,g,h;if(!c||d.origType===a.type||c!==b&&!f.contains(b,c))g=a.type,a.type=d.origType,h=d.handler.apply(this,arguments),a.type=g;return h}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(){if(f.nodeName(this,"form"))return!1;f.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=f.nodeName(c,"input")||f.nodeName(c,"button")?c.form:b;d&&!d._submit_attached&&(f.event.add(d,"submit._submit",function(a){this.parentNode&&f.event.simulate("submit",this.parentNode,a,!0)}),d._submit_attached=!0)})},teardown:function(){if(f.nodeName(this,"form"))return!1;f.event.remove(this,"._submit")}}),f.support.changeBubbles||(f.event.special.change={setup:function(){if(A.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")f.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),f.event.add(this,"click._change",function(a){this._just_changed&&(this._just_changed=!1,f.event.simulate("change",this,a,!0))});return!1}f.event.add(this,"beforeactivate._change",function(a){var b=a.target;A.test(b.nodeName)&&!b._change_attached&&(f.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&f.event.simulate("change",this.parentNode,a,!0)}),b._change_attached=!0)})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){f.event.remove(this,"._change");return A.test(this.nodeName)}}),f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){var d=0,e=function(a){f.event.simulate(b,a.target,f.event.fix(a),!0)};f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.fn.extend({on:function(a,c,d,e,g){var h,i;if(typeof a=="object"){typeof c!="string"&&(d=c,c=b);for(i in a)this.on(i,c,d,a[i],g);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=M;else if(!e)return this;g===1&&(h=e,e=function(a){f().off(a);return h.apply(this,arguments)},e.guid=h.guid||(h.guid=f.guid++));return this.each(function(){f.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on.call(this,a,b,c,d,1)},off:function(a,c,d){if(a&&a.preventDefault&&a.handleObj){var e=a.handleObj;f(a.delegateTarget).off(e.namespace?e.type+"."+e.namespace:e.type,e.selector,e.handler);return this}if(typeof a=="object"){for(var g in a)this.off(g,c,a[g]);return this}if(c===!1||typeof c=="function")d=c,c=b;d===!1&&(d=M);return this.each(function(){f.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){f(this.context).on(a,this.selector,b,c);return this},die:function(a,b){f(this.context).off(a,this.selector||"**",b);return this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length==1?this.off(a,"**"):this.off(b,a,c)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f._data(this,"lastToggle"+a.guid)||0)%d;f._data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),G.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),H.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}if(j.nodeType===1){g||(j[d]=c,j.sizset=h);if(typeof b!="string"){if(j===b){k=!0;break}}else if(m.filter(b,[j]).length>0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}j.nodeType===1&&!g&&(j[d]=c,j.sizset=h);if(j.nodeName.toLowerCase()===b){k=j;break}j=j[a]}e[h]=k}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},m.matches=function(a,b){return m(a,null,null,b)},m.matchesSelector=function(a,b){return m(b,null,null,[a]).length>0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e<f;e++){h=o.order[e];if(g=o.leftMatch[h].exec(a)){i=g[1],g.splice(1,1);if(i.substr(i.length-1)!=="\\"){g[1]=(g[1]||"").replace(j,""),d=o.find[h](g,b,c);if(d!=null){a=a.replace(o.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},m.filter=function(a,c,d,e){var f,g,h,i,j,k,l,n,p,q=a,r=[],s=c,t=c&&c[0]&&m.isXML(c[0]);while(a&&c.length){for(h in o.filter)if((f=o.leftMatch[h].exec(a))!=null&&f[2]){k=o.filter[h],l=f[1],g=!1,f.splice(1,1);if(l.substr(l.length-1)==="\\")continue;s===r&&(r=[]);if(o.preFilter[h]){f=o.preFilter[h](f,s,d,r,e,t);if(!f)g=i=!0;else if(f===!0)continue}if(f)for(n=0;(j=s[n])!=null;n++)j&&(i=k(j,f,n,s),p=e^i,d&&i!=null?p?g=!0:s[n]=!1:p&&(r.push(j),g=!0));if(i!==b){d||(s=r),a=a.replace(o.match[h],"");if(!g)return[];break}}if(a===q)if(g==null)m.error(a);else break;q=a}return s},m.error=function(a){throw"Syntax error, unrecognized expression: "+a};var n=m.getText=function(a){var b,c,d=a.nodeType,e="";if(d){if(d===1){if(typeof a.textContent=="string")return a.textContent;if(typeof a.innerText=="string")return a.innerText.replace(k,"");for(a=a.firstChild;a;a=a.nextSibling)e+=n(a)}else if(d===3||d===4)return a.nodeValue}else for(b=0;c=a[b];b++)c.nodeType!==8&&(e+=n(c));return e},o=m.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!l.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&m.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&m.filter(b,a,!0)}},"":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("parentNode",b,f,a,d,c)},"~":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("previousSibling",b,f,a,d,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(j,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}m.error(e)},CHILD:function(a,b){var c,e,f,g,h,i,j,k=b[1],l=a;switch(k){case"only":case"first":while(l=l.previousSibling)if(l.nodeType===1)return!1;if(k==="first")return!0;l=a;case"last":while(l=l.nextSibling)if(l.nodeType===1)return!1;return!0;case"nth":c=b[2],e=b[3];if(c===1&&e===0)return!0;f=b[0],g=a.parentNode;if(g&&(g[d]!==f||!a.nodeIndex)){i=0;for(l=g.firstChild;l;l=l.nextSibling)l.nodeType===1&&(l.nodeIndex=++i);g[d]=f}j=a.nodeIndex-e;return c===0?j===0:j%c===0&&j/c>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c<e;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var u,v;c.documentElement.compareDocumentPosition?u=function(a,b){if(a===b){h=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(u=function(a,b){if(a===b){h=!0;return 0}if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,i=b.parentNode,j=g;if(g===i)return v(a,b);if(!g)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return v(e[k],f[k]);return k===c?v(a,f[k],-1):v(e[k],b,1)},v=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h<i;h++)m(a,g[h],e,c);return m.filter(f,e)};m.attr=f.attr,m.selectors.attrMap={},f.find=m,f.expr=m.selectors,f.expr[":"]=f.expr.filters,f.unique=m.uniqueSort,f.text=m.getText,f.isXMLDoc=m.isXML,f.contains=m.contains}();var O=/Until$/,P=/^(?:parents|prevUntil|prevAll)/,Q=/,/,R=/^.[^:#\[\.,]*$/,S=Array.prototype.slice,T=f.expr.match.POS,U={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(W(this,a,!1),"not",a)},filter:function(a){return this.pushStack(W(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?T.test(a)?f(a,this.context).index(this[0])>=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d<a.length;d++)f(g).is(a[d])&&c.push({selector:a[d],elem:g,level:h});g=g.parentNode,h++}return c}var i=T.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(i?i.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(V(c[0])||V(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=S.call(arguments);O.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!U[a]?f.unique(e):e,(this.length>1||Q.test(d))&&P.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var Y="abbr article aside audio canvas datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",Z=/ jQuery\d+="(?:\d+|null)"/g,$=/^\s+/,_=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,ba=/<([\w:]+)/,bb=/<tbody/i,bc=/<|&#?\w+;/,bd=/<(?:script|style)/i,be=/<(?:script|object|embed|option|style)/i,bf=new RegExp("<(?:"+Y.replace(" ","|")+")","i"),bg=/checked\s*(?:[^=]|=\s*.checked.)/i,bh=/\/(java|ecma)script/i,bi=/^\s*<!(?:\[CDATA\[|\-\-)/,bj={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bk=X(c);bj.optgroup=bj.option,bj.tbody=bj.tfoot=bj.colgroup=bj.caption=bj.thead,bj.th=bj.td,f.support.htmlSerialize||(bj._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after" +,arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Z,""):null;if(typeof a=="string"&&!bd.test(a)&&(f.support.leadingWhitespace||!$.test(a))&&!bj[(ba.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(_,"<$1></$2>");try{for(var c=0,d=this.length;c<d;c++)this[c].nodeType===1&&(f.cleanData(this[c].getElementsByTagName("*")),this[c].innerHTML=a)}catch(e){this.empty().append(a)}}else f.isFunction(a)?this.each(function(b){var c=f(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bg.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bl(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,br)}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i,j=a[0];b&&b[0]&&(i=b[0].ownerDocument||b[0]),i.createDocumentFragment||(i=c),a.length===1&&typeof j=="string"&&j.length<512&&i===c&&j.charAt(0)==="<"&&!be.test(j)&&(f.support.checkClone||!bg.test(j))&&!f.support.unknownElems&&bf.test(j)&&(g=!0,h=f.fragments[j],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[j]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bn(a,d),e=bo(a),g=bo(d);for(h=0;e[h];++h)g[h]&&bn(e[h],g[h])}if(b){bm(a,d);if(c){e=bo(a),g=bo(d);for(h=0;e[h];++h)bm(e[h],g[h])}}e=g=null;return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!bc.test(k))k=b.createTextNode(k);else{k=k.replace(_,"<$1></$2>");var l=(ba.exec(k)||["",""])[1].toLowerCase(),m=bj[l]||bj._default,n=m[0],o=b.createElement("div");b===c?bk.appendChild(o):X(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=bb.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]==="<table>"&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&$.test(k)&&o.insertBefore(b.createTextNode($.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i<r;i++)bq(k[i]);else bq(k);k.nodeType?h.push(k):h=f.merge(h,k)}if(d){g=function(a){return!a.type||bh.test(a.type)};for(j=0;h[j];j++)if(e&&f.nodeName(h[j],"script")&&(!h[j].type||h[j].type.toLowerCase()==="text/javascript"))e.push(h[j].parentNode?h[j].parentNode.removeChild(h[j]):h[j]);else{if(h[j].nodeType===1){var s=f.grep(h[j].getElementsByTagName("script"),g);h.splice.apply(h,[j+1,0].concat(s))}d.appendChild(h[j])}}return h},cleanData:function(a){var b,c,d=f.cache,e=f.event.special,g=f.support.deleteExpando;for(var h=0,i;(i=a[h])!=null;h++){if(i.nodeName&&f.noData[i.nodeName.toLowerCase()])continue;c=i[f.expando];if(c){b=d[c];if(b&&b.events){for(var j in b.events)e[j]?f.event.remove(i,j):f.removeEvent(i,j,b.handle);b.handle&&(b.handle.elem=null)}g?delete i[f.expando]:i.removeAttribute&&i.removeAttribute(f.expando),delete d[c]}}}});var bs=/alpha\([^)]*\)/i,bt=/opacity=([^)]*)/,bu=/([A-Z]|^ms)/g,bv=/^-?\d+(?:px)?$/i,bw=/^-?\d/,bx=/^([\-+])=([\-+.\de]+)/,by={position:"absolute",visibility:"hidden",display:"block"},bz=["Left","Right"],bA=["Top","Bottom"],bB,bC,bD;f.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return f.access(this,a,c,!0,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)})},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bB(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bx.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(bB)return bB(a,c)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]}}),f.curCSS=f.css,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){var e;if(c){if(a.offsetWidth!==0)return bE(a,b,d);f.swap(a,by,function(){e=bE(a,b,d)});return e}},set:function(a,b){if(!bv.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bt.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bs,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bs.test(g)?g.replace(bs,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bB(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bC=function(a,c){var d,e,g;c=c.replace(bu,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bD=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bv.test(f)&&bw.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bB=bC||bD,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bF=/%20/g,bG=/\[\]$/,bH=/\r?\n/g,bI=/#.*$/,bJ=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bK=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bL=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bM=/^(?:GET|HEAD)$/,bN=/^\/\//,bO=/\?/,bP=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bQ=/^(?:select|textarea)/i,bR=/\s+/,bS=/([?&])_=[^&]*/,bT=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bU=f.fn.load,bV={},bW={},bX,bY,bZ=["*/"]+["*"];try{bX=e.href}catch(b$){bX=c.createElement("a"),bX.href="",bX=bX.href}bY=bT.exec(bX.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bU)return bU.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bP,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bQ.test(this.nodeName)||bK.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bH,"\r\n")}}):{name:b.name,value:c.replace(bH,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?cb(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),cb(a,b);return a},ajaxSettings:{url:bX,isLocal:bL.test(bY[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bZ},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:b_(bV),ajaxTransport:b_(bW),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cd(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=ce(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bJ.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bI,"").replace(bN,bY[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bR),d.crossDomain==null&&(r=bT.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bY[1]&&r[2]==bY[2]&&(r[3]||(r[1]==="http:"?80:443))==(bY[3]||(bY[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),ca(bV,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bM.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bO.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bS,"$1_="+x);d.url=y+(y===d.url?(bO.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bZ+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=ca(bW,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){s<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)cc(g,a[g],c,e);return d.join("&").replace(bF,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cf=f.now(),cg=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cf++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cg.test(b.url)||e&&cg.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cg,l),b.url===j&&(e&&(k=k.replace(cg,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ch=a.ActiveXObject?function(){for(var a in cj)cj[a](0,1)}:!1,ci=0,cj;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ck()||cl()}:ck,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ch&&delete cj[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++ci,ch&&(cj||(cj={},f(a).unload(ch)),cj[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cm={},cn,co,cp=/^(?:toggle|show|hide)$/,cq=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cr,cs=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],ct;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cw("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),e===""&&f.css(d,"display")==="none"&&f._data(d,"olddisplay",cx(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(cw("hide",3),a,b,c);var d,e,g=0,h=this.length;for(;g<h;g++)d=this[g],d.style&&(e=f.css(d,"display"),e!=="none"&&!f._data(d,"olddisplay")&&f._data(d,"olddisplay",e));for(g=0;g<h;g++)this[g].style&&(this[g].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(cw("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){function g(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]),h=a[g],f.isArray(h)?(b.animatedProperties[g]=h[1],h=a[g]=h[0]):b.animatedProperties[g]=b.specialEasing&&b.specialEasing[g]||b.easing||"swing";if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(!f.support.inlineBlockNeedsLayout||cx(this.nodeName)==="inline"?this.style.display="inline-block":this.style.zoom=1))}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)j=new f.fx(this,b,i),h=a[i],cp.test(h)?(o=f._data(this,"toggle"+i)||(h==="toggle"?d?"show":"hide":0),o?(f._data(this,"toggle"+i,o==="show"?"hide":"show"),j[o]()):j[h]()):(k=cq.exec(h),l=j.cur(),k?(m=parseFloat(k[2]),n=k[3]||(f.cssNumber[i]?"":"px"),n!=="px"&&(f.style(this,i,(m||1)+n),l=(m||1)/j.cur()*l,f.style(this,i,l+n)),k[1]&&(m=(k[1]==="-="?-1:1)*m+l),j.custom(l,m,n)):j.custom(l,h,""));return!0}var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);a=f.extend({},a);return e.queue===!1?this.each(g):this.queue(e.queue,g)},stop:function(a,c,d){typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]);return this.each(function(){function h(a,b,c){var e=b[c];f.removeData(a,c,!0),e.stop(d)}var b,c=!1,e=f.timers,g=f._data(this);d||f._unmark(!0,this);if(a==null)for(b in g)g[b].stop&&b.indexOf(".run")===b.length-4&&h(this,g,b);else g[b=a+".run"]&&g[b].stop&&h(this,g,b);for(b=e.length;b--;)e[b].elem===this&&(a==null||e[b].queue===a)&&(d?e[b](!0):e[b].saveState(),c=!0,e.splice(b,1));(!d||!c)&&f.dequeue(this,a)})}}),f.each({slideDown:cw("show",1),slideUp:cw("hide",1),slideToggle:cw("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";d.old=d.complete,d.complete=function(a){f.isFunction(d.old)&&d.old.call(this),d.queue?f.dequeue(this,d.queue):a!==!1&&f._unmark(this)};return d},easing:{linear:function(a,b,c,d){return c+d*a},swing:function(a,b,c,d){return(-Math.cos(a*Math.PI)/2+.5)*d+c}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,c,d){function h(a){return e.step(a)}var e=this,g=f.fx;this.startTime=ct||cu(),this.end=c,this.now=this.start=a,this.pos=this.state=0,this.unit=d||this.unit||(f.cssNumber[this.prop]?"":"px"),h.queue=this.options.queue,h.elem=this.elem,h.saveState=function(){e.options.hide&&f._data(e.elem,"fxshow"+e.prop)===b&&f._data(e.elem,"fxshow"+e.prop,e.start)},h()&&f.timers.push(h)&&!cr&&(cr=setInterval(g.tick,g.interval))},show:function(){var a=f._data(this.elem,"fxshow"+this.prop);this.options.orig[this.prop]=a||f.style(this.elem,this.prop),this.options.show=!0,a!==b?this.custom(this.cur(),a):this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f._data(this.elem,"fxshow"+this.prop)||f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b,c,d,e=ct||cu(),g=!0,h=this.elem,i=this.options;if(a||e>=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||f.fx.stop()},interval:13,stop:function(){clearInterval(cr),cr=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=a.now+a.unit:a.elem[a.prop]=a.now}}}),f.each(["width","height"],function(a,b){f.fx.step[b]=function(a){f.style(a.elem,b,Math.max(0,a.now))}}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var cy=/^t(?:able|d|h)$/i,cz=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?f.fn.offset=function(a){var b=this[0],c;if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);try{c=b.getBoundingClientRect()}catch(d){}var e=b.ownerDocument,g=e.documentElement;if(!c||!f.contains(g,b))return c?{top:c.top,left:c.left}:{top:0,left:0};var h=e.body,i=cA(e),j=g.clientTop||h.clientTop||0,k=g.clientLeft||h.clientLeft||0,l=i.pageYOffset||f.support.boxModel&&g.scrollTop||h.scrollTop,m=i.pageXOffset||f.support.boxModel&&g.scrollLeft||h.scrollLeft,n=c.top+l-j,o=c.left+m-k;return{top:n,left:o}}:f.fn.offset=function(a){var b=this[0];if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);var c,d=b.offsetParent,e=b,g=b.ownerDocument,h=g.documentElement,i=g.body,j=g.defaultView,k=j?j.getComputedStyle(b,null):b.currentStyle,l=b.offsetTop,m=b.offsetLeft;while((b=b.parentNode)&&b!==i&&b!==h){if(f.support.fixedPosition&&k.position==="fixed")break;c=j?j.getComputedStyle(b,null):b.currentStyle,l-=b.scrollTop,m-=b.scrollLeft,b===d&&(l+=b.offsetTop,m+=b.offsetLeft,f.support.doesNotAddBorder&&(!f.support.doesAddBorderForTableAndCells||!cy.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),e=d,d=b.offsetParent),f.support.subtractsBorderForOverflowNotVisible&&c.overflow!=="visible"&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),k=c}if(k.position==="relative"||k.position==="static")l+=i.offsetTop,m+=i.offsetLeft;f.support.fixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},f.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cz.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cz.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cA(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cA(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window);
\ No newline at end of file diff --git a/devtools/client/inspector/markup/test/lib_jquery_2.1.1_min.js b/devtools/client/inspector/markup/test/lib_jquery_2.1.1_min.js new file mode 100644 index 000000000..e5ace116b --- /dev/null +++ b/devtools/client/inspector/markup/test/lib_jquery_2.1.1_min.js @@ -0,0 +1,4 @@ +/*! jQuery v2.1.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.1",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="<div class='a'></div><div class='a i'></div>",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="<select msallowclip=''><option selected=''></option></select>",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=lb(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=mb(b);function pb(){}pb.prototype=d.filters=d.pseudos,d.setFilters=new pb,g=fb.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=S.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=T.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(R," ")}),h=h.slice(c.length));for(g in d.filter)!(e=X[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?fb.error(a):z(a,i).slice(0)};function qb(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+Math.random()}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b) +},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?n.queue(this[0],a):void 0===b?this:this.each(function(){var c=n.queue(this,a,b);n._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&n.dequeue(this,a)})},dequeue:function(a){return this.each(function(){n.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=n.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=L.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var Q=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,R=["Top","Right","Bottom","Left"],S=function(a,b){return a=b||a,"none"===n.css(a,"display")||!n.contains(a.ownerDocument,a)},T=/^(?:checkbox|radio)$/i;!function(){var a=l.createDocumentFragment(),b=a.appendChild(l.createElement("div")),c=l.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button;return null==a.pageX&&null!=b.clientX&&(c=a.target.ownerDocument||l,d=c.documentElement,e=c.body,a.pageX=b.clientX+(d&&d.scrollLeft||e&&e.scrollLeft||0)-(d&&d.clientLeft||e&&e.clientLeft||0),a.pageY=b.clientY+(d&&d.scrollTop||e&&e.scrollTop||0)-(d&&d.clientTop||e&&e.clientTop||0)),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},fix:function(a){if(a[n.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=W.test(e)?this.mouseHooks:V.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new n.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=l),3===a.target.nodeType&&(a.target=a.target.parentNode),g.filter?g.filter(a,f):a},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==_()&&this.focus?(this.focus(),!1):void 0},delegateType:"focusin"},blur:{trigger:function(){return this===_()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&n.nodeName(this,"input")?(this.click(),!1):void 0},_default:function(a){return n.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=n.extend(new n.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?n.event.trigger(e,null,b):n.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},n.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)},n.Event=function(a,b){return this instanceof n.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?Z:$):this.type=a,b&&n.extend(this,b),this.timeStamp=a&&a.timeStamp||n.now(),void(this[n.expando]=!0)):new n.Event(a,b)},n.Event.prototype={isDefaultPrevented:$,isPropagationStopped:$,isImmediatePropagationStopped:$,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=Z,a&&a.preventDefault&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=Z,a&&a.stopPropagation&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=Z,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},n.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){n.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!n.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),k.focusinBubbles||n.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){n.event.simulate(b,a.target,n.event.fix(a),!0)};n.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=L.access(d,b);e||d.addEventListener(a,c,!0),L.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=L.access(d,b)-1;e?L.access(d,b,e):(d.removeEventListener(a,c,!0),L.remove(d,b))}}}),n.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(g in a)this.on(g,b,c,a[g],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=$;else if(!d)return this;return 1===e&&(f=d,d=function(a){return n().off(a),f.apply(this,arguments)},d.guid=f.guid||(f.guid=n.guid++)),this.each(function(){n.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,n(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=$),this.each(function(){n.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){n.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?n.event.trigger(a,b,c,!0):void 0}});var ab=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,ib={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1></$2>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1></$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=qb[0].contentDocument,b.write(),b.close(),c=sb(a,b),qb.detach()),rb[a]=c),c}var ub=/^margin/,vb=new RegExp("^("+Q+")(?!px)[a-z%]+$","i"),wb=function(a){return a.ownerDocument.defaultView.getComputedStyle(a,null)};function xb(a,b,c){var d,e,f,g,h=a.style;return c=c||wb(a),c&&(g=c.getPropertyValue(b)||c[b]),c&&(""!==g||n.contains(a.ownerDocument,a)||(g=n.style(a,b)),vb.test(g)&&ub.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function yb(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d=l.documentElement,e=l.createElement("div"),f=l.createElement("div");if(f.style){f.style.backgroundClip="content-box",f.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===f.style.backgroundClip,e.style.cssText="border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;position:absolute",e.appendChild(f);function g(){f.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",f.innerHTML="",d.appendChild(e);var g=a.getComputedStyle(f,null);b="1%"!==g.top,c="4px"===g.width,d.removeChild(e)}a.getComputedStyle&&n.extend(k,{pixelPosition:function(){return g(),b},boxSizingReliable:function(){return null==c&&g(),c},reliableMarginRight:function(){var b,c=f.appendChild(l.createElement("div"));return c.style.cssText=f.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",c.style.marginRight=c.style.width="0",f.style.width="1px",d.appendChild(e),b=!parseFloat(a.getComputedStyle(c,null).marginRight),d.removeChild(e),b}})}}(),n.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var zb=/^(none|table(?!-c[ea]).+)/,Ab=new RegExp("^("+Q+")(.*)$","i"),Bb=new RegExp("^([+-])=("+Q+")","i"),Cb={position:"absolute",visibility:"hidden",display:"block"},Db={letterSpacing:"0",fontWeight:"400"},Eb=["Webkit","O","Moz","ms"];function Fb(a,b){if(b in a)return b;var c=b[0].toUpperCase()+b.slice(1),d=b,e=Eb.length;while(e--)if(b=Eb[e]+c,b in a)return b;return d}function Gb(a,b,c){var d=Ab.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Hb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=n.css(a,c+R[f],!0,e)),d?("content"===c&&(g-=n.css(a,"padding"+R[f],!0,e)),"margin"!==c&&(g-=n.css(a,"border"+R[f]+"Width",!0,e))):(g+=n.css(a,"padding"+R[f],!0,e),"padding"!==c&&(g+=n.css(a,"border"+R[f]+"Width",!0,e)));return g}function Ib(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=wb(a),g="border-box"===n.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=xb(a,b,f),(0>e||null==e)&&(e=a.style[b]),vb.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Hb(a,b,c||(g?"border":"content"),d,f)+"px"}function Jb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=L.get(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&S(d)&&(f[g]=L.access(d,"olddisplay",tb(d.nodeName)))):(e=S(d),"none"===c&&e||L.set(d,"olddisplay",e?c:n.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}n.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=xb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=n.camelCase(b),i=a.style;return b=n.cssProps[h]||(n.cssProps[h]=Fb(i,h)),g=n.cssHooks[b]||n.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=Bb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(n.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||n.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=n.camelCase(b);return b=n.cssProps[h]||(n.cssProps[h]=Fb(a.style,h)),g=n.cssHooks[b]||n.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=xb(a,b,d)),"normal"===e&&b in Db&&(e=Db[b]),""===c||c?(f=parseFloat(e),c===!0||n.isNumeric(f)?f||0:e):e}}),n.each(["height","width"],function(a,b){n.cssHooks[b]={get:function(a,c,d){return c?zb.test(n.css(a,"display"))&&0===a.offsetWidth?n.swap(a,Cb,function(){return Ib(a,b,d)}):Ib(a,b,d):void 0},set:function(a,c,d){var e=d&&wb(a);return Gb(a,c,d?Hb(a,b,d,"border-box"===n.css(a,"boxSizing",!1,e),e):0)}}}),n.cssHooks.marginRight=yb(k.reliableMarginRight,function(a,b){return b?n.swap(a,{display:"inline-block"},xb,[a,"marginRight"]):void 0}),n.each({margin:"",padding:"",border:"Width"},function(a,b){n.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+R[d]+b]=f[d]||f[d-2]||f[0];return e}},ub.test(a)||(n.cssHooks[a+b].set=Gb)}),n.fn.extend({css:function(a,b){return J(this,function(a,b,c){var d,e,f={},g=0;if(n.isArray(b)){for(d=wb(a),e=b.length;e>g;g++)f[b[g]]=n.css(a,b[g],!1,d);return f}return void 0!==c?n.style(a,b,c):n.css(a,b)},a,b,arguments.length>1)},show:function(){return Jb(this,!0)},hide:function(){return Jb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){S(this)?n(this).show():n(this).hide()})}});function Kb(a,b,c,d,e){return new Kb.prototype.init(a,b,c,d,e)}n.Tween=Kb,Kb.prototype={constructor:Kb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(n.cssNumber[c]?"":"px")},cur:function(){var a=Kb.propHooks[this.prop];return a&&a.get?a.get(this):Kb.propHooks._default.get(this)},run:function(a){var b,c=Kb.propHooks[this.prop];return this.pos=b=this.options.duration?n.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Kb.propHooks._default.set(this),this}},Kb.prototype.init.prototype=Kb.prototype,Kb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=n.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){n.fx.step[a.prop]?n.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[n.cssProps[a.prop]]||n.cssHooks[a.prop])?n.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Kb.propHooks.scrollTop=Kb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},n.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},n.fx=Kb.prototype.init,n.fx.step={};var Lb,Mb,Nb=/^(?:toggle|show|hide)$/,Ob=new RegExp("^(?:([+-])=|)("+Q+")([a-z%]*)$","i"),Pb=/queueHooks$/,Qb=[Vb],Rb={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=Ob.exec(b),f=e&&e[3]||(n.cssNumber[a]?"":"px"),g=(n.cssNumber[a]||"px"!==f&&+d)&&Ob.exec(n.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,n.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function Sb(){return setTimeout(function(){Lb=void 0}),Lb=n.now()}function Tb(a,b){var c,d=0,e={height:a};for(b=b?1:0;4>d;d+=2-b)c=R[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function Ub(a,b,c){for(var d,e=(Rb[b]||[]).concat(Rb["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function Vb(a,b,c){var d,e,f,g,h,i,j,k,l=this,m={},o=a.style,p=a.nodeType&&S(a),q=L.get(a,"fxshow");c.queue||(h=n._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,l.always(function(){l.always(function(){h.unqueued--,n.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=n.css(a,"display"),k="none"===j?L.get(a,"olddisplay")||tb(a.nodeName):j,"inline"===k&&"none"===n.css(a,"float")&&(o.display="inline-block")),c.overflow&&(o.overflow="hidden",l.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],Nb.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}m[d]=q&&q[d]||n.style(a,d)}else j=void 0;if(n.isEmptyObject(m))"inline"===("none"===j?tb(a.nodeName):j)&&(o.display=j);else{q?"hidden"in q&&(p=q.hidden):q=L.access(a,"fxshow",{}),f&&(q.hidden=!p),p?n(a).show():l.done(function(){n(a).hide()}),l.done(function(){var b;L.remove(a,"fxshow");for(b in m)n.style(a,b,m[b])});for(d in m)g=Ub(p?q[d]:0,d,l),d in q||(q[d]=g.start,p&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function Wb(a,b){var c,d,e,f,g;for(c in a)if(d=n.camelCase(c),e=b[d],f=a[c],n.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=n.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function Xb(a,b,c){var d,e,f=0,g=Qb.length,h=n.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Lb||Sb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:n.extend({},b),opts:n.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:Lb||Sb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=n.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(Wb(k,j.opts.specialEasing);g>f;f++)if(d=Qb[f].call(j,a,k,j.opts))return d;return n.map(k,Ub,j),n.isFunction(j.opts.start)&&j.opts.start.call(a,j),n.fx.timer(n.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}n.Animation=n.extend(Xb,{tweener:function(a,b){n.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],Rb[c]=Rb[c]||[],Rb[c].unshift(b)},prefilter:function(a,b){b?Qb.unshift(a):Qb.push(a)}}),n.speed=function(a,b,c){var d=a&&"object"==typeof a?n.extend({},a):{complete:c||!c&&b||n.isFunction(a)&&a,duration:a,easing:c&&b||b&&!n.isFunction(b)&&b};return d.duration=n.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in n.fx.speeds?n.fx.speeds[d.duration]:n.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){n.isFunction(d.old)&&d.old.call(this),d.queue&&n.dequeue(this,d.queue)},d},n.fn.extend({fadeTo:function(a,b,c,d){return this.filter(S).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=n.isEmptyObject(a),f=n.speed(b,c,d),g=function(){var b=Xb(this,n.extend({},a),f);(e||L.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=n.timers,g=L.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&Pb.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&n.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=L.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=n.timers,g=d?d.length:0;for(c.finish=!0,n.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),n.each(["toggle","show","hide"],function(a,b){var c=n.fn[b];n.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(Tb(b,!0),a,d,e)}}),n.each({slideDown:Tb("show"),slideUp:Tb("hide"),slideToggle:Tb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){n.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),n.timers=[],n.fx.tick=function(){var a,b=0,c=n.timers;for(Lb=n.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||n.fx.stop(),Lb=void 0},n.fx.timer=function(a){n.timers.push(a),a()?n.fx.start():n.timers.pop()},n.fx.interval=13,n.fx.start=function(){Mb||(Mb=setInterval(n.fx.tick,n.fx.interval))},n.fx.stop=function(){clearInterval(Mb),Mb=null},n.fx.speeds={slow:600,fast:200,_default:400},n.fn.delay=function(a,b){return a=n.fx?n.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a=l.createElement("input"),b=l.createElement("select"),c=b.appendChild(l.createElement("option"));a.type="checkbox",k.checkOn=""!==a.value,k.optSelected=c.selected,b.disabled=!0,k.optDisabled=!c.disabled,a=l.createElement("input"),a.value="t",a.type="radio",k.radioValue="t"===a.value}();var Yb,Zb,$b=n.expr.attrHandle;n.fn.extend({attr:function(a,b){return J(this,n.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===U?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),d=n.attrHooks[b]||(n.expr.match.bool.test(b)?Zb:Yb)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=n.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void n.removeAttr(a,b)) +},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)&&(a[d]=!1),a.removeAttribute(c)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),Zb={set:function(a,b,c){return b===!1?n.removeAttr(a,c):a.setAttribute(c,c),c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=$b[b]||n.find.attr;$b[b]=function(a,b,d){var e,f;return d||(f=$b[b],$b[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,$b[b]=f),e}});var _b=/^(?:input|select|textarea|button)$/i;n.fn.extend({prop:function(a,b){return J(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[n.propFix[a]||a]})}}),n.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!n.isXMLDoc(a),f&&(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){return a.hasAttribute("tabindex")||_b.test(a.nodeName)||a.href?a.tabIndex:-1}}}}),k.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this});var ac=/[\t\r\n\f]/g;n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h="string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=n.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0===arguments.length||"string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?n.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(n.isFunction(a)?function(c){n(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=n(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===U||"boolean"===c)&&(this.className&&L.set(this,"__className__",this.className),this.className=this.className||a===!1?"":L.get(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(ac," ").indexOf(b)>=0)return!0;return!1}});var bc=/\r/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(bc,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.trim(n.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=n.inArray(d.value,f)>=0)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>=0:void 0}},k.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var cc=n.now(),dc=/\?/;n.parseJSON=function(a){return JSON.parse(a+"")},n.parseXML=function(a){var b,c;if(!a||"string"!=typeof a)return null;try{c=new DOMParser,b=c.parseFromString(a,"text/xml")}catch(d){b=void 0}return(!b||b.getElementsByTagName("parsererror").length)&&n.error("Invalid XML: "+a),b};var ec,fc,gc=/#.*$/,hc=/([?&])_=[^&]*/,ic=/^(.*?):[ \t]*([^\r\n]*)$/gm,jc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,kc=/^(?:GET|HEAD)$/,lc=/^\/\//,mc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,nc={},oc={},pc="*/".concat("*");try{fc=location.href}catch(qc){fc=l.createElement("a"),fc.href="",fc=fc.href}ec=mc.exec(fc.toLowerCase())||[];function rc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(n.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function sc(a,b,c,d){var e={},f=a===oc;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function tc(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&n.extend(!0,a,d),a}function uc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function vc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:fc,type:"GET",isLocal:jc.test(ec[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":pc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":n.parseJSON,"text xml":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?tc(tc(a,n.ajaxSettings),b):tc(n.ajaxSettings,a)},ajaxPrefilter:rc(nc),ajaxTransport:rc(oc),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=n.ajaxSetup({},b),l=k.context||k,m=k.context&&(l.nodeType||l.jquery)?n(l):n.event,o=n.Deferred(),p=n.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!f){f={};while(b=ic.exec(e))f[b[1].toLowerCase()]=b[2]}b=f[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?e:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return c&&c.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||fc)+"").replace(gc,"").replace(lc,ec[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=n.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(h=mc.exec(k.url.toLowerCase()),k.crossDomain=!(!h||h[1]===ec[1]&&h[2]===ec[2]&&(h[3]||("http:"===h[1]?"80":"443"))===(ec[3]||("http:"===ec[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=n.param(k.data,k.traditional)),sc(nc,k,b,v),2===t)return v;i=k.global,i&&0===n.active++&&n.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!kc.test(k.type),d=k.url,k.hasContent||(k.data&&(d=k.url+=(dc.test(d)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=hc.test(d)?d.replace(hc,"$1_="+cc++):d+(dc.test(d)?"&":"?")+"_="+cc++)),k.ifModified&&(n.lastModified[d]&&v.setRequestHeader("If-Modified-Since",n.lastModified[d]),n.etag[d]&&v.setRequestHeader("If-None-Match",n.etag[d])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+pc+"; q=0.01":""):k.accepts["*"]);for(j in k.headers)v.setRequestHeader(j,k.headers[j]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(j in{success:1,error:1,complete:1})v[j](k[j]);if(c=sc(oc,k,b,v)){v.readyState=1,i&&m.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,c.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,f,h){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),c=void 0,e=h||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,f&&(u=uc(k,v,f)),u=vc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(n.lastModified[d]=w),w=v.getResponseHeader("etag"),w&&(n.etag[d]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,i&&m.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),i&&(m.trigger("ajaxComplete",[v,k]),--n.active||n.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return n.get(a,b,c,"json")},getScript:function(a,b){return n.get(a,void 0,b,"script")}}),n.each(["get","post"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),n.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){n.fn[b]=function(a){return this.on(b,a)}}),n._evalUrl=function(a){return n.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},n.fn.extend({wrapAll:function(a){var b;return n.isFunction(a)?this.each(function(b){n(this).wrapAll(a.call(this,b))}):(this[0]&&(b=n(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this)},wrapInner:function(a){return this.each(n.isFunction(a)?function(b){n(this).wrapInner(a.call(this,b))}:function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,"body")||n(this).replaceWith(this.childNodes)}).end()}}),n.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0},n.expr.filters.visible=function(a){return!n.expr.filters.hidden(a)};var wc=/%20/g,xc=/\[\]$/,yc=/\r?\n/g,zc=/^(?:submit|button|image|reset|file)$/i,Ac=/^(?:input|select|textarea|keygen)/i;function Bc(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||xc.test(a)?d(a,e):Bc(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==n.type(b))d(a,b);else for(e in b)Bc(a+"["+e+"]",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)Bc(c,a[c],b,e);return d.join("&").replace(wc,"+")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,"elements");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(":disabled")&&Ac.test(this.nodeName)&&!zc.test(a)&&(this.checked||!T.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(yc,"\r\n")}}):{name:b.name,value:c.replace(yc,"\r\n")}}).get()}}),n.ajaxSettings.xhr=function(){try{return new XMLHttpRequest}catch(a){}};var Cc=0,Dc={},Ec={0:200,1223:204},Fc=n.ajaxSettings.xhr();a.ActiveXObject&&n(a).on("unload",function(){for(var a in Dc)Dc[a]()}),k.cors=!!Fc&&"withCredentials"in Fc,k.ajax=Fc=!!Fc,n.ajaxTransport(function(a){var b;return k.cors||Fc&&!a.crossDomain?{send:function(c,d){var e,f=a.xhr(),g=++Cc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)f.setRequestHeader(e,c[e]);b=function(a){return function(){b&&(delete Dc[g],b=f.onload=f.onerror=null,"abort"===a?f.abort():"error"===a?d(f.status,f.statusText):d(Ec[f.status]||f.status,f.statusText,"string"==typeof f.responseText?{text:f.responseText}:void 0,f.getAllResponseHeaders()))}},f.onload=b(),f.onerror=b("error"),b=Dc[g]=b("abort");try{f.send(a.hasContent&&a.data||null)}catch(h){if(b)throw h}},abort:function(){b&&b()}}:void 0}),n.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),n.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(d,e){b=n("<script>").prop({async:!0,charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&e("error"===a.type?404:200,a.type)}),l.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Gc=[],Hc=/(=)\?(?=&|$)|\?\?/;n.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Gc.pop()||n.expando+"_"+cc++;return this[a]=!0,a}}),n.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Hc.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Hc.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=n.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Hc,"$1"+e):b.jsonp!==!1&&(b.url+=(dc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||n.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Gc.push(e)),g&&n.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),n.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||l;var d=v.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=n.buildFragment([a],b,e),e&&e.length&&n(e).remove(),n.merge([],d.childNodes))};var Ic=n.fn.load;n.fn.load=function(a,b,c){if("string"!=typeof a&&Ic)return Ic.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=n.trim(a.slice(h)),a=a.slice(0,h)),n.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&n.ajax({url:a,type:e,dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?n("<div>").append(n.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,f||[a.responseText,b,a])}),this},n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};var Jc=a.document.documentElement;function Kc(a){return n.isWindow(a)?a:9===a.nodeType&&a.defaultView}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,"position"),l=n(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=n.css(a,"top"),i=n.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d=this[0],e={top:0,left:0},f=d&&d.ownerDocument;if(f)return b=f.documentElement,n.contains(b,d)?(typeof d.getBoundingClientRect!==U&&(e=d.getBoundingClientRect()),c=Kc(f),{top:e.top+c.pageYOffset-b.clientTop,left:e.left+c.pageXOffset-b.clientLeft}):e},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===n.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],"html")||(d=a.offset()),d.top+=n.css(a[0],"borderTopWidth",!0),d.left+=n.css(a[0],"borderLeftWidth",!0)),{top:b.top-d.top-n.css(c,"marginTop",!0),left:b.left-d.left-n.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||Jc;while(a&&!n.nodeName(a,"html")&&"static"===n.css(a,"position"))a=a.offsetParent;return a||Jc})}}),n.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(b,c){var d="pageYOffset"===c;n.fn[b]=function(e){return J(this,function(b,e,f){var g=Kc(b);return void 0===f?g?g[c]:b[e]:void(g?g.scrollTo(d?a.pageXOffset:f,d?f:a.pageYOffset):b[e]=f)},b,e,arguments.length,null)}}),n.each(["top","left"],function(a,b){n.cssHooks[b]=yb(k.pixelPosition,function(a,c){return c?(c=xb(a,b),vb.test(c)?n(a).position()[b]+"px":c):void 0})}),n.each({Height:"height",Width:"width"},function(a,b){n.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return J(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.size=function(){return this.length},n.fn.andSelf=n.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return n});var Lc=a.jQuery,Mc=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=Mc),b&&a.jQuery===n&&(a.jQuery=Lc),n},typeof b===U&&(a.jQuery=a.$=n),n}); diff --git a/devtools/client/inspector/markup/utils.js b/devtools/client/inspector/markup/utils.js new file mode 100644 index 000000000..8fab9d963 --- /dev/null +++ b/devtools/client/inspector/markup/utils.js @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Apply a 'flashed' background and foreground color to elements. Intended + * to be used with flashElementOff as a way of drawing attention to an element. + * + * @param {Node} backgroundElt + * The element to set the highlighted background color on. + * @param {Node} foregroundElt + * The element to set the matching foreground color on. + * Optional. This will equal backgroundElt if not set. + */ +function flashElementOn(backgroundElt, foregroundElt = backgroundElt) { + if (!backgroundElt || !foregroundElt) { + return; + } + + // Make sure the animation class is not here + backgroundElt.classList.remove("flash-out"); + + // Change the background + backgroundElt.classList.add("theme-bg-contrast"); + + foregroundElt.classList.add("theme-fg-contrast"); + [].forEach.call( + foregroundElt.querySelectorAll("[class*=theme-fg-color]"), + span => span.classList.add("theme-fg-contrast") + ); +} + +/** + * Remove a 'flashed' background and foreground color to elements. + * See flashElementOn. + * + * @param {Node} backgroundElt + * The element to reomve the highlighted background color on. + * @param {Node} foregroundElt + * The element to remove the matching foreground color on. + * Optional. This will equal backgroundElt if not set. + */ +function flashElementOff(backgroundElt, foregroundElt = backgroundElt) { + if (!backgroundElt || !foregroundElt) { + return; + } + + // Add the animation class to smoothly remove the background + backgroundElt.classList.add("flash-out"); + + // Remove the background + backgroundElt.classList.remove("theme-bg-contrast"); + + foregroundElt.classList.remove("theme-fg-contrast"); + [].forEach.call( + foregroundElt.querySelectorAll("[class*=theme-fg-color]"), + span => span.classList.remove("theme-fg-contrast") + ); +} + +/** + * Retrieve the available width between a provided element left edge and a container right + * edge. This used can be used as a max-width for inplace-editor (autocomplete) widgets + * replacing Editor elements of the the markup-view; + */ +function getAutocompleteMaxWidth(element, container) { + let elementRect = element.getBoundingClientRect(); + let containerRect = container.getBoundingClientRect(); + return containerRect.right - elementRect.left - 2; +} + +/** + * Parse attribute names and values from a string. + * + * @param {String} attr + * The input string for which names/values are to be parsed. + * @param {HTMLDocument} doc + * A document that can be used to test valid attributes. + * @return {Array} + * An array of attribute names and their values. + */ +function parseAttributeValues(attr, doc) { + attr = attr.trim(); + + let parseAndGetNode = str => { + return new DOMParser().parseFromString(str, "text/html").body.childNodes[0]; + }; + + // Handle bad user inputs by appending a " or ' if it fails to parse without + // them. Also note that a SVG tag is used to make sure the HTML parser + // preserves mixed-case attributes + let el = parseAndGetNode("<svg " + attr + "></svg>") || + parseAndGetNode("<svg " + attr + "\"></svg>") || + parseAndGetNode("<svg " + attr + "'></svg>"); + + let div = doc.createElement("div"); + let attributes = []; + for (let {name, value} of el.attributes) { + // Try to set on an element in the document, throws exception on bad input. + // Prevents InvalidCharacterError - "String contains an invalid character". + try { + div.setAttribute(name, value); + attributes.push({ name, value }); + } catch (e) { + // This may throw exceptions on bad input. + // Prevents InvalidCharacterError - "String contains an invalid + // character". + } + } + + return attributes; +} + +/** + * Truncate the string and add ellipsis to the middle of the string. + */ +function truncateString(str, maxLength) { + if (!str || str.length <= maxLength) { + return str; + } + + return str.substring(0, Math.ceil(maxLength / 2)) + + "…" + + str.substring(str.length - Math.floor(maxLength / 2)); +} + +module.exports = { + flashElementOn, + flashElementOff, + getAutocompleteMaxWidth, + parseAttributeValues, + truncateString, +}; diff --git a/devtools/client/inspector/markup/views/element-container.js b/devtools/client/inspector/markup/views/element-container.js new file mode 100644 index 000000000..851a803cb --- /dev/null +++ b/devtools/client/inspector/markup/views/element-container.js @@ -0,0 +1,193 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PREVIEW_MAX_DIM_PREF = "devtools.inspector.imagePreviewTooltipSize"; + +const promise = require("promise"); +const Services = require("Services"); +const Heritage = require("sdk/core/heritage"); +const {Task} = require("devtools/shared/task"); +const nodeConstants = require("devtools/shared/dom-node-constants"); +const clipboardHelper = require("devtools/shared/platform/clipboard"); +const {setImageTooltip, setBrokenImageTooltip} = + require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper"); +const {setEventTooltip} = require("devtools/client/shared/widgets/tooltip/EventTooltipHelper"); +const MarkupContainer = require("devtools/client/inspector/markup/views/markup-container"); +const ElementEditor = require("devtools/client/inspector/markup/views/element-editor"); + +/** + * An implementation of MarkupContainer for Elements that can contain + * child nodes. + * Allows editing of tag name, attributes, expanding / collapsing. + * + * @param {MarkupView} markupView + * The markup view that owns this container. + * @param {NodeFront} node + * The node to display. + */ +function MarkupElementContainer(markupView, node) { + MarkupContainer.prototype.initialize.call(this, markupView, node, + "elementcontainer"); + + if (node.nodeType === nodeConstants.ELEMENT_NODE) { + this.editor = new ElementEditor(this, node); + } else { + throw new Error("Invalid node for MarkupElementContainer"); + } + + this.tagLine.appendChild(this.editor.elt); +} + +MarkupElementContainer.prototype = Heritage.extend(MarkupContainer.prototype, { + _buildEventTooltipContent: Task.async(function* (target, tooltip) { + if (target.hasAttribute("data-event")) { + yield tooltip.hide(); + + let listenerInfo = yield this.node.getEventListenerInfo(); + + let toolbox = this.markup.toolbox; + setEventTooltip(tooltip, listenerInfo, toolbox); + // Disable the image preview tooltip while we display the event details + this.markup._disableImagePreviewTooltip(); + tooltip.once("hidden", () => { + // Enable the image preview tooltip after closing the event details + this.markup._enableImagePreviewTooltip(); + }); + tooltip.show(target); + } + }), + + /** + * Generates the an image preview for this Element. The element must be an + * image or canvas (@see isPreviewable). + * + * @return {Promise} that is resolved with an object of form + * { data, size: { naturalWidth, naturalHeight, resizeRatio } } where + * - data is the data-uri for the image preview. + * - size contains information about the original image size and if + * the preview has been resized. + * + * If this element is not previewable or the preview cannot be generated for + * some reason, the Promise is rejected. + */ + _getPreview: function () { + if (!this.isPreviewable()) { + return promise.reject("_getPreview called on a non-previewable element."); + } + + if (this.tooltipDataPromise) { + // A preview request is already pending. Re-use that request. + return this.tooltipDataPromise; + } + + // Fetch the preview from the server. + this.tooltipDataPromise = Task.spawn(function* () { + let maxDim = Services.prefs.getIntPref(PREVIEW_MAX_DIM_PREF); + let preview = yield this.node.getImageData(maxDim); + let data = yield preview.data.string(); + + // Clear the pending preview request. We can't reuse the results later as + // the preview contents might have changed. + this.tooltipDataPromise = null; + return { data, size: preview.size }; + }.bind(this)); + + return this.tooltipDataPromise; + }, + + /** + * Executed by MarkupView._isImagePreviewTarget which is itself called when + * the mouse hovers over a target in the markup-view. + * Checks if the target is indeed something we want to have an image tooltip + * preview over and, if so, inserts content into the tooltip. + * + * @return {Promise} that resolves when the tooltip content is ready. Resolves + * true if the tooltip should be displayed, false otherwise. + */ + isImagePreviewTarget: Task.async(function* (target, tooltip) { + // Is this Element previewable. + if (!this.isPreviewable()) { + return false; + } + + // If the Element has an src attribute, the tooltip is shown when hovering + // over the src url. If not, the tooltip is shown when hovering over the tag + // name. + let src = this.editor.getAttributeElement("src"); + let expectedTarget = src ? src.querySelector(".link") : this.editor.tag; + if (target !== expectedTarget) { + return false; + } + + try { + let { data, size } = yield this._getPreview(); + // The preview is ready. + let options = { + naturalWidth: size.naturalWidth, + naturalHeight: size.naturalHeight, + maxDim: Services.prefs.getIntPref(PREVIEW_MAX_DIM_PREF) + }; + + setImageTooltip(tooltip, this.markup.doc, data, options); + } catch (e) { + // Indicate the failure but show the tooltip anyway. + setBrokenImageTooltip(tooltip, this.markup.doc); + } + return true; + }), + + copyImageDataUri: function () { + // We need to send again a request to gettooltipData even if one was sent + // for the tooltip, because we want the full-size image + this.node.getImageData().then(data => { + data.data.string().then(str => { + clipboardHelper.copyString(str); + }); + }); + }, + + setInlineTextChild: function (inlineTextChild) { + this.inlineTextChild = inlineTextChild; + this.editor.updateTextEditor(); + }, + + clearInlineTextChild: function () { + this.inlineTextChild = undefined; + this.editor.updateTextEditor(); + }, + + /** + * Trigger new attribute field for input. + */ + addAttribute: function () { + this.editor.newAttr.editMode(); + }, + + /** + * Trigger attribute field for editing. + */ + editAttribute: function (attrName) { + this.editor.attrElements.get(attrName).editMode(); + }, + + /** + * Remove attribute from container. + * This is an undoable action. + */ + removeAttribute: function (attrName) { + let doMods = this.editor._startModifyingAttributes(); + let undoMods = this.editor._startModifyingAttributes(); + this.editor._saveAttribute(attrName, undoMods); + doMods.removeAttribute(attrName); + this.undo.do(() => { + doMods.apply(); + }, () => { + undoMods.apply(); + }); + } +}); + +module.exports = MarkupElementContainer; diff --git a/devtools/client/inspector/markup/views/element-editor.js b/devtools/client/inspector/markup/views/element-editor.js new file mode 100644 index 000000000..3149086eb --- /dev/null +++ b/devtools/client/inspector/markup/views/element-editor.js @@ -0,0 +1,560 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Services = require("Services"); +const TextEditor = require("devtools/client/inspector/markup/views/text-editor"); +const { + getAutocompleteMaxWidth, + flashElementOn, + flashElementOff, + parseAttributeValues, + truncateString, +} = require("devtools/client/inspector/markup/utils"); +const {editableField, InplaceEditor} = + require("devtools/client/shared/inplace-editor"); +const {parseAttribute} = + require("devtools/client/shared/node-attribute-parser"); +const {getCssProperties} = require("devtools/shared/fronts/css-properties"); + +// Page size for pageup/pagedown +const COLLAPSE_DATA_URL_REGEX = /^data.+base64/; +const COLLAPSE_DATA_URL_LENGTH = 60; + +// Contains only void (without end tag) HTML elements +const HTML_VOID_ELEMENTS = [ "area", "base", "br", "col", "command", "embed", + "hr", "img", "input", "keygen", "link", "meta", "param", "source", + "track", "wbr" ]; + +/** + * Creates an editor for an Element node. + * + * @param {MarkupContainer} container + * The container owning this editor. + * @param {Element} node + * The node being edited. + */ +function ElementEditor(container, node) { + this.container = container; + this.node = node; + this.markup = this.container.markup; + this.template = this.markup.template.bind(this.markup); + this.doc = this.markup.doc; + this._cssProperties = getCssProperties(this.markup.toolbox); + + this.attrElements = new Map(); + this.animationTimers = {}; + + // The templates will fill the following properties + this.elt = null; + this.tag = null; + this.closeTag = null; + this.attrList = null; + this.newAttr = null; + this.closeElt = null; + + // Create the main editor + this.template("element", this); + + // Make the tag name editable (unless this is a remote node or + // a document element) + if (!node.isDocumentElement) { + // Make the tag optionally tabbable but not by default. + this.tag.setAttribute("tabindex", "-1"); + editableField({ + element: this.tag, + multiline: true, + maxWidth: () => getAutocompleteMaxWidth(this.tag, this.container.elt), + trigger: "dblclick", + stopOnReturn: true, + done: this.onTagEdit.bind(this), + contextMenu: this.markup.inspector.onTextBoxContextMenu, + cssProperties: this._cssProperties + }); + } + + // Make the new attribute space editable. + this.newAttr.editMode = editableField({ + element: this.newAttr, + multiline: true, + maxWidth: () => getAutocompleteMaxWidth(this.newAttr, this.container.elt), + trigger: "dblclick", + stopOnReturn: true, + contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED, + popup: this.markup.popup, + done: (val, commit) => { + if (!commit) { + return; + } + + let doMods = this._startModifyingAttributes(); + let undoMods = this._startModifyingAttributes(); + this._applyAttributes(val, null, doMods, undoMods); + this.container.undo.do(() => { + doMods.apply(); + }, function () { + undoMods.apply(); + }); + }, + contextMenu: this.markup.inspector.onTextBoxContextMenu, + cssProperties: this._cssProperties + }); + + let displayName = this.node.displayName; + this.tag.textContent = displayName; + this.closeTag.textContent = displayName; + + let isVoidElement = HTML_VOID_ELEMENTS.includes(displayName); + if (node.isInHTMLDocument && isVoidElement) { + this.elt.classList.add("void-element"); + } + + this.update(); + this.initialized = true; +} + +ElementEditor.prototype = { + set selected(value) { + if (this.textEditor) { + this.textEditor.selected = value; + } + }, + + flashAttribute: function (attrName) { + if (this.animationTimers[attrName]) { + clearTimeout(this.animationTimers[attrName]); + } + + flashElementOn(this.getAttributeElement(attrName)); + + this.animationTimers[attrName] = setTimeout(() => { + flashElementOff(this.getAttributeElement(attrName)); + }, this.markup.CONTAINER_FLASHING_DURATION); + }, + + /** + * Returns information about node in the editor. + * + * @param {DOMNode} node + * The node to get information from. + * @return {Object} An object literal with the following information: + * {type: "attribute", name: "rel", value: "index", el: node} + */ + getInfoAtNode: function (node) { + if (!node) { + return null; + } + + let type = null; + let name = null; + let value = null; + + // Attribute + let attribute = node.closest(".attreditor"); + if (attribute) { + type = "attribute"; + name = attribute.querySelector(".attr-name").textContent; + value = attribute.querySelector(".attr-value").textContent; + } + + return {type, name, value, el: node}; + }, + + /** + * Update the state of the editor from the node. + */ + update: function () { + let nodeAttributes = this.node.attributes || []; + + // Keep the data model in sync with attributes on the node. + let currentAttributes = new Set(nodeAttributes.map(a => a.name)); + for (let name of this.attrElements.keys()) { + if (!currentAttributes.has(name)) { + this.removeAttribute(name); + } + } + + // Only loop through the current attributes on the node. Missing + // attributes have already been removed at this point. + for (let attr of nodeAttributes) { + let el = this.attrElements.get(attr.name); + let valueChanged = el && + el.dataset.value !== attr.value; + let isEditing = el && el.querySelector(".editable").inplaceEditor; + let canSimplyShowEditor = el && (!valueChanged || isEditing); + + if (canSimplyShowEditor) { + // Element already exists and doesn't need to be recreated. + // Just show it (it's hidden by default due to the template). + el.style.removeProperty("display"); + } else { + // Create a new editor, because the value of an existing attribute + // has changed. + let attribute = this._createAttribute(attr, el); + attribute.style.removeProperty("display"); + + // Temporarily flash the attribute to highlight the change. + // But not if this is the first time the editor instance has + // been created. + if (this.initialized) { + this.flashAttribute(attr.name); + } + } + } + + // Update the event bubble display + this.eventNode.style.display = this.node.hasEventListeners ? + "inline-block" : "none"; + + this.updateTextEditor(); + }, + + /** + * Update the inline text editor in case of a single text child node. + */ + updateTextEditor: function () { + let node = this.node.inlineTextChild; + + if (this.textEditor && this.textEditor.node != node) { + this.elt.removeChild(this.textEditor.elt); + this.textEditor = null; + } + + if (node && !this.textEditor) { + // Create a text editor added to this editor. + // This editor won't receive an update automatically, so we rely on + // child text editors to let us know that we need updating. + this.textEditor = new TextEditor(this.container, node, "text"); + this.elt.insertBefore(this.textEditor.elt, + this.elt.firstChild.nextSibling.nextSibling); + } + + if (this.textEditor) { + this.textEditor.update(); + } + }, + + _startModifyingAttributes: function () { + return this.node.startModifyingAttributes(); + }, + + /** + * Get the element used for one of the attributes of this element. + * + * @param {String} attrName + * The name of the attribute to get the element for + * @return {DOMNode} + */ + getAttributeElement: function (attrName) { + return this.attrList.querySelector( + ".attreditor[data-attr=" + CSS.escape(attrName) + "] .attr-value"); + }, + + /** + * Remove an attribute from the attrElements object and the DOM. + * + * @param {String} attrName + * The name of the attribute to remove + */ + removeAttribute: function (attrName) { + let attr = this.attrElements.get(attrName); + if (attr) { + this.attrElements.delete(attrName); + attr.remove(); + } + }, + + _createAttribute: function (attribute, before = null) { + // Create the template editor, which will save some variables here. + let data = { + attrName: attribute.name, + attrValue: attribute.value, + tabindex: this.container.canFocus ? "0" : "-1", + }; + this.template("attribute", data); + let {attr, inner, name, val} = data; + + // Double quotes need to be handled specially to prevent DOMParser failing. + // name="v"a"l"u"e" when editing -> name='v"a"l"u"e"' + // name="v'a"l'u"e" when editing -> name="v'a"l'u"e" + let editValueDisplayed = attribute.value || ""; + let hasDoubleQuote = editValueDisplayed.includes('"'); + let hasSingleQuote = editValueDisplayed.includes("'"); + let initial = attribute.name + '="' + editValueDisplayed + '"'; + + // Can't just wrap value with ' since the value contains both " and '. + if (hasDoubleQuote && hasSingleQuote) { + editValueDisplayed = editValueDisplayed.replace(/\"/g, """); + initial = attribute.name + '="' + editValueDisplayed + '"'; + } + + // Wrap with ' since there are no single quotes in the attribute value. + if (hasDoubleQuote && !hasSingleQuote) { + initial = attribute.name + "='" + editValueDisplayed + "'"; + } + + // Make the attribute editable. + attr.editMode = editableField({ + element: inner, + trigger: "dblclick", + stopOnReturn: true, + selectAll: false, + initial: initial, + multiline: true, + maxWidth: () => getAutocompleteMaxWidth(inner, this.container.elt), + contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED, + popup: this.markup.popup, + start: (editor, event) => { + // If the editing was started inside the name or value areas, + // select accordingly. + if (event && event.target === name) { + editor.input.setSelectionRange(0, name.textContent.length); + } else if (event && event.target.closest(".attr-value") === val) { + let length = editValueDisplayed.length; + let editorLength = editor.input.value.length; + let start = editorLength - (length + 1); + editor.input.setSelectionRange(start, start + length); + } else { + editor.input.select(); + } + }, + done: (newValue, commit, direction) => { + if (!commit || newValue === initial) { + return; + } + + let doMods = this._startModifyingAttributes(); + let undoMods = this._startModifyingAttributes(); + + // Remove the attribute stored in this editor and re-add any attributes + // parsed out of the input element. Restore original attribute if + // parsing fails. + this.refocusOnEdit(attribute.name, attr, direction); + this._saveAttribute(attribute.name, undoMods); + doMods.removeAttribute(attribute.name); + this._applyAttributes(newValue, attr, doMods, undoMods); + this.container.undo.do(() => { + doMods.apply(); + }, () => { + undoMods.apply(); + }); + }, + contextMenu: this.markup.inspector.onTextBoxContextMenu, + cssProperties: this._cssProperties + }); + + // Figure out where we should place the attribute. + if (attribute.name == "id") { + before = this.attrList.firstChild; + } else if (attribute.name == "class") { + let idNode = this.attrElements.get("id"); + before = idNode ? idNode.nextSibling : this.attrList.firstChild; + } + this.attrList.insertBefore(attr, before); + + this.removeAttribute(attribute.name); + this.attrElements.set(attribute.name, attr); + + // Parse the attribute value to detect whether there are linkable parts in + // it (make sure to pass a complete list of existing attributes to the + // parseAttribute function, by concatenating attribute, because this could + // be a newly added attribute not yet on this.node). + let attributes = this.node.attributes.filter(existingAttribute => { + return existingAttribute.name !== attribute.name; + }); + attributes.push(attribute); + let parsedLinksData = parseAttribute(this.node.namespaceURI, + this.node.tagName, attributes, attribute.name); + + // Create links in the attribute value, and collapse long attributes if + // needed. + let collapse = value => { + if (value && value.match(COLLAPSE_DATA_URL_REGEX)) { + return truncateString(value, COLLAPSE_DATA_URL_LENGTH); + } + return this.markup.collapseAttributes + ? truncateString(value, this.markup.collapseAttributeLength) + : value; + }; + + val.innerHTML = ""; + for (let token of parsedLinksData) { + if (token.type === "string") { + val.appendChild(this.doc.createTextNode(collapse(token.value))); + } else { + let link = this.doc.createElement("span"); + link.classList.add("link"); + link.setAttribute("data-type", token.type); + link.setAttribute("data-link", token.value); + link.textContent = collapse(token.value); + val.appendChild(link); + } + } + + name.textContent = attribute.name; + + return attr; + }, + + /** + * Parse a user-entered attribute string and apply the resulting + * attributes to the node. This operation is undoable. + * + * @param {String} value + * The user-entered value. + * @param {DOMNode} attrNode + * The attribute editor that created this + * set of attributes, used to place new attributes where the + * user put them. + */ + _applyAttributes: function (value, attrNode, doMods, undoMods) { + let attrs = parseAttributeValues(value, this.doc); + for (let attr of attrs) { + // Create an attribute editor next to the current attribute if needed. + this._createAttribute(attr, attrNode ? attrNode.nextSibling : null); + this._saveAttribute(attr.name, undoMods); + doMods.setAttribute(attr.name, attr.value); + } + }, + + /** + * Saves the current state of the given attribute into an attribute + * modification list. + */ + _saveAttribute: function (name, undoMods) { + let node = this.node; + if (node.hasAttribute(name)) { + let oldValue = node.getAttribute(name); + undoMods.setAttribute(name, oldValue); + } else { + undoMods.removeAttribute(name); + } + }, + + /** + * Listen to mutations, and when the attribute list is regenerated + * try to focus on the attribute after the one that's being edited now. + * If the attribute order changes, go to the beginning of the attribute list. + */ + refocusOnEdit: function (attrName, attrNode, direction) { + // Only allow one refocus on attribute change at a time, so when there's + // more than 1 request in parallel, the last one wins. + if (this._editedAttributeObserver) { + this.markup.inspector.off("markupmutation", this._editedAttributeObserver); + this._editedAttributeObserver = null; + } + + let container = this.markup.getContainer(this.node); + + let activeAttrs = [...this.attrList.childNodes] + .filter(el => el.style.display != "none"); + let attributeIndex = activeAttrs.indexOf(attrNode); + + let onMutations = this._editedAttributeObserver = (e, mutations) => { + let isDeletedAttribute = false; + let isNewAttribute = false; + + for (let mutation of mutations) { + let inContainer = + this.markup.getContainer(mutation.target) === container; + if (!inContainer) { + continue; + } + + let isOriginalAttribute = mutation.attributeName === attrName; + + isDeletedAttribute = isDeletedAttribute || isOriginalAttribute && + mutation.newValue === null; + isNewAttribute = isNewAttribute || mutation.attributeName !== attrName; + } + + let isModifiedOrder = isDeletedAttribute && isNewAttribute; + this._editedAttributeObserver = null; + + // "Deleted" attributes are merely hidden, so filter them out. + let visibleAttrs = [...this.attrList.childNodes] + .filter(el => el.style.display != "none"); + let activeEditor; + if (visibleAttrs.length > 0) { + if (!direction) { + // No direction was given; stay on current attribute. + activeEditor = visibleAttrs[attributeIndex]; + } else if (isModifiedOrder) { + // The attribute was renamed, reordering the existing attributes. + // So let's go to the beginning of the attribute list for consistency. + activeEditor = visibleAttrs[0]; + } else { + let newAttributeIndex; + if (isDeletedAttribute) { + newAttributeIndex = attributeIndex; + } else if (direction == Services.focus.MOVEFOCUS_FORWARD) { + newAttributeIndex = attributeIndex + 1; + } else if (direction == Services.focus.MOVEFOCUS_BACKWARD) { + newAttributeIndex = attributeIndex - 1; + } + + // The number of attributes changed (deleted), or we moved through + // the array so check we're still within bounds. + if (newAttributeIndex >= 0 && + newAttributeIndex <= visibleAttrs.length - 1) { + activeEditor = visibleAttrs[newAttributeIndex]; + } + } + } + + // Either we have no attributes left, + // or we just edited the last attribute and want to move on. + if (!activeEditor) { + activeEditor = this.newAttr; + } + + // Refocus was triggered by tab or shift-tab. + // Continue in edit mode. + if (direction) { + activeEditor.editMode(); + } else { + // Refocus was triggered by enter. + // Exit edit mode (but restore focus). + let editable = activeEditor === this.newAttr ? + activeEditor : activeEditor.querySelector(".editable"); + editable.focus(); + } + + this.markup.emit("refocusedonedit"); + }; + + // Start listening for mutations until we find an attributes change + // that modifies this attribute. + this.markup.inspector.once("markupmutation", onMutations); + }, + + /** + * Called when the tag name editor has is done editing. + */ + onTagEdit: function (newTagName, isCommit) { + if (!isCommit || + newTagName.toLowerCase() === this.node.tagName.toLowerCase() || + !("editTagName" in this.markup.walker)) { + return; + } + + // Changing the tagName removes the node. Make sure the replacing node gets + // selected afterwards. + this.markup.reselectOnRemoved(this.node, "edittagname"); + this.markup.walker.editTagName(this.node, newTagName).then(null, () => { + // Failed to edit the tag name, cancel the reselection. + this.markup.cancelReselectOnRemoved(); + }); + }, + + destroy: function () { + for (let key in this.animationTimers) { + clearTimeout(this.animationTimers[key]); + } + this.animationTimers = null; + } +}; + +module.exports = ElementEditor; diff --git a/devtools/client/inspector/markup/views/html-editor.js b/devtools/client/inspector/markup/views/html-editor.js new file mode 100644 index 000000000..6f99391b6 --- /dev/null +++ b/devtools/client/inspector/markup/views/html-editor.js @@ -0,0 +1,180 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Editor = require("devtools/client/sourceeditor/editor"); +const Services = require("Services"); +const EventEmitter = require("devtools/shared/event-emitter"); + +/** + * A wrapper around the Editor component, that allows editing of HTML. + * + * The main functionality this provides around the Editor is the ability + * to show/hide/position an editor inplace. It only appends once to the + * body, and uses CSS to position the editor. The reason it is done this + * way is that the editor is loaded in an iframe, and calling appendChild + * causes it to reload. + * + * Meant to be embedded inside of an HTML page, as in markup.xhtml. + * + * @param {HTMLDocument} htmlDocument + * The document to attach the editor to. Will also use this + * document as a basis for listening resize events. + */ +function HTMLEditor(htmlDocument) { + this.doc = htmlDocument; + this.container = this.doc.createElement("div"); + this.container.className = "html-editor theme-body"; + this.container.style.display = "none"; + this.editorInner = this.doc.createElement("div"); + this.editorInner.className = "html-editor-inner"; + this.container.appendChild(this.editorInner); + + this.doc.body.appendChild(this.container); + this.hide = this.hide.bind(this); + this.refresh = this.refresh.bind(this); + + EventEmitter.decorate(this); + + this.doc.defaultView.addEventListener("resize", + this.refresh, true); + + let config = { + mode: Editor.modes.html, + lineWrapping: true, + styleActiveLine: false, + extraKeys: {}, + theme: "mozilla markup-view" + }; + + config.extraKeys[ctrl("Enter")] = this.hide; + config.extraKeys.F2 = this.hide; + config.extraKeys.Esc = this.hide.bind(this, false); + + this.container.addEventListener("click", this.hide, false); + this.editorInner.addEventListener("click", stopPropagation, false); + this.editor = new Editor(config); + + this.editor.appendToLocalElement(this.editorInner); + this.hide(false); +} + +HTMLEditor.prototype = { + + /** + * Need to refresh position by manually setting CSS values, so this will + * need to be called on resizes and other sizing changes. + */ + refresh: function () { + let element = this._attachedElement; + + if (element) { + this.container.style.top = element.offsetTop + "px"; + this.container.style.left = element.offsetLeft + "px"; + this.container.style.width = element.offsetWidth + "px"; + this.container.style.height = element.parentNode.offsetHeight + "px"; + this.editor.refresh(); + } + }, + + /** + * Anchor the editor to a particular element. + * + * @param {DOMNode} element + * The element that the editor will be anchored to. + * Should belong to the HTMLDocument passed into the constructor. + */ + _attach: function (element) { + this._detach(); + this._attachedElement = element; + element.classList.add("html-editor-container"); + this.refresh(); + }, + + /** + * Unanchor the editor from an element. + */ + _detach: function () { + if (this._attachedElement) { + this._attachedElement.classList.remove("html-editor-container"); + this._attachedElement = undefined; + } + }, + + /** + * Anchor the editor to a particular element, and show the editor. + * + * @param {DOMNode} element + * The element that the editor will be anchored to. + * Should belong to the HTMLDocument passed into the constructor. + * @param {String} text + * Value to set the contents of the editor to + * @param {Function} cb + * The function to call when hiding + */ + show: function (element, text) { + if (this._visible) { + return; + } + + this._originalValue = text; + this.editor.setText(text); + this._attach(element); + this.container.style.display = "flex"; + this._visible = true; + + this.editor.refresh(); + this.editor.focus(); + + this.emit("popupshown"); + }, + + /** + * Hide the editor, optionally committing the changes + * + * @param {Boolean} shouldCommit + * A change will be committed by default. If this param + * strictly equals false, no change will occur. + */ + hide: function (shouldCommit) { + if (!this._visible) { + return; + } + + this.container.style.display = "none"; + this._detach(); + + let newValue = this.editor.getText(); + let valueHasChanged = this._originalValue !== newValue; + let preventCommit = shouldCommit === false || !valueHasChanged; + this._originalValue = undefined; + this._visible = undefined; + this.emit("popuphidden", !preventCommit, newValue); + }, + + /** + * Destroy this object and unbind all event handlers + */ + destroy: function () { + this.doc.defaultView.removeEventListener("resize", + this.refresh, true); + this.container.removeEventListener("click", this.hide, false); + this.editorInner.removeEventListener("click", stopPropagation, false); + + this.hide(false); + this.container.remove(); + this.editor.destroy(); + } +}; + +function ctrl(k) { + return (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") + k; +} + +function stopPropagation(e) { + e.stopPropagation(); +} + +module.exports = HTMLEditor; diff --git a/devtools/client/inspector/markup/views/markup-container.js b/devtools/client/inspector/markup/views/markup-container.js new file mode 100644 index 000000000..b54157242 --- /dev/null +++ b/devtools/client/inspector/markup/views/markup-container.js @@ -0,0 +1,720 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {Task} = require("devtools/shared/task"); +const {KeyCodes} = require("devtools/client/shared/keycodes"); +const {flashElementOn, flashElementOff} = + require("devtools/client/inspector/markup/utils"); + +const DRAG_DROP_MIN_INITIAL_DISTANCE = 10; + +/** + * The main structure for storing a document node in the markup + * tree. Manages creation of the editor for the node and + * a <ul> for placing child elements, and expansion/collapsing + * of the element. + * + * This should not be instantiated directly, instead use one of: + * MarkupReadOnlyContainer + * MarkupTextContainer + * MarkupElementContainer + */ +function MarkupContainer() { } + +/** + * Unique identifier used to set markup container node id. + * @type {Number} + */ +let markupContainerID = 0; + +MarkupContainer.prototype = { + /* + * Initialize the MarkupContainer. Should be called while one + * of the other contain classes is instantiated. + * + * @param {MarkupView} markupView + * The markup view that owns this container. + * @param {NodeFront} node + * The node to display. + * @param {String} templateID + * Which template to render for this container + */ + initialize: function (markupView, node, templateID) { + this.markup = markupView; + this.node = node; + this.undo = this.markup.undo; + this.win = this.markup._frame.contentWindow; + this.id = "treeitem-" + markupContainerID++; + this.htmlElt = this.win.document.documentElement; + + // The template will fill the following properties + this.elt = null; + this.expander = null; + this.tagState = null; + this.tagLine = null; + this.children = null; + this.markup.template(templateID, this); + this.elt.container = this; + + this._onMouseDown = this._onMouseDown.bind(this); + this._onToggle = this._onToggle.bind(this); + this._onMouseUp = this._onMouseUp.bind(this); + this._onMouseMove = this._onMouseMove.bind(this); + this._onKeyDown = this._onKeyDown.bind(this); + + // Binding event listeners + this.elt.addEventListener("mousedown", this._onMouseDown, false); + this.win.addEventListener("mouseup", this._onMouseUp, true); + this.win.addEventListener("mousemove", this._onMouseMove, true); + this.elt.addEventListener("dblclick", this._onToggle, false); + if (this.expander) { + this.expander.addEventListener("click", this._onToggle, false); + } + + // Marking the node as shown or hidden + this.updateIsDisplayed(); + }, + + toString: function () { + return "[MarkupContainer for " + this.node + "]"; + }, + + isPreviewable: function () { + if (this.node.tagName && !this.node.isPseudoElement) { + let tagName = this.node.tagName.toLowerCase(); + let srcAttr = this.editor.getAttributeElement("src"); + let isImage = tagName === "img" && srcAttr; + let isCanvas = tagName === "canvas"; + + return isImage || isCanvas; + } + + return false; + }, + + /** + * Show whether the element is displayed or not + * If an element has the attribute `display: none` or has been hidden with + * the H key, it is not displayed (faded in markup view). + * Otherwise, it is displayed. + */ + updateIsDisplayed: function () { + this.elt.classList.remove("not-displayed"); + if (!this.node.isDisplayed || this.node.hidden) { + this.elt.classList.add("not-displayed"); + } + }, + + /** + * True if the current node has children. The MarkupView + * will set this attribute for the MarkupContainer. + */ + _hasChildren: false, + + get hasChildren() { + return this._hasChildren; + }, + + set hasChildren(value) { + this._hasChildren = value; + this.updateExpander(); + }, + + /** + * A list of all elements with tabindex that are not in container's children. + */ + get focusableElms() { + return [...this.tagLine.querySelectorAll("[tabindex]")]; + }, + + /** + * An indicator that the container internals are focusable. + */ + get canFocus() { + return this._canFocus; + }, + + /** + * Toggle focusable state for container internals. + */ + set canFocus(value) { + if (this._canFocus === value) { + return; + } + + this._canFocus = value; + + if (value) { + this.tagLine.addEventListener("keydown", this._onKeyDown, true); + this.focusableElms.forEach(elm => elm.setAttribute("tabindex", "0")); + } else { + this.tagLine.removeEventListener("keydown", this._onKeyDown, true); + // Exclude from tab order. + this.focusableElms.forEach(elm => elm.setAttribute("tabindex", "-1")); + } + }, + + /** + * If conatiner and its contents are focusable, exclude them from tab order, + * and, if necessary, remove focus. + */ + clearFocus: function () { + if (!this.canFocus) { + return; + } + + this.canFocus = false; + let doc = this.markup.doc; + + if (!doc.activeElement || doc.activeElement === doc.body) { + return; + } + + let parent = doc.activeElement; + + while (parent && parent !== this.elt) { + parent = parent.parentNode; + } + + if (parent) { + doc.activeElement.blur(); + } + }, + + /** + * True if the current node can be expanded. + */ + get canExpand() { + return this._hasChildren && !this.node.inlineTextChild; + }, + + /** + * True if this is the root <html> element and can't be collapsed. + */ + get mustExpand() { + return this.node._parent === this.markup.walker.rootNode; + }, + + /** + * True if current node can be expanded and collapsed. + */ + get showExpander() { + return this.canExpand && !this.mustExpand; + }, + + updateExpander: function () { + if (!this.expander) { + return; + } + + if (this.showExpander) { + this.expander.style.visibility = "visible"; + // Update accessibility expanded state. + this.tagLine.setAttribute("aria-expanded", this.expanded); + } else { + this.expander.style.visibility = "hidden"; + // No need for accessible expanded state indicator when expander is not + // shown. + this.tagLine.removeAttribute("aria-expanded"); + } + }, + + /** + * If current node has no children, ignore them. Otherwise, consider them a + * group from the accessibility point of view. + */ + setChildrenRole: function () { + this.children.setAttribute("role", + this.hasChildren ? "group" : "presentation"); + }, + + /** + * Set an appropriate DOM tree depth level for a node and its subtree. + */ + updateLevel: function () { + // ARIA level should already be set when container template is rendered. + let currentLevel = this.tagLine.getAttribute("aria-level"); + let newLevel = this.level; + if (currentLevel === newLevel) { + // If level did not change, ignore this node and its subtree. + return; + } + + this.tagLine.setAttribute("aria-level", newLevel); + let childContainers = this.getChildContainers(); + if (childContainers) { + childContainers.forEach(container => container.updateLevel()); + } + }, + + /** + * If the node has children, return the list of containers for all these + * children. + */ + getChildContainers: function () { + if (!this.hasChildren) { + return null; + } + + return [...this.children.children].filter(node => node.container) + .map(node => node.container); + }, + + /** + * True if the node has been visually expanded in the tree. + */ + get expanded() { + return !this.elt.classList.contains("collapsed"); + }, + + setExpanded: function (value) { + if (!this.expander) { + return; + } + + if (!this.canExpand) { + value = false; + } + if (this.mustExpand) { + value = true; + } + + if (value && this.elt.classList.contains("collapsed")) { + // Expanding a node means cloning its "inline" closing tag into a new + // tag-line that the user can interact with and showing the children. + let closingTag = this.elt.querySelector(".close"); + if (closingTag) { + if (!this.closeTagLine) { + let line = this.markup.doc.createElement("div"); + line.classList.add("tag-line"); + // Closing tag is not important for accessibility. + line.setAttribute("role", "presentation"); + + let tagState = this.markup.doc.createElement("div"); + tagState.classList.add("tag-state"); + line.appendChild(tagState); + + line.appendChild(closingTag.cloneNode(true)); + + flashElementOff(line); + this.closeTagLine = line; + } + this.elt.appendChild(this.closeTagLine); + } + + this.elt.classList.remove("collapsed"); + this.expander.setAttribute("open", ""); + this.hovered = false; + this.markup.emit("expanded"); + } else if (!value) { + if (this.closeTagLine) { + this.elt.removeChild(this.closeTagLine); + this.closeTagLine = undefined; + } + this.elt.classList.add("collapsed"); + this.expander.removeAttribute("open"); + this.markup.emit("collapsed"); + } + if (this.showExpander) { + this.tagLine.setAttribute("aria-expanded", this.expanded); + } + }, + + parentContainer: function () { + return this.elt.parentNode ? this.elt.parentNode.container : null; + }, + + /** + * Determine tree depth level of a given node. This is used to specify ARIA + * level for node tree items and to give them better semantic context. + */ + get level() { + let level = 1; + let parent = this.node.parentNode(); + while (parent && parent !== this.markup.walker.rootNode) { + level++; + parent = parent.parentNode(); + } + return level; + }, + + _isDragging: false, + _dragStartY: 0, + + set isDragging(isDragging) { + let rootElt = this.markup.getContainer(this.markup._rootNode).elt; + this._isDragging = isDragging; + this.markup.isDragging = isDragging; + this.tagLine.setAttribute("aria-grabbed", isDragging); + + if (isDragging) { + this.htmlElt.classList.add("dragging"); + this.elt.classList.add("dragging"); + this.markup.doc.body.classList.add("dragging"); + rootElt.setAttribute("aria-dropeffect", "move"); + } else { + this.htmlElt.classList.remove("dragging"); + this.elt.classList.remove("dragging"); + this.markup.doc.body.classList.remove("dragging"); + rootElt.setAttribute("aria-dropeffect", "none"); + } + }, + + get isDragging() { + return this._isDragging; + }, + + /** + * Check if element is draggable. + */ + isDraggable: function () { + let tagName = this.node.tagName && this.node.tagName.toLowerCase(); + + return !this.node.isPseudoElement && + !this.node.isAnonymous && + !this.node.isDocumentElement && + tagName !== "body" && + tagName !== "head" && + this.win.getSelection().isCollapsed && + this.node.parentNode().tagName !== null; + }, + + /** + * Move keyboard focus to a next/previous focusable element inside container + * that is not part of its children (only if current focus is on first or last + * element). + * + * @param {DOMNode} current currently focused element + * @param {Boolean} back direction + * @return {DOMNode} newly focused element if any + */ + _wrapMoveFocus: function (current, back) { + let elms = this.focusableElms; + let next; + if (back) { + if (elms.indexOf(current) === 0) { + next = elms[elms.length - 1]; + next.focus(); + } + } else if (elms.indexOf(current) === elms.length - 1) { + next = elms[0]; + next.focus(); + } + return next; + }, + + _onKeyDown: function (event) { + let {target, keyCode, shiftKey} = event; + let isInput = this.markup._isInputOrTextarea(target); + + // Ignore all keystrokes that originated in editors except for when 'Tab' is + // pressed. + if (isInput && keyCode !== KeyCodes.DOM_VK_TAB) { + return; + } + + switch (keyCode) { + case KeyCodes.DOM_VK_TAB: + // Only handle 'Tab' if tabbable element is on the edge (first or last). + if (isInput) { + // Corresponding tabbable element is editor's next sibling. + let next = this._wrapMoveFocus(target.nextSibling, shiftKey); + if (next) { + event.preventDefault(); + // Keep the editing state if possible. + if (next._editable) { + let e = this.markup.doc.createEvent("Event"); + e.initEvent(next._trigger, true, true); + next.dispatchEvent(e); + } + } + } else { + let next = this._wrapMoveFocus(target, shiftKey); + if (next) { + event.preventDefault(); + } + } + break; + case KeyCodes.DOM_VK_ESCAPE: + this.clearFocus(); + this.markup.getContainer(this.markup._rootNode).elt.focus(); + if (this.isDragging) { + // Escape when dragging is handled by markup view itself. + return; + } + event.preventDefault(); + break; + default: + return; + } + event.stopPropagation(); + }, + + _onMouseDown: function (event) { + let {target, button, metaKey, ctrlKey} = event; + let isLeftClick = button === 0; + let isMiddleClick = button === 1; + let isMetaClick = isLeftClick && (metaKey || ctrlKey); + + // The "show more nodes" button already has its onclick, so early return. + if (target.nodeName === "button") { + return; + } + + // target is the MarkupContainer itself. + this.hovered = false; + this.markup.navigate(this); + // Make container tabbable descendants tabbable and focus in. + this.canFocus = true; + this.focus(); + event.stopPropagation(); + + // Preventing the default behavior will avoid the body to gain focus on + // mouseup (through bubbling) when clicking on a non focusable node in the + // line. So, if the click happened outside of a focusable element, do + // prevent the default behavior, so that the tagname or textcontent gains + // focus. + if (!target.closest(".editor [tabindex]")) { + event.preventDefault(); + } + + // Follow attribute links if middle or meta click. + if (isMiddleClick || isMetaClick) { + let link = target.dataset.link; + let type = target.dataset.type; + // Make container tabbable descendants not tabbable (by default). + this.canFocus = false; + this.markup.inspector.followAttributeLink(type, link); + return; + } + + // Start node drag & drop (if the mouse moved, see _onMouseMove). + if (isLeftClick && this.isDraggable()) { + this._isPreDragging = true; + this._dragStartY = event.pageY; + } + }, + + /** + * On mouse up, stop dragging. + */ + _onMouseUp: Task.async(function* () { + this._isPreDragging = false; + + if (this.isDragging) { + this.cancelDragging(); + + let dropTargetNodes = this.markup.dropTargetNodes; + + if (!dropTargetNodes) { + return; + } + + yield this.markup.walker.insertBefore(this.node, dropTargetNodes.parent, + dropTargetNodes.nextSibling); + this.markup.emit("drop-completed"); + } + }), + + /** + * On mouse move, move the dragged element and indicate the drop target. + */ + _onMouseMove: function (event) { + // If this is the first move after mousedown, only start dragging after the + // mouse has travelled a few pixels and then indicate the start position. + let initialDiff = Math.abs(event.pageY - this._dragStartY); + if (this._isPreDragging && initialDiff >= DRAG_DROP_MIN_INITIAL_DISTANCE) { + this._isPreDragging = false; + this.isDragging = true; + + // If this is the last child, use the closing <div.tag-line> of parent as + // indicator. + let position = this.elt.nextElementSibling || + this.markup.getContainer(this.node.parentNode()) + .closeTagLine; + this.markup.indicateDragTarget(position); + } + + if (this.isDragging) { + let x = 0; + let y = event.pageY - this.win.scrollY; + + // Ensure we keep the dragged element within the markup view. + if (y < 0) { + y = 0; + } else if (y >= this.markup.doc.body.offsetHeight - this.win.scrollY) { + y = this.markup.doc.body.offsetHeight - this.win.scrollY - 1; + } + + let diff = y - this._dragStartY + this.win.scrollY; + this.elt.style.top = diff + "px"; + + let el = this.markup.doc.elementFromPoint(x, y); + this.markup.indicateDropTarget(el); + } + }, + + cancelDragging: function () { + if (!this.isDragging) { + return; + } + + this._isPreDragging = false; + this.isDragging = false; + this.elt.style.removeProperty("top"); + }, + + /** + * Temporarily flash the container to attract attention. + * Used for markup mutations. + */ + flashMutation: function () { + if (!this.selected) { + flashElementOn(this.tagState, this.editor.elt); + if (this._flashMutationTimer) { + clearTimeout(this._flashMutationTimer); + this._flashMutationTimer = null; + } + this._flashMutationTimer = setTimeout(() => { + flashElementOff(this.tagState, this.editor.elt); + }, this.markup.CONTAINER_FLASHING_DURATION); + } + }, + + _hovered: false, + + /** + * Highlight the currently hovered tag + its closing tag if necessary + * (that is if the tag is expanded) + */ + set hovered(value) { + this.tagState.classList.remove("flash-out"); + this._hovered = value; + if (value) { + if (!this.selected) { + this.tagState.classList.add("theme-bg-darker"); + } + if (this.closeTagLine) { + this.closeTagLine.querySelector(".tag-state").classList.add( + "theme-bg-darker"); + } + } else { + this.tagState.classList.remove("theme-bg-darker"); + if (this.closeTagLine) { + this.closeTagLine.querySelector(".tag-state").classList.remove( + "theme-bg-darker"); + } + } + }, + + /** + * True if the container is visible in the markup tree. + */ + get visible() { + return this.elt.getBoundingClientRect().height > 0; + }, + + /** + * True if the container is currently selected. + */ + _selected: false, + + get selected() { + return this._selected; + }, + + set selected(value) { + this.tagState.classList.remove("flash-out"); + this._selected = value; + this.editor.selected = value; + // Markup tree item should have accessible selected state. + this.tagLine.setAttribute("aria-selected", value); + if (this._selected) { + let container = this.markup.getContainer(this.markup._rootNode); + if (container) { + container.elt.setAttribute("aria-activedescendant", this.id); + } + this.tagLine.setAttribute("selected", ""); + this.tagState.classList.add("theme-selected"); + } else { + this.tagLine.removeAttribute("selected"); + this.tagState.classList.remove("theme-selected"); + } + }, + + /** + * Update the container's editor to the current state of the + * viewed node. + */ + update: function () { + if (this.node.pseudoClassLocks.length) { + this.elt.classList.add("pseudoclass-locked"); + } else { + this.elt.classList.remove("pseudoclass-locked"); + } + + if (this.editor.update) { + this.editor.update(); + } + }, + + /** + * Try to put keyboard focus on the current editor. + */ + focus: function () { + // Elements with tabindex of -1 are not focusable. + let focusable = this.editor.elt.querySelector("[tabindex='0']"); + if (focusable) { + focusable.focus(); + } + }, + + _onToggle: function (event) { + this.markup.navigate(this); + if (this.hasChildren) { + this.markup.setNodeExpanded(this.node, !this.expanded, event.altKey); + } + event.stopPropagation(); + }, + + /** + * Get rid of event listeners and references, when the container is no longer + * needed + */ + destroy: function () { + // Remove event listeners + this.elt.removeEventListener("mousedown", this._onMouseDown, false); + this.elt.removeEventListener("dblclick", this._onToggle, false); + this.tagLine.removeEventListener("keydown", this._onKeyDown, true); + if (this.win) { + this.win.removeEventListener("mouseup", this._onMouseUp, true); + this.win.removeEventListener("mousemove", this._onMouseMove, true); + } + + this.win = null; + this.htmlElt = null; + + if (this.expander) { + this.expander.removeEventListener("click", this._onToggle, false); + } + + // Recursively destroy children containers + let firstChild = this.children.firstChild; + while (firstChild) { + // Not all children of a container are containers themselves + // ("show more nodes" button is one example) + if (firstChild.container) { + firstChild.container.destroy(); + } + this.children.removeChild(firstChild); + firstChild = this.children.firstChild; + } + + this.editor.destroy(); + } +}; + +module.exports = MarkupContainer; diff --git a/devtools/client/inspector/markup/views/moz.build b/devtools/client/inspector/markup/views/moz.build new file mode 100644 index 000000000..846bc6a84 --- /dev/null +++ b/devtools/client/inspector/markup/views/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DevToolsModules( + 'element-container.js', + 'element-editor.js', + 'html-editor.js', + 'markup-container.js', + 'read-only-container.js', + 'read-only-editor.js', + 'root-container.js', + 'text-container.js', + 'text-editor.js', +) diff --git a/devtools/client/inspector/markup/views/read-only-container.js b/devtools/client/inspector/markup/views/read-only-container.js new file mode 100644 index 000000000..fd645baac --- /dev/null +++ b/devtools/client/inspector/markup/views/read-only-container.js @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Heritage = require("sdk/core/heritage"); +const ReadOnlyEditor = require("devtools/client/inspector/markup/views/read-only-editor"); +const MarkupContainer = require("devtools/client/inspector/markup/views/markup-container"); + +/** + * An implementation of MarkupContainer for Pseudo Elements, + * Doctype nodes, or any other type generic node that doesn't + * fit for other editors. + * Does not allow any editing, just viewing / selecting. + * + * @param {MarkupView} markupView + * The markup view that owns this container. + * @param {NodeFront} node + * The node to display. + */ +function MarkupReadOnlyContainer(markupView, node) { + MarkupContainer.prototype.initialize.call(this, markupView, node, + "readonlycontainer"); + + this.editor = new ReadOnlyEditor(this, node); + this.tagLine.appendChild(this.editor.elt); +} + +MarkupReadOnlyContainer.prototype = + Heritage.extend(MarkupContainer.prototype, {}); + +module.exports = MarkupReadOnlyContainer; diff --git a/devtools/client/inspector/markup/views/read-only-editor.js b/devtools/client/inspector/markup/views/read-only-editor.js new file mode 100644 index 000000000..dbc39eeb7 --- /dev/null +++ b/devtools/client/inspector/markup/views/read-only-editor.js @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const nodeConstants = require("devtools/shared/dom-node-constants"); + +/** + * Creates an editor for non-editable nodes. + */ +function ReadOnlyEditor(container, node) { + this.container = container; + this.markup = this.container.markup; + this.template = this.markup.template.bind(this.markup); + this.elt = null; + this.template("generic", this); + + if (node.isPseudoElement) { + this.tag.classList.add("theme-fg-color5"); + this.tag.textContent = node.isBeforePseudoElement ? "::before" : "::after"; + } else if (node.nodeType == nodeConstants.DOCUMENT_TYPE_NODE) { + this.elt.classList.add("comment"); + this.tag.textContent = node.doctypeString; + } else { + this.tag.textContent = node.nodeName; + } +} + +ReadOnlyEditor.prototype = { + destroy: function () { + this.elt.remove(); + }, + + /** + * Stub method for consistency with ElementEditor. + */ + getInfoAtNode: function () { + return null; + } +}; + +module.exports = ReadOnlyEditor; diff --git a/devtools/client/inspector/markup/views/root-container.js b/devtools/client/inspector/markup/views/root-container.js new file mode 100644 index 000000000..ccc918fca --- /dev/null +++ b/devtools/client/inspector/markup/views/root-container.js @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Dummy container node used for the root document element. + */ +function RootContainer(markupView, node) { + this.doc = markupView.doc; + this.elt = this.doc.createElement("ul"); + // Root container has tree semantics for accessibility. + this.elt.setAttribute("role", "tree"); + this.elt.setAttribute("tabindex", "0"); + this.elt.setAttribute("aria-dropeffect", "none"); + this.elt.container = this; + this.children = this.elt; + this.node = node; + this.toString = () => "[root container]"; +} + +RootContainer.prototype = { + hasChildren: true, + expanded: true, + update: function () {}, + destroy: function () {}, + + /** + * If the node has children, return the list of containers for all these children. + * @return {Array} An array of child containers or null. + */ + getChildContainers: function () { + return [...this.children.children].filter(node => node.container) + .map(node => node.container); + }, + + /** + * Set the expanded state of the container node. + * @param {Boolean} value + */ + setExpanded: function () {}, + + /** + * Set an appropriate role of the container's children node. + */ + setChildrenRole: function () {}, + + /** + * Set an appropriate DOM tree depth level for a node and its subtree. + */ + updateLevel: function () {} +}; + +module.exports = RootContainer; diff --git a/devtools/client/inspector/markup/views/text-container.js b/devtools/client/inspector/markup/views/text-container.js new file mode 100644 index 000000000..357f17778 --- /dev/null +++ b/devtools/client/inspector/markup/views/text-container.js @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Heritage = require("sdk/core/heritage"); +const nodeConstants = require("devtools/shared/dom-node-constants"); +const TextEditor = require("devtools/client/inspector/markup/views/text-editor"); +const MarkupContainer = require("devtools/client/inspector/markup/views/markup-container"); + +/** + * An implementation of MarkupContainer for text node and comment nodes. + * Allows basic text editing in a textarea. + * + * @param {MarkupView} markupView + * The markup view that owns this container. + * @param {NodeFront} node + * The node to display. + * @param {Inspector} inspector + * The inspector tool container the markup-view + */ +function MarkupTextContainer(markupView, node) { + MarkupContainer.prototype.initialize.call(this, markupView, node, + "textcontainer"); + + if (node.nodeType == nodeConstants.TEXT_NODE) { + this.editor = new TextEditor(this, node, "text"); + } else if (node.nodeType == nodeConstants.COMMENT_NODE) { + this.editor = new TextEditor(this, node, "comment"); + } else { + throw new Error("Invalid node for MarkupTextContainer"); + } + + this.tagLine.appendChild(this.editor.elt); +} + +MarkupTextContainer.prototype = Heritage.extend(MarkupContainer.prototype, {}); + +module.exports = MarkupTextContainer; diff --git a/devtools/client/inspector/markup/views/text-editor.js b/devtools/client/inspector/markup/views/text-editor.js new file mode 100644 index 000000000..f3c83ca87 --- /dev/null +++ b/devtools/client/inspector/markup/views/text-editor.js @@ -0,0 +1,109 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {getAutocompleteMaxWidth} = require("devtools/client/inspector/markup/utils"); +const {editableField} = require("devtools/client/shared/inplace-editor"); +const {getCssProperties} = require("devtools/shared/fronts/css-properties"); +const {LocalizationHelper} = require("devtools/shared/l10n"); + +const INSPECTOR_L10N = + new LocalizationHelper("devtools/client/locales/inspector.properties"); + +/** + * Creates a simple text editor node, used for TEXT and COMMENT + * nodes. + * + * @param {MarkupContainer} container + * The container owning this editor. + * @param {DOMNode} node + * The node being edited. + * @param {String} templateId + * The template id to use to build the editor. + */ +function TextEditor(container, node, templateId) { + this.container = container; + this.markup = this.container.markup; + this.node = node; + this.template = this.markup.template.bind(templateId); + this._selected = false; + + this.markup.template(templateId, this); + + editableField({ + element: this.value, + stopOnReturn: true, + trigger: "dblclick", + multiline: true, + maxWidth: () => getAutocompleteMaxWidth(this.value, this.container.elt), + trimOutput: false, + done: (val, commit) => { + if (!commit) { + return; + } + this.node.getNodeValue().then(longstr => { + longstr.string().then(oldValue => { + longstr.release().then(null, console.error); + + this.container.undo.do(() => { + this.node.setNodeValue(val); + }, () => { + this.node.setNodeValue(oldValue); + }); + }); + }); + }, + cssProperties: getCssProperties(this.markup.toolbox), + contextMenu: this.markup.inspector.onTextBoxContextMenu + }); + + this.update(); +} + +TextEditor.prototype = { + get selected() { + return this._selected; + }, + + set selected(value) { + if (value === this._selected) { + return; + } + this._selected = value; + this.update(); + }, + + update: function () { + let longstr = null; + this.node.getNodeValue().then(ret => { + longstr = ret; + return longstr.string(); + }).then(str => { + longstr.release().then(null, console.error); + this.value.textContent = str; + + let isWhitespace = !/[^\s]/.exec(str); + this.value.classList.toggle("whitespace", isWhitespace); + + let chars = str.replace(/\n/g, "⏎") + .replace(/\t/g, "⇥") + .replace(/ /g, "◦"); + this.value.setAttribute("title", isWhitespace + ? INSPECTOR_L10N.getFormatStr("markupView.whitespaceOnly", chars) + : ""); + }).then(null, console.error); + }, + + destroy: function () {}, + + /** + * Stub method for consistency with ElementEditor. + */ + getInfoAtNode: function () { + return null; + } +}; + +module.exports = TextEditor; diff --git a/devtools/client/inspector/moz.build b/devtools/client/inspector/moz.build new file mode 100644 index 000000000..bdf3e3887 --- /dev/null +++ b/devtools/client/inspector/moz.build @@ -0,0 +1,23 @@ +# 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/. + +DIRS += [ + 'components', + 'computed', + 'fonts', + 'layout', + 'markup', + 'rules', + 'shared' +] + +DevToolsModules( + 'breadcrumbs.js', + 'inspector-commands.js', + 'inspector-search.js', + 'panel.js', + 'toolsidebar.js', +) + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/devtools/client/inspector/panel.js b/devtools/client/inspector/panel.js new file mode 100644 index 000000000..7f733491b --- /dev/null +++ b/devtools/client/inspector/panel.js @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function InspectorPanel(iframeWindow, toolbox) { + this._inspector = new iframeWindow.Inspector(toolbox); +} +InspectorPanel.prototype = { + open() { + return this._inspector.init(); + }, + + destroy() { + return this._inspector.destroy(); + } +}; +exports.InspectorPanel = InspectorPanel; diff --git a/devtools/client/inspector/rules/models/element-style.js b/devtools/client/inspector/rules/models/element-style.js new file mode 100644 index 000000000..7f015ba08 --- /dev/null +++ b/devtools/client/inspector/rules/models/element-style.js @@ -0,0 +1,412 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const promise = require("promise"); +const {Rule} = require("devtools/client/inspector/rules/models/rule"); +const {promiseWarn} = require("devtools/client/inspector/shared/utils"); +const {ELEMENT_STYLE} = require("devtools/shared/specs/styles"); +const {getCssProperties} = require("devtools/shared/fronts/css-properties"); + +/** + * ElementStyle is responsible for the following: + * Keeps track of which properties are overridden. + * Maintains a list of Rule objects for a given element. + * + * @param {Element} element + * The element whose style we are viewing. + * @param {CssRuleView} ruleView + * The instance of the rule-view panel. + * @param {Object} store + * The ElementStyle can use this object to store metadata + * that might outlast the rule view, particularly the current + * set of disabled properties. + * @param {PageStyleFront} pageStyle + * Front for the page style actor that will be providing + * the style information. + * @param {Boolean} showUserAgentStyles + * Should user agent styles be inspected? + */ +function ElementStyle(element, ruleView, store, pageStyle, + showUserAgentStyles) { + this.element = element; + this.ruleView = ruleView; + this.store = store || {}; + this.pageStyle = pageStyle; + this.showUserAgentStyles = showUserAgentStyles; + this.rules = []; + this.cssProperties = getCssProperties(this.ruleView.inspector.toolbox); + + // We don't want to overwrite this.store.userProperties so we only create it + // if it doesn't already exist. + if (!("userProperties" in this.store)) { + this.store.userProperties = new UserProperties(); + } + + if (!("disabled" in this.store)) { + this.store.disabled = new WeakMap(); + } +} + +ElementStyle.prototype = { + // The element we're looking at. + element: null, + + destroy: function () { + if (this.destroyed) { + return; + } + this.destroyed = true; + + for (let rule of this.rules) { + if (rule.editor) { + rule.editor.destroy(); + } + } + }, + + /** + * Called by the Rule object when it has been changed through the + * setProperty* methods. + */ + _changed: function () { + if (this.onChanged) { + this.onChanged(); + } + }, + + /** + * Refresh the list of rules to be displayed for the active element. + * Upon completion, this.rules[] will hold a list of Rule objects. + * + * Returns a promise that will be resolved when the elementStyle is + * ready. + */ + populate: function () { + let populated = this.pageStyle.getApplied(this.element, { + inherited: true, + matchedSelectors: true, + filter: this.showUserAgentStyles ? "ua" : undefined, + }).then(entries => { + if (this.destroyed) { + return promise.resolve(undefined); + } + + if (this.populated !== populated) { + // Don't care anymore. + return promise.resolve(undefined); + } + + // Store the current list of rules (if any) during the population + // process. They will be reused if possible. + let existingRules = this.rules; + + this.rules = []; + + for (let entry of entries) { + this._maybeAddRule(entry, existingRules); + } + + // Mark overridden computed styles. + this.markOverriddenAll(); + + this._sortRulesForPseudoElement(); + + // We're done with the previous list of rules. + for (let r of existingRules) { + if (r && r.editor) { + r.editor.destroy(); + } + } + + return undefined; + }).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); + }); + this.populated = populated; + return this.populated; + }, + + /** + * Put pseudo elements in front of others. + */ + _sortRulesForPseudoElement: function () { + this.rules = this.rules.sort((a, b) => { + return (a.pseudoElement || "z") > (b.pseudoElement || "z"); + }); + }, + + /** + * 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, 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 (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 visible properties. + if (options.inherited && !rule.hasAnyVisibleProperties()) { + return false; + } + + this.rules.push(rule); + return true; + }, + + /** + * Calls markOverridden with all supported pseudo elements + */ + markOverriddenAll: function () { + this.markOverridden(); + for (let pseudo of this.cssProperties.pseudoElements) { + this.markOverridden(pseudo); + } + }, + + /** + * Mark the properties listed in this.rules for a given pseudo element + * with an overridden flag if an earlier property overrides it. + * + * @param {String} pseudo + * Which pseudo element to flag as overridden. + * Empty string or undefined will default to no pseudo element. + */ + markOverridden: function (pseudo = "") { + // Gather all the text properties applied by these rules, ordered + // 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.matchedSelectors.length > 0 || + rule.domRule.type === ELEMENT_STYLE) && + rule.pseudoElement === pseudo && !rule.keyframes) { + for (let textProp of rule.textProps.slice(0).reverse()) { + if (textProp.enabled) { + textProps.push(textProp); + } + } + } + } + + // Gather all the computed properties applied by those text + // properties. + let computedProps = []; + for (let textProp of textProps) { + computedProps = computedProps.concat(textProp.computed); + } + + // Walk over the computed properties. As we see a property name + // for the first time, mark that property's name as taken by this + // property. + // + // If we come across a property whose name is already taken, check + // its priority against the property that was found first: + // + // If the new property is a higher priority, mark the old + // property overridden and mark the property name as taken by + // the new property. + // + // 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). + earlier._overriddenDirty = !earlier._overriddenDirty; + earlier.overridden = true; + overridden = false; + } else { + overridden = !!earlier; + } + + computedProp._overriddenDirty = + (!!computedProp.overridden !== overridden); + computedProp.overridden = overridden; + if (!computedProp.overridden && computedProp.textProp.enabled) { + taken[computedProp.name] = computedProp; + } + } + + // For each TextProperty, mark it overridden if all of its + // computed properties are marked overridden. Update the text + // property's associated editor, if any. This will clear the + // _overriddenDirty state on all computed properties. + for (let textProp of textProps) { + // _updatePropertyOverridden will return true if the + // overridden state has changed for the text property. + if (this._updatePropertyOverridden(textProp)) { + textProp.updateEditor(); + } + } + }, + + /** + * Mark a given TextProperty as overridden or not depending on the + * state of its computed properties. Clears the _overriddenDirty state + * on all computed properties. + * + * @param {TextProperty} prop + * The text property to update. + * @return {Boolean} true if the TextProperty's overridden state (or any of + * its computed properties overridden state) changed. + */ + _updatePropertyOverridden: function (prop) { + let overridden = true; + let dirty = false; + for (let computedProp of prop.computed) { + if (!computedProp.overridden) { + overridden = false; + } + dirty = computedProp._overriddenDirty || dirty; + delete computedProp._overriddenDirty; + } + + dirty = (!!prop.overridden !== overridden) || dirty; + prop.overridden = overridden; + return dirty; + } +}; + +/** + * Store of CSSStyleDeclarations mapped to properties that have been changed by + * the user. + */ +function UserProperties() { + this.map = new Map(); +} + +UserProperties.prototype = { + /** + * Get a named property for a given CSSStyleDeclaration. + * + * @param {CSSStyleDeclaration} style + * The CSSStyleDeclaration against which the property is mapped. + * @param {String} name + * The name of the property to get. + * @param {String} value + * Default value. + * @return {String} + * The property value if it has previously been set by the user, null + * otherwise. + */ + getProperty: function (style, name, value) { + let key = this.getKey(style); + let entry = this.map.get(key, null); + + if (entry && name in entry) { + return entry[name]; + } + return value; + }, + + /** + * Set a named property for a given CSSStyleDeclaration. + * + * @param {CSSStyleDeclaration} style + * The CSSStyleDeclaration against which the property is to be mapped. + * @param {String} bame + * The name of the property to set. + * @param {String} userValue + * The value of the property to set. + */ + setProperty: function (style, bame, userValue) { + let key = this.getKey(style, bame); + let entry = this.map.get(key, null); + + if (entry) { + entry[bame] = userValue; + } else { + let props = {}; + props[bame] = userValue; + this.map.set(key, props); + } + }, + + /** + * Check whether a named property for a given CSSStyleDeclaration is stored. + * + * @param {CSSStyleDeclaration} style + * The CSSStyleDeclaration against which the property would be mapped. + * @param {String} name + * The name of the property to check. + */ + contains: function (style, name) { + let key = this.getKey(style, name); + let entry = this.map.get(key, null); + return !!entry && name in entry; + }, + + getKey: function (style, name) { + return style.actorID + ":" + name; + }, + + clear: function () { + this.map.clear(); + } +}; + +exports.ElementStyle = ElementStyle; diff --git a/devtools/client/inspector/rules/models/moz.build b/devtools/client/inspector/rules/models/moz.build new file mode 100644 index 000000000..1c5c0f89f --- /dev/null +++ b/devtools/client/inspector/rules/models/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DevToolsModules( + 'element-style.js', + 'rule.js', + 'text-property.js', +) diff --git a/devtools/client/inspector/rules/models/rule.js b/devtools/client/inspector/rules/models/rule.js new file mode 100644 index 000000000..1a3fa057a --- /dev/null +++ b/devtools/client/inspector/rules/models/rule.js @@ -0,0 +1,686 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const promise = require("promise"); +const CssLogic = require("devtools/shared/inspector/css-logic"); +const {ELEMENT_STYLE} = require("devtools/shared/specs/styles"); +const {TextProperty} = + require("devtools/client/inspector/rules/models/text-property"); +const {promiseWarn} = require("devtools/client/inspector/shared/utils"); +const {parseDeclarations} = require("devtools/shared/css/parsing-utils"); +const Services = require("Services"); + +const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties"; +const {LocalizationHelper} = require("devtools/shared/l10n"); +const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES); + +/** + * Rule is responsible for the following: + * Manages a single style declaration or rule. + * Applies changes to the properties in a rule. + * Maintains a list of TextProperty objects. + * + * @param {ElementStyle} elementStyle + * The ElementStyle to which this rule belongs. + * @param {Object} options + * The information used to construct this rule. Properties include: + * rule: A StyleRuleActor + * inherited: An element this rule was inherited from. If omitted, + * the rule applies directly to the current element. + * isSystem: Is this a user agent style? + * isUnmatched: True if the rule does not match the current selected + * element, otherwise, false. + */ +function Rule(elementStyle, options) { + this.elementStyle = elementStyle; + this.domRule = options.rule || null; + this.style = options.rule; + this.matchedSelectors = options.matchedSelectors || []; + this.pseudoElement = options.pseudoElement || ""; + + this.isSystem = options.isSystem; + this.isUnmatched = options.isUnmatched || false; + this.inherited = options.inherited || null; + this.keyframes = options.keyframes || null; + this._modificationDepth = 0; + + if (this.domRule && this.domRule.mediaText) { + this.mediaText = this.domRule.mediaText; + } + + this.cssProperties = this.elementStyle.ruleView.cssProperties; + + // 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() { + let title = CssLogic.shortSource(this.sheet); + if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) { + title += ":" + this.ruleLine; + } + + return title + (this.mediaText ? " @media " + this.mediaText : ""); + }, + + get inheritedSource() { + if (this._inheritedSource) { + return this._inheritedSource; + } + this._inheritedSource = ""; + if (this.inherited) { + let eltText = this.inherited.displayName; + if (this.inherited.id) { + eltText += "#" + this.inherited.id; + } + this._inheritedSource = + STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", eltText); + } + return this._inheritedSource; + }, + + get keyframesName() { + if (this._keyframesName) { + return this._keyframesName; + } + this._keyframesName = ""; + if (this.keyframes) { + this._keyframesName = + STYLE_INSPECTOR_L10N.getFormatStr("rule.keyframe", this.keyframes.name); + } + return this._keyframesName; + }, + + get selectorText() { + return this.domRule.selectors ? this.domRule.selectors.join(", ") : + CssLogic.l10n("rule.sourceElement"); + }, + + /** + * The rule's stylesheet. + */ + get sheet() { + return this.domRule ? this.domRule.parentStyleSheet : null; + }, + + /** + * The rule's line within a stylesheet + */ + get ruleLine() { + return this.domRule ? this.domRule.line : ""; + }, + + /** + * The rule's column within a stylesheet + */ + get ruleColumn() { + return this.domRule ? this.domRule.column : null; + }, + + /** + * 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 () { + return this.domRule.getOriginalLocation().then(({href, + line, mediaText}) => { + let mediaString = mediaText ? " @" + mediaText : ""; + let linePart = line > 0 ? (":" + line) : ""; + + let sourceStrings = { + full: (href || CssLogic.l10n("rule.sourceInline")) + linePart + + mediaString, + short: CssLogic.shortSource({href: href}) + linePart + mediaString + }; + + return sourceStrings; + }); + }, + + /** + * Returns true if the rule matches the creation options + * specified. + * + * @param {Object} options + * Creation options. See the Rule constructor for documentation. + */ + matches: function (options) { + return this.style === options.rule; + }, + + /** + * Create a new TextProperty to include in the rule. + * + * @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 + * True if the property should be enabled. + * @param {TextProperty} siblingProp + * Optional, property next to which the new property will be added. + */ + createProperty: function (name, value, priority, enabled, siblingProp) { + let prop = new TextProperty(this, name, value, priority, enabled); + + let ind; + if (siblingProp) { + ind = this.textProps.indexOf(siblingProp) + 1; + this.textProps.splice(ind, 0, prop); + } else { + ind = this.textProps.length; + this.textProps.push(prop); + } + + this.applyProperties((modifications) => { + modifications.createProperty(ind, name, value, priority, enabled); + // Now that the rule has been updated, the server might have given us data + // that changes the state of the property. Update it now. + prop.updateEditor(); + }); + + return prop; + }, + + /** + * 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. + */ + _applyPropertiesNoAuthored: function (modifications) { + this.elementStyle.markOverriddenAll(); + + 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(-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); + } + + return modifications.apply().then(() => { + let cssProps = {}; + // Note that even though StyleRuleActors normally provide parsed + // declarations already, _applyPropertiesNoAuthored is only used when + // connected to older backend that do not provide them. So parse here. + for (let cssProp of parseDeclarations(this.cssProperties.isKnown, + this.style.authoredText)) { + cssProps[cssProp.name] = cssProp; + } + + for (let textProp of this.textProps) { + if (!textProp.enabled) { + continue; + } + let cssProp = cssProps[textProp.name]; + + if (!cssProp) { + cssProp = { + name: textProp.name, + value: "", + priority: "" + }; + } + + textProp.priority = cssProp.priority; + } + }); + }, + + /** + * 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(); + } + } + }); + }, + + /** + * 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( + this.cssProperties); + 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 oldName = property.name; + property.name = name; + let index = this.textProps.indexOf(property); + this.applyProperties((modifications) => { + modifications.renameProperty(index, oldName, name); + }); + }, + + /** + * Sets the value and priority of a property, then reapply all properties. + * + * @param {TextProperty} property + * The property to manipulate. + * @param {String} value + * The property's value (not including priority). + * @param {String} priority + * The property's priority (either "important" or an empty string). + */ + setPropertyValue: function (property, value, priority) { + if (value === property.value && priority === property.priority) { + return; + } + + property.value = value; + property.priority = priority; + + 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(this.cssProperties); + 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 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) { + 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) => { + modifications.removeProperty(index, property.name); + }); + }, + + /** + * Get the list of TextProperties from the style. Needs + * to parse the style's authoredText. + */ + _getTextProperties: function () { + let textProps = []; + let store = this.elementStyle.store; + + // Starting with FF49, StyleRuleActors provide parsed declarations. + let props = this.style.declarations; + if (!props.length) { + props = parseDeclarations(this.cssProperties.isKnown, + this.style.authoredText, true); + } + + for (let prop of props) { + let name = prop.name; + // If the authored text has an invalid property, it will show up + // as nameless. Skip these as we don't currently have a good + // way to display them. + if (!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 && !this.cssProperties.isInherited(name); + let value = store.userProperties.getProperty(this.style, name, + prop.value); + 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. + */ + _getDisabledProperties: function () { + let store = this.elementStyle.store; + + // Include properties from the disabled property store, if any. + let disabledProps = store.disabled.get(this.style); + if (!disabledProps) { + return []; + } + + let textProps = []; + + for (let prop of disabledProps) { + let value = store.userProperties.getProperty(this.style, prop.name, + prop.value); + let textProp = new TextProperty(this, prop.name, value, prop.priority); + textProp.enabled = false; + textProps.push(textProp); + } + + return textProps; + }, + + /** + * Reread the current state of the rules and rebuild text + * properties as needed. + */ + refresh: function (options) { + this.matchedSelectors = options.matchedSelectors || []; + let newTextProps = this._getTextProperties(); + + // Update current properties for each property present on the style. + // This will mark any touched properties with _visited so we + // can detect properties that weren't touched (because they were + // removed from the style). + // Also keep track of properties that didn't exist in the current set + // of properties. + let brandNewProps = []; + for (let newProp of newTextProps) { + if (!this._updateTextProperty(newProp)) { + brandNewProps.push(newProp); + } + } + + // Refresh editors and disabled state for all the properties that + // were updated. + for (let prop of this.textProps) { + // Properties that weren't touched during the update + // process must no longer exist on the node. Mark them disabled. + if (!prop._visited) { + prop.enabled = false; + prop.updateEditor(); + } else { + delete prop._visited; + } + } + + // Add brand new properties. + this.textProps = this.textProps.concat(brandNewProps); + + // Refresh the editor if one already exists. + if (this.editor) { + this.editor.populate(); + } + }, + + /** + * Update the current TextProperties that match a given property + * 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 + * 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) { + continue; + } + + // Mark this property visited. + prop._visited = true; + + // Start at rank 1 for matching name. + let rank = 1; + + // Value and Priority matches add 2 to the rank. + // Being enabled adds 1. This ranks better matches higher, + // with priority breaking ties. + if (prop.value === newProp.value) { + rank += 2; + if (prop.priority === newProp.priority) { + rank += 2; + } + } + + if (prop.enabled) { + rank += 1; + } + + if (rank > match.rank) { + if (match.prop) { + // We outrank a previous match, disable it. + match.prop.enabled = false; + match.prop.updateEditor(); + } + match.rank = rank; + match.prop = prop; + } else if (rank) { + // A previous match outranks us, disable ourself. + prop.enabled = false; + prop.updateEditor(); + } + } + + // If we found a match, update its value with the new text property + // value. + if (match.prop) { + match.prop.set(newProp); + return true; + } + + return false; + }, + + /** + * Jump between editable properties in the UI. If the focus direction is + * forward, begin editing the next property name if available or focus the + * new property editor otherwise. If the focus direction is backward, + * begin editing the previous property value or focus the selector editor if + * this is the first element in the property list. + * + * @param {TextProperty} textProperty + * 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 === Services.focus.MOVEFOCUS_FORWARD) { + for (++index; index < this.textProps.length; ++index) { + if (!this.textProps[index].invisible) { + break; + } + } + if (index === this.textProps.length) { + textProperty.rule.editor.closeBrace.click(); + } else { + this.textProps[index].editor.nameSpan.click(); + } + } else if (direction === Services.focus.MOVEFOCUS_BACKWARD) { + for (--index; index >= 0; --index) { + if (!this.textProps[index].invisible) { + break; + } + } + if (index < 0) { + textProperty.editor.ruleEditor.selectorText.click(); + } else { + this.textProps[index].editor.valueSpan.click(); + } + } + }, + + /** + * Return a string representation of the rule. + */ + stringifyRule: function () { + let selectorText = this.selectorText; + let cssText = ""; + let terminator = Services.appinfo.OS === "WINNT" ? "\r\n" : "\n"; + + for (let textProp of this.textProps) { + 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; + } +}; + +exports.Rule = Rule; diff --git a/devtools/client/inspector/rules/models/text-property.js b/devtools/client/inspector/rules/models/text-property.js new file mode 100644 index 000000000..3bbe6e91d --- /dev/null +++ b/devtools/client/inspector/rules/models/text-property.js @@ -0,0 +1,215 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {escapeCSSComment} = require("devtools/shared/css/parsing-utils"); +const {getCssProperties} = require("devtools/shared/fronts/css-properties"); + +/** + * TextProperty is responsible for the following: + * 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. + * + * @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, enabled = true, + invisible = false) { + this.rule = rule; + this.name = name; + this.value = value; + this.priority = priority; + this.enabled = !!enabled; + this.invisible = invisible; + this.panelDoc = this.rule.elementStyle.ruleView.inspector.panelDoc; + + const toolbox = this.rule.elementStyle.ruleView.inspector.toolbox; + this.cssProperties = getCssProperties(toolbox); + + this.updateComputed(); +} + +TextProperty.prototype = { + /** + * Update the editor associated with this text property, + * if any. + */ + updateEditor: function () { + if (this.editor) { + this.editor.update(); + } + }, + + /** + * Update the list of computed properties for this text property. + */ + updateComputed: function () { + if (!this.name) { + return; + } + + // This is a bit funky. To get the list of computed properties + // for this text property, we'll set the property on a dummy element + // and see what the computed style looks like. + let dummyElement = this.rule.elementStyle.ruleView.dummyElement; + let dummyStyle = dummyElement.style; + dummyStyle.cssText = ""; + dummyStyle.setProperty(this.name, this.value, this.priority); + + this.computed = []; + + // Manually get all the properties that are set when setting a value on + // this.name and check the computed style on dummyElement for each one. + // If we just read dummyStyle, it would skip properties when value === "". + let subProps = this.cssProperties.getSubproperties(this.name); + + for (let prop of subProps) { + this.computed.push({ + textProp: this, + name: prop, + value: dummyStyle.getPropertyValue(prop), + priority: dummyStyle.getPropertyPriority(prop), + }); + } + }, + + /** + * Set all the values from another TextProperty instance into + * this TextProperty instance. + * + * @param {TextProperty} prop + * The other TextProperty instance. + */ + set: function (prop) { + let changed = false; + for (let item of ["name", "value", "priority", "enabled"]) { + if (this[item] !== prop[item]) { + this[item] = prop[item]; + changed = true; + } + } + + if (changed) { + this.updateEditor(); + } + }, + + setValue: function (value, priority, force = false) { + let store = this.rule.elementStyle.store; + + 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); + } + + this.rule.setPropertyName(this, name); + this.updateEditor(); + }, + + setEnabled: function (value) { + this.rule.setPropertyEnabled(this, value); + this.updateEditor(); + }, + + remove: function () { + this.rule.removeProperty(this); + }, + + /** + * Return a string representation of the rule property. + */ + stringifyProperty: function () { + // Get the displayed property value + let declaration = this.name + ": " + this.editor.valueSpan.textContent + + ";"; + + // 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 () { + return this.cssProperties.isKnown(this.name); + }, + + /** + * Validate this property. Does it make sense for this value to be assigned + * to this property name? + * + * @return {Boolean} true if the property value is valid, false otherwise. + */ + isValid: function () { + // Starting with FF49, StyleRuleActors provide a list of parsed + // declarations, with data about their validity, but if we don't have this, + // compute validity locally (which might not be correct, but better than + // nothing). + if (!this.rule.domRule.declarations) { + return this.cssProperties.isValidOnClient(this.name, this.value, this.panelDoc); + } + + let selfIndex = this.rule.textProps.indexOf(this); + + // When adding a new property in the rule-view, the TextProperty object is + // created right away before the rule gets updated on the server, so we're + // not going to find the corresponding declaration object yet. Default to + // true. + if (!this.rule.domRule.declarations[selfIndex]) { + return true; + } + + return this.rule.domRule.declarations[selfIndex].isValid; + } +}; + +exports.TextProperty = TextProperty; diff --git a/devtools/client/inspector/rules/moz.build b/devtools/client/inspector/rules/moz.build new file mode 100644 index 000000000..e826c1414 --- /dev/null +++ b/devtools/client/inspector/rules/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DIRS += [ + 'models', + 'views', +] + +DevToolsModules( + 'rules.js', +) + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/devtools/client/inspector/rules/rules.js b/devtools/client/inspector/rules/rules.js new file mode 100644 index 000000000..8c5ec7617 --- /dev/null +++ b/devtools/client/inspector/rules/rules.js @@ -0,0 +1,1673 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const promise = require("promise"); +const Services = require("Services"); +const {Task} = require("devtools/shared/task"); +const {Tools} = require("devtools/client/definitions"); +const {l10n} = require("devtools/shared/inspector/css-logic"); +const {ELEMENT_STYLE} = require("devtools/shared/specs/styles"); +const {OutputParser} = require("devtools/client/shared/output-parser"); +const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils"); +const {ElementStyle} = require("devtools/client/inspector/rules/models/element-style"); +const {Rule} = require("devtools/client/inspector/rules/models/rule"); +const {RuleEditor} = require("devtools/client/inspector/rules/views/rule-editor"); +const {gDevTools} = require("devtools/client/framework/devtools"); +const {getCssProperties} = require("devtools/shared/fronts/css-properties"); +const HighlightersOverlay = require("devtools/client/inspector/shared/highlighters-overlay"); +const { + VIEW_NODE_SELECTOR_TYPE, + VIEW_NODE_PROPERTY_TYPE, + VIEW_NODE_VALUE_TYPE, + VIEW_NODE_IMAGE_URL_TYPE, + VIEW_NODE_LOCATION_TYPE, +} = require("devtools/client/inspector/shared/node-types"); +const StyleInspectorMenu = require("devtools/client/inspector/shared/style-inspector-menu"); +const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay"); +const {createChild, promiseWarn, throttle} = require("devtools/client/inspector/shared/utils"); +const EventEmitter = require("devtools/shared/event-emitter"); +const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts"); +const clipboardHelper = require("devtools/shared/platform/clipboard"); +const {AutocompletePopup} = require("devtools/client/shared/autocomplete-popup"); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles"; +const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit"; +const PREF_ENABLE_MDN_DOCS_TOOLTIP = + "devtools.inspector.mdnDocsTooltip.enabled"; +const FILTER_CHANGED_TIMEOUT = 150; + +// This is used to parse user input when filtering. +const FILTER_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*;?$/; +// This is used to parse the filter search value to see if the filter +// should be strict or not +const FILTER_STRICT_RE = /\s*`(.*?)`\s*$/; + +/** + * Our model looks like this: + * + * 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 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. + * + * View hierarchy mostly follows the model hierarchy. + * + * CssRuleView: + * Owns an ElementStyle and creates a list of RuleEditors for its + * Rules. + * RuleEditor: + * Owns a Rule object and creates a list of TextPropertyEditors + * for its TextProperties. + * Manages creation of new text properties. + * TextPropertyEditor: + * Owns a TextProperty object. + * Manages changes to the TextProperty. + * Can be expanded to display computed properties. + * Can mark a property disabled or enabled. + */ + +/** + * CssRuleView is a view of the style rules and declarations that + * apply to a given element. After construction, the 'element' + * property will be available with the user interface. + * + * @param {Inspector} inspector + * Inspector toolbox panel + * @param {Document} document + * The document that will contain the rule view. + * @param {Object} store + * The CSS rule view can use this object to store metadata + * that might outlast the rule view, particularly the current + * set of disabled properties. + * @param {PageStyleFront} pageStyle + * The PageStyleFront for communicating with the remote server. + */ +function CssRuleView(inspector, document, store, pageStyle) { + this.inspector = inspector; + this.styleDocument = document; + this.styleWindow = this.styleDocument.defaultView; + this.store = store || {}; + this.pageStyle = pageStyle; + + // Allow tests to override throttling behavior, as this can cause intermittents. + this.throttle = throttle; + + this.cssProperties = getCssProperties(inspector.toolbox); + + this._outputParser = new OutputParser(document, this.cssProperties); + + this._onAddRule = this._onAddRule.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + this._onCopy = this._onCopy.bind(this); + this._onFilterStyles = this._onFilterStyles.bind(this); + this._onClearSearch = this._onClearSearch.bind(this); + this._onTogglePseudoClassPanel = this._onTogglePseudoClassPanel.bind(this); + this._onTogglePseudoClass = this._onTogglePseudoClass.bind(this); + + let doc = this.styleDocument; + this.element = doc.getElementById("ruleview-container-focusable"); + this.addRuleButton = doc.getElementById("ruleview-add-rule-button"); + this.searchField = doc.getElementById("ruleview-searchbox"); + this.searchClearButton = doc.getElementById("ruleview-searchinput-clear"); + this.pseudoClassPanel = doc.getElementById("pseudo-class-panel"); + this.pseudoClassToggle = doc.getElementById("pseudo-class-panel-toggle"); + this.hoverCheckbox = doc.getElementById("pseudo-hover-toggle"); + this.activeCheckbox = doc.getElementById("pseudo-active-toggle"); + this.focusCheckbox = doc.getElementById("pseudo-focus-toggle"); + + this.searchClearButton.hidden = true; + + this.shortcuts = new KeyShortcuts({ window: this.styleWindow }); + this._onShortcut = this._onShortcut.bind(this); + this.shortcuts.on("Escape", this._onShortcut); + this.shortcuts.on("Return", this._onShortcut); + this.shortcuts.on("Space", this._onShortcut); + this.shortcuts.on("CmdOrCtrl+F", this._onShortcut); + this.element.addEventListener("copy", this._onCopy); + this.element.addEventListener("contextmenu", this._onContextMenu); + this.addRuleButton.addEventListener("click", this._onAddRule); + this.searchField.addEventListener("input", this._onFilterStyles); + this.searchField.addEventListener("contextmenu", this.inspector.onTextBoxContextMenu); + this.searchClearButton.addEventListener("click", this._onClearSearch); + this.pseudoClassToggle.addEventListener("click", + this._onTogglePseudoClassPanel); + this.hoverCheckbox.addEventListener("click", this._onTogglePseudoClass); + this.activeCheckbox.addEventListener("click", this._onTogglePseudoClass); + this.focusCheckbox.addEventListener("click", this._onTogglePseudoClass); + + this._handlePrefChange = this._handlePrefChange.bind(this); + this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this); + + this._prefObserver = new PrefObserver("devtools."); + this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged); + this._prefObserver.on(PREF_UA_STYLES, this._handlePrefChange); + this._prefObserver.on(PREF_DEFAULT_COLOR_UNIT, this._handlePrefChange); + this._prefObserver.on(PREF_ENABLE_MDN_DOCS_TOOLTIP, this._handlePrefChange); + + this.showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES); + this.enableMdnDocsTooltip = + Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP); + + // The popup will be attached to the toolbox document. + this.popup = new AutocompletePopup(inspector._toolbox.doc, { + autoSelect: true, + theme: "auto" + }); + + this._showEmpty(); + + this._contextmenu = new StyleInspectorMenu(this, { isRuleView: true }); + + // Add the tooltips and highlighters to the view + this.tooltips = new TooltipsOverlay(this); + this.tooltips.addToView(); + this.highlighters = new HighlightersOverlay(this); + this.highlighters.addToView(); + + EventEmitter.decorate(this); +} + +CssRuleView.prototype = { + // The element that we're inspecting. + _viewedElement: null, + + // Used for cancelling timeouts in the style filter. + _filterChangedTimeout: null, + + // Empty, unconnected element of the same type as this node, used + // to figure out how shorthand properties will be parsed. + _dummyElement: null, + + // Get the dummy elemenet. + get dummyElement() { + return this._dummyElement; + }, + + // Get the filter search value. + get searchValue() { + return this.searchField.value.toLowerCase(); + }, + + /** + * Get an instance of SelectorHighlighter (used to highlight nodes that match + * selectors in the rule-view). A new instance is only created the first time + * this function is called. The same instance will then be returned. + * + * @return {Promise} Resolves to the instance of the highlighter. + */ + getSelectorHighlighter: Task.async(function* () { + let utils = this.inspector.toolbox.highlighterUtils; + if (!utils.supportsCustomHighlighters()) { + return null; + } + + if (this.selectorHighlighter) { + return this.selectorHighlighter; + } + + try { + let h = yield utils.getHighlighterByType("SelectorHighlighter"); + this.selectorHighlighter = h; + return h; + } catch (e) { + // The SelectorHighlighter type could not be created in the + // current target. It could be an older server, or a XUL page. + return null; + } + }), + + /** + * Highlight/unhighlight all the nodes that match a given set of selectors + * inside the document of the current selected node. + * Only one selector can be highlighted at a time, so calling the method a + * second time with a different selector will first unhighlight the previously + * highlighted nodes. + * Calling the method a second time with the same selector will just + * unhighlight the highlighted nodes. + * + * @param {DOMNode} selectorIcon + * The icon that was clicked to toggle the selector. The + * class 'highlighted' will be added when the selector is + * highlighted. + * @param {String} selector + * The selector used to find nodes in the page. + */ + toggleSelectorHighlighter: function (selectorIcon, selector) { + if (this.lastSelectorIcon) { + this.lastSelectorIcon.classList.remove("highlighted"); + } + selectorIcon.classList.remove("highlighted"); + + this.unhighlightSelector().then(() => { + if (selector !== this.highlighters.selectorHighlighterShown) { + this.highlighters.selectorHighlighterShown = selector; + selectorIcon.classList.add("highlighted"); + this.lastSelectorIcon = selectorIcon; + this.highlightSelector(selector).then(() => { + this.emit("ruleview-selectorhighlighter-toggled", true); + }, e => console.error(e)); + } else { + this.highlighters.selectorHighlighterShown = null; + this.emit("ruleview-selectorhighlighter-toggled", false); + } + }, e => console.error(e)); + }, + + highlightSelector: Task.async(function* (selector) { + let node = this.inspector.selection.nodeFront; + + let highlighter = yield this.getSelectorHighlighter(); + if (!highlighter) { + return; + } + + yield highlighter.show(node, { + hideInfoBar: true, + hideGuides: true, + selector + }); + }), + + unhighlightSelector: Task.async(function* () { + let highlighter = yield this.getSelectorHighlighter(); + if (!highlighter) { + return; + } + + yield highlighter.hide(); + }), + + /** + * Get the type of a given node in the rule-view + * + * @param {DOMNode} node + * The node which we want information about + * @return {Object} The type information object contains the following props: + * - type {String} One of the VIEW_NODE_XXX_TYPE const in + * client/inspector/shared/node-types + * - value {Object} Depends on the type of the node + * returns null of the node isn't anything we care about + */ + getNodeInfo: function (node) { + if (!node) { + return null; + } + + let type, value; + let classes = node.classList; + let prop = getParentTextProperty(node); + + if (classes.contains("ruleview-propertyname") && prop) { + type = VIEW_NODE_PROPERTY_TYPE; + value = { + property: node.textContent, + value: getPropertyNameAndValue(node).value, + enabled: prop.enabled, + overridden: prop.overridden, + pseudoElement: prop.rule.pseudoElement, + sheetHref: prop.rule.domRule.href, + textProperty: prop + }; + } else if (classes.contains("ruleview-propertyvalue") && prop) { + type = VIEW_NODE_VALUE_TYPE; + value = { + property: getPropertyNameAndValue(node).name, + value: node.textContent, + enabled: prop.enabled, + overridden: prop.overridden, + pseudoElement: prop.rule.pseudoElement, + sheetHref: prop.rule.domRule.href, + textProperty: prop + }; + } else if (classes.contains("theme-link") && + !classes.contains("ruleview-rule-source") && prop) { + type = VIEW_NODE_IMAGE_URL_TYPE; + value = { + property: getPropertyNameAndValue(node).name, + value: node.parentNode.textContent, + url: node.href, + enabled: prop.enabled, + overridden: prop.overridden, + pseudoElement: prop.rule.pseudoElement, + sheetHref: prop.rule.domRule.href, + textProperty: prop + }; + } else if (classes.contains("ruleview-selector-unmatched") || + classes.contains("ruleview-selector-matched") || + classes.contains("ruleview-selectorcontainer") || + classes.contains("ruleview-selector") || + classes.contains("ruleview-selector-attribute") || + classes.contains("ruleview-selector-pseudo-class") || + classes.contains("ruleview-selector-pseudo-class-lock")) { + type = VIEW_NODE_SELECTOR_TYPE; + value = this._getRuleEditorForNode(node).selectorText.textContent; + } else if (classes.contains("ruleview-rule-source") || + classes.contains("ruleview-rule-source-label")) { + type = VIEW_NODE_LOCATION_TYPE; + let rule = this._getRuleEditorForNode(node).rule; + value = (rule.sheet && rule.sheet.href) ? rule.sheet.href : rule.title; + } else { + return null; + } + + return {type, value}; + }, + + /** + * Retrieve the RuleEditor instance that should be stored on + * the offset parent of the node + */ + _getRuleEditorForNode: function (node) { + if (!node.offsetParent) { + // some nodes don't have an offsetParent, but their parentNode does + node = node.parentNode; + } + return node.offsetParent._ruleEditor; + }, + + /** + * Context menu handler. + */ + _onContextMenu: function (event) { + this._contextmenu.show(event); + }, + + /** + * Callback for copy event. Copy the selected text. + * + * @param {Event} event + * copy event object. + */ + _onCopy: function (event) { + if (event) { + this.copySelection(event.target); + event.preventDefault(); + } + }, + + /** + * Copy the current selection. The current target is necessary + * if the selection is inside an input or a textarea + * + * @param {DOMNode} target + * DOMNode target of the copy action + */ + copySelection: function (target) { + try { + let text = ""; + + let nodeName = target && target.nodeName; + if (nodeName === "input" || nodeName == "textarea") { + let start = Math.min(target.selectionStart, target.selectionEnd); + let end = Math.max(target.selectionStart, target.selectionEnd); + let count = end - start; + text = target.value.substr(start, count); + } else { + text = this.styleWindow.getSelection().toString(); + + // Remove any double newlines. + text = text.replace(/(\r?\n)\r?\n/g, "$1"); + } + + clipboardHelper.copyString(text); + } catch (e) { + console.error(e); + } + }, + + /** + * A helper for _onAddRule that handles the case where the actor + * does not support as-authored styles. + */ + _onAddNewRuleNonAuthored: function () { + let elementStyle = this._elementStyle; + let element = elementStyle.element; + let rules = elementStyle.rules; + let pseudoClasses = element.pseudoClassLocks; + + 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) { + this.element.appendChild(editor.element); + } else { + for (let rule of rules) { + if (rule.domRule.type === ELEMENT_STYLE) { + let referenceElement = rule.editor.element.nextSibling; + this.element.insertBefore(editor.element, referenceElement); + break; + } + } + } + + // 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.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; + }, + + setPageStyle: function (pageStyle) { + this.pageStyle = pageStyle; + }, + + /** + * Return {Boolean} true if the rule view currently has an input + * editor visible. + */ + get isEditing() { + return this.tooltips.isEditing || + this.element.querySelectorAll(".styleinspector-propertyeditor") + .length > 0; + }, + + _handlePrefChange: function (pref) { + if (pref === PREF_UA_STYLES) { + this.showUserAgentStyles = Services.prefs.getBoolPref(pref); + } + + // Reselect the currently selected element + let refreshOnPrefs = [PREF_UA_STYLES, PREF_DEFAULT_COLOR_UNIT]; + if (refreshOnPrefs.indexOf(pref) > -1) { + this.selectElement(this._viewedElement, true); + } + }, + + /** + * Update source links when pref for showing original sources changes + */ + _onSourcePrefChanged: function () { + if (this._elementStyle && this._elementStyle.rules) { + for (let rule of this._elementStyle.rules) { + if (rule.editor) { + rule.editor.updateSourceLink(); + } + } + this.inspector.emit("rule-view-sourcelinks-updated"); + } + }, + + /** + * Set the filter style search value. + * @param {String} value + * The search value. + */ + setFilterStyles: function (value = "") { + this.searchField.value = value; + this.searchField.focus(); + this._onFilterStyles(); + }, + + /** + * Called when the user enters a search term in the filter style search box. + */ + _onFilterStyles: function () { + if (this._filterChangedTimeout) { + clearTimeout(this._filterChangedTimeout); + } + + let filterTimeout = (this.searchValue.length > 0) ? + FILTER_CHANGED_TIMEOUT : 0; + this.searchClearButton.hidden = this.searchValue.length === 0; + + this._filterChangedTimeout = setTimeout(() => { + if (this.searchField.value.length > 0) { + this.searchField.setAttribute("filled", true); + } else { + this.searchField.removeAttribute("filled"); + } + + this.searchData = { + searchPropertyMatch: FILTER_PROP_RE.exec(this.searchValue), + searchPropertyName: this.searchValue, + searchPropertyValue: this.searchValue, + strictSearchValue: "", + strictSearchPropertyName: false, + strictSearchPropertyValue: false, + strictSearchAllValues: false + }; + + if (this.searchData.searchPropertyMatch) { + // Parse search value as a single property line and extract the + // property name and value. If the parsed property name or value is + // contained in backquotes (`), extract the value within the backquotes + // and set the corresponding strict search for the property to true. + if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[1])) { + this.searchData.strictSearchPropertyName = true; + this.searchData.searchPropertyName = + FILTER_STRICT_RE.exec(this.searchData.searchPropertyMatch[1])[1]; + } else { + this.searchData.searchPropertyName = + this.searchData.searchPropertyMatch[1]; + } + + if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[2])) { + this.searchData.strictSearchPropertyValue = true; + this.searchData.searchPropertyValue = + FILTER_STRICT_RE.exec(this.searchData.searchPropertyMatch[2])[1]; + } else { + this.searchData.searchPropertyValue = + this.searchData.searchPropertyMatch[2]; + } + + // Strict search for stylesheets will match the property line regex. + // Extract the search value within the backquotes to be used + // in the strict search for stylesheets in _highlightStyleSheet. + if (FILTER_STRICT_RE.test(this.searchValue)) { + this.searchData.strictSearchValue = + FILTER_STRICT_RE.exec(this.searchValue)[1]; + } + } else if (FILTER_STRICT_RE.test(this.searchValue)) { + // If the search value does not correspond to a property line and + // is contained in backquotes, extract the search value within the + // backquotes and set the flag to perform a strict search for all + // the values (selector, stylesheet, property and computed values). + let searchValue = FILTER_STRICT_RE.exec(this.searchValue)[1]; + this.searchData.strictSearchAllValues = true; + this.searchData.searchPropertyName = searchValue; + this.searchData.searchPropertyValue = searchValue; + this.searchData.strictSearchValue = searchValue; + } + + this._clearHighlight(this.element); + this._clearRules(); + this._createEditors(); + + this.inspector.emit("ruleview-filtered"); + + this._filterChangeTimeout = null; + }, filterTimeout); + }, + + /** + * Called when the user clicks on the clear button in the filter style search + * box. Returns true if the search box is cleared and false otherwise. + */ + _onClearSearch: function () { + if (this.searchField.value) { + this.setFilterStyles(""); + return true; + } + + return false; + }, + + destroy: function () { + this.isDestroyed = true; + this.clear(); + + this._dummyElement = null; + + this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged); + this._prefObserver.off(PREF_UA_STYLES, this._handlePrefChange); + this._prefObserver.off(PREF_DEFAULT_COLOR_UNIT, this._handlePrefChange); + this._prefObserver.destroy(); + + this._outputParser = null; + + // Remove context menu + if (this._contextmenu) { + this._contextmenu.destroy(); + this._contextmenu = null; + } + + this.tooltips.destroy(); + this.highlighters.destroy(); + + // Remove bound listeners + this.shortcuts.destroy(); + this.element.removeEventListener("copy", this._onCopy); + this.element.removeEventListener("contextmenu", this._onContextMenu); + this.addRuleButton.removeEventListener("click", this._onAddRule); + this.searchField.removeEventListener("input", this._onFilterStyles); + this.searchField.removeEventListener("contextmenu", + this.inspector.onTextBoxContextMenu); + this.searchClearButton.removeEventListener("click", this._onClearSearch); + this.pseudoClassToggle.removeEventListener("click", + this._onTogglePseudoClassPanel); + this.hoverCheckbox.removeEventListener("click", this._onTogglePseudoClass); + this.activeCheckbox.removeEventListener("click", this._onTogglePseudoClass); + this.focusCheckbox.removeEventListener("click", this._onTogglePseudoClass); + + this.searchField = null; + this.searchClearButton = null; + this.pseudoClassPanel = null; + this.pseudoClassToggle = null; + this.hoverCheckbox = null; + this.activeCheckbox = null; + this.focusCheckbox = null; + + this.inspector = null; + this.styleDocument = null; + this.styleWindow = null; + + if (this.element.parentNode) { + this.element.parentNode.removeChild(this.element); + } + + if (this._elementStyle) { + this._elementStyle.destroy(); + } + + this.popup.destroy(); + }, + + /** + * Mark the view as selecting an element, disabling all interaction, and + * visually clearing the view after a few milliseconds to avoid confusion + * about which element's styles the rule view shows. + */ + _startSelectingElement: function () { + this.element.classList.add("non-interactive"); + }, + + /** + * Mark the view as no longer selecting an element, re-enabling interaction. + */ + _stopSelectingElement: function () { + this.element.classList.remove("non-interactive"); + }, + + /** + * Update the view with a new selected element. + * + * @param {NodeActor} element + * The node whose style rules we'll inspect. + * @param {Boolean} allowRefresh + * Update the view even if the element is the same as last time. + */ + selectElement: function (element, allowRefresh = false) { + let refresh = (this._viewedElement === element); + if (refresh && !allowRefresh) { + return promise.resolve(undefined); + } + + if (this.popup.isOpen) { + this.popup.hidePopup(); + } + + this.clear(false); + this._viewedElement = element; + + this.clearPseudoClassPanel(); + this.refreshAddRuleButtonState(); + + if (!this._viewedElement) { + this._stopSelectingElement(); + this._clearRules(); + this._showEmpty(); + this.refreshPseudoClassPanel(); + return promise.resolve(undefined); + } + + // To figure out how shorthand properties are interpreted by the + // engine, we will set properties on a dummy element and observe + // how their .style attribute reflects them as computed values. + let dummyElementPromise = promise.resolve(this.styleDocument).then(document => { + // ::before and ::after do not have a namespaceURI + let namespaceURI = this.element.namespaceURI || + document.documentElement.namespaceURI; + this._dummyElement = document.createElementNS(namespaceURI, + this.element.tagName); + }).then(null, promiseWarn); + + let elementStyle = new ElementStyle(element, this, this.store, + this.pageStyle, this.showUserAgentStyles); + this._elementStyle = elementStyle; + + this._startSelectingElement(); + + return dummyElementPromise.then(() => { + if (this._elementStyle === elementStyle) { + return this._populate(); + } + return undefined; + }).then(() => { + if (this._elementStyle === elementStyle) { + if (!refresh) { + this.element.scrollTop = 0; + } + this._stopSelectingElement(); + this._elementStyle.onChanged = () => { + this._changed(); + }; + } + }).then(null, e => { + if (this._elementStyle === elementStyle) { + this._stopSelectingElement(); + this._clearRules(); + } + console.error(e); + }); + }, + + /** + * Update the rules for the currently highlighted element. + */ + refreshPanel: function () { + // Ignore refreshes during editing or when no element is selected. + if (this.isEditing || !this._elementStyle) { + return promise.resolve(undefined); + } + + // Repopulate the element style once the current modifications are done. + let promises = []; + for (let rule of this._elementStyle.rules) { + if (rule._applyingModifications) { + promises.push(rule._applyingModifications); + } + } + + return promise.all(promises).then(() => { + return this._populate(); + }); + }, + + /** + * Clear the pseudo class options panel by removing the checked and disabled + * attributes for each checkbox. + */ + clearPseudoClassPanel: function () { + this.hoverCheckbox.checked = this.hoverCheckbox.disabled = false; + this.activeCheckbox.checked = this.activeCheckbox.disabled = false; + this.focusCheckbox.checked = this.focusCheckbox.disabled = false; + }, + + /** + * Update the pseudo class options for the currently highlighted element. + */ + refreshPseudoClassPanel: function () { + if (!this._elementStyle || !this.inspector.selection.isElementNode()) { + this.hoverCheckbox.disabled = true; + this.activeCheckbox.disabled = true; + this.focusCheckbox.disabled = true; + return; + } + + for (let pseudoClassLock of this._elementStyle.element.pseudoClassLocks) { + switch (pseudoClassLock) { + case ":hover": { + this.hoverCheckbox.checked = true; + break; + } + case ":active": { + this.activeCheckbox.checked = true; + break; + } + case ":focus": { + this.focusCheckbox.checked = true; + break; + } + } + } + }, + + _populate: function () { + let elementStyle = this._elementStyle; + return this._elementStyle.populate().then(() => { + if (this._elementStyle !== elementStyle || this.isDestroyed) { + return null; + } + + this._clearRules(); + let onEditorsReady = this._createEditors(); + this.refreshPseudoClassPanel(); + + // Notify anyone that cares that we refreshed. + return onEditorsReady.then(() => { + this.emit("ruleview-refreshed"); + }, e => console.error(e)); + }).then(null, promiseWarn); + }, + + /** + * Show the user that the rule view has no node selected. + */ + _showEmpty: function () { + if (this.styleDocument.getElementById("ruleview-no-results")) { + return; + } + + createChild(this.element, "div", { + id: "ruleview-no-results", + textContent: l10n("rule.empty") + }); + }, + + /** + * Clear the rules. + */ + _clearRules: function () { + this.element.innerHTML = ""; + }, + + /** + * Clear the rule view. + */ + clear: function (clearDom = true) { + this.lastSelectorIcon = null; + + if (clearDom) { + this._clearRules(); + } + this._viewedElement = null; + + if (this._elementStyle) { + this._elementStyle.destroy(); + this._elementStyle = null; + } + }, + + /** + * Called when the user has made changes to the ElementStyle. + * Emits an event that clients can listen to. + */ + _changed: function () { + this.emit("ruleview-changed"); + }, + + /** + * Text for header that shows above rules for this element + */ + get selectedElementLabel() { + if (this._selectedElementLabel) { + return this._selectedElementLabel; + } + this._selectedElementLabel = l10n("rule.selectedElement"); + return this._selectedElementLabel; + }, + + /** + * Text for header that shows above rules for pseudo elements + */ + get pseudoElementLabel() { + if (this._pseudoElementLabel) { + return this._pseudoElementLabel; + } + this._pseudoElementLabel = l10n("rule.pseudoElement"); + return this._pseudoElementLabel; + }, + + get showPseudoElements() { + if (this._showPseudoElements === undefined) { + this._showPseudoElements = + Services.prefs.getBoolPref("devtools.inspector.show_pseudo_elements"); + } + return this._showPseudoElements; + }, + + /** + * Creates an expandable container in the rule view + * + * @param {String} label + * 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 (label, isPseudo = false) { + let header = this.styleDocument.createElementNS(HTML_NS, "div"); + header.className = this._getRuleViewHeaderClassName(true); + header.textContent = label; + + let twisty = this.styleDocument.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.styleDocument.createElementNS(HTML_NS, "div"); + container.classList.add("ruleview-expandable-container"); + container.hidden = false; + this.element.appendChild(container); + + header.addEventListener("dblclick", () => { + this._toggleContainerVisibility(twisty, container, isPseudo, + !this.showPseudoElements); + }, false); + + twisty.addEventListener("click", () => { + this._toggleContainerVisibility(twisty, container, isPseudo, + !this.showPseudoElements); + }, false); + + if (isPseudo) { + this._toggleContainerVisibility(twisty, container, isPseudo, + this.showPseudoElements); + } + + return container; + }, + + /** + * Toggle the visibility of an expandable container + * + * @param {DOMNode} twisty + * Clickable toggle DOM Node + * @param {DOMNode} container + * Expandable container DOM Node + * @param {Boolean} isPseudo + * Whether or not the container will hold pseudo element rules + * @param {Boolean} showPseudo + * Whether or not pseudo element rules should be displayed + */ + _toggleContainerVisibility: function (twisty, container, isPseudo, + showPseudo) { + let isOpen = twisty.getAttribute("open"); + + if (isPseudo) { + this._showPseudoElements = !!showPseudo; + + Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements", + this.showPseudoElements); + + container.hidden = !this.showPseudoElements; + isOpen = !this.showPseudoElements; + } else { + container.hidden = !container.hidden; + } + + if (isOpen) { + twisty.removeAttribute("open"); + } else { + twisty.setAttribute("open", "true"); + } + }, + + _getRuleViewHeaderClassName: function (isPseudo) { + let baseClassName = "theme-gutter ruleview-header"; + return isPseudo ? baseClassName + " ruleview-expandable-header" : + baseClassName; + }, + + /** + * Creates editor UI for each of the rules in _elementStyle. + */ + _createEditors: function () { + // 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 seenSearchTerm = false; + let container = null; + + if (!this._elementStyle.rules) { + return promise.resolve(); + } + + let editorReadyPromises = []; + for (let rule of this._elementStyle.rules) { + if (rule.domRule.system) { + continue; + } + + // Initialize rule editor + if (!rule.editor) { + rule.editor = new RuleEditor(this, rule); + editorReadyPromises.push(rule.editor.once("source-link-updated")); + } + + // Filter the rules and highlight any matches if there is a search input + if (this.searchValue && this.searchData) { + if (this.highlightRule(rule)) { + seenSearchTerm = true; + } else if (rule.domRule.type !== ELEMENT_STYLE) { + continue; + } + } + + // Only print header for this element if there are pseudo elements + if (seenPseudoElement && !seenNormalElement && !rule.pseudoElement) { + seenNormalElement = true; + let div = this.styleDocument.createElementNS(HTML_NS, "div"); + div.className = this._getRuleViewHeaderClassName(); + div.textContent = this.selectedElementLabel; + this.element.appendChild(div); + } + + let inheritedSource = rule.inheritedSource; + if (inheritedSource && inheritedSource !== lastInheritedSource) { + let div = this.styleDocument.createElementNS(HTML_NS, "div"); + div.className = this._getRuleViewHeaderClassName(); + div.textContent = inheritedSource; + lastInheritedSource = inheritedSource; + this.element.appendChild(div); + } + + if (!seenPseudoElement && rule.pseudoElement) { + seenPseudoElement = true; + container = this.createExpandableContainer(this.pseudoElementLabel, + true); + } + + let keyframes = rule.keyframes; + if (keyframes && keyframes !== lastKeyframes) { + lastKeyframes = keyframes; + container = this.createExpandableContainer(rule.keyframesName); + } + + if (container && (rule.pseudoElement || keyframes)) { + container.appendChild(rule.editor.element); + } else { + this.element.appendChild(rule.editor.element); + } + } + + if (this.searchValue && !seenSearchTerm) { + this.searchField.classList.add("devtools-style-searchbox-no-match"); + } else { + this.searchField.classList.remove("devtools-style-searchbox-no-match"); + } + + return promise.all(editorReadyPromises); + }, + + /** + * Highlight rules that matches the filter search value and returns a + * boolean indicating whether or not rules were highlighted. + * + * @param {Rule} rule + * The rule object we're highlighting if its rule selectors or + * property values match the search value. + * @return {Boolean} true if the rule was highlighted, false otherwise. + */ + 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 (!textProp.invisible && this._highlightProperty(textProp.editor)) { + isHighlighted = true; + } + } + + return isHighlighted; + }, + + /** + * Highlights the rule selector that matches the filter search value and + * returns a boolean indicating whether or not the selector was highlighted. + * + * @param {Rule} rule + * The Rule object. + * @return {Boolean} true if the rule selector was highlighted, + * false otherwise. + */ + _highlightRuleSelector: function (rule) { + let isSelectorHighlighted = false; + + let selectorNodes = [...rule.editor.selectorText.childNodes]; + if (rule.domRule.type === CSSRule.KEYFRAME_RULE) { + selectorNodes = [rule.editor.selectorText]; + } else if (rule.domRule.type === ELEMENT_STYLE) { + selectorNodes = []; + } + + // Highlight search matches in the rule selectors + for (let selectorNode of selectorNodes) { + let selector = selectorNode.textContent.toLowerCase(); + if ((this.searchData.strictSearchAllValues && + selector === this.searchData.strictSearchValue) || + (!this.searchData.strictSearchAllValues && + selector.includes(this.searchValue))) { + selectorNode.classList.add("ruleview-highlight"); + isSelectorHighlighted = true; + } + } + + return isSelectorHighlighted; + }, + + /** + * Highlights the stylesheet source that matches the filter search value and + * returns a boolean indicating whether or not the stylesheet source was + * highlighted. + * + * @return {Boolean} true if the stylesheet source was highlighted, false + * otherwise. + */ + _highlightStyleSheet: function (rule) { + let styleSheetSource = rule.title.toLowerCase(); + let isStyleSheetHighlighted = this.searchData.strictSearchValue ? + styleSheetSource === this.searchData.strictSearchValue : + styleSheetSource.includes(this.searchValue); + + if (isStyleSheetHighlighted) { + rule.editor.source.classList.add("ruleview-highlight"); + } + + return isStyleSheetHighlighted; + }, + + /** + * Highlights the rule properties and computed properties that match the + * filter search value and returns a boolean indicating whether or not the + * property or computed property was highlighted. + * + * @param {TextPropertyEditor} editor + * The rule property TextPropertyEditor object. + * @return {Boolean} true if the property or computed property was + * highlighted, false otherwise. + */ + _highlightProperty: function (editor) { + let isPropertyHighlighted = this._highlightRuleProperty(editor); + let isComputedHighlighted = this._highlightComputedProperty(editor); + + // Expand the computed list if a computed property is highlighted and the + // property rule is not highlighted + if (!isPropertyHighlighted && isComputedHighlighted && + !editor.computed.hasAttribute("user-open")) { + editor.expandForFilter(); + } + + return isPropertyHighlighted || isComputedHighlighted; + }, + + /** + * Called when TextPropertyEditor is updated and updates the rule property + * highlight. + * + * @param {TextPropertyEditor} editor + * The rule property TextPropertyEditor object. + */ + _updatePropertyHighlight: function (editor) { + if (!this.searchValue || !this.searchData) { + return; + } + + this._clearHighlight(editor.element); + + if (this._highlightProperty(editor)) { + this.searchField.classList.remove("devtools-style-searchbox-no-match"); + } + }, + + /** + * Highlights the rule property that matches the filter search value + * and returns a boolean indicating whether or not the property was + * highlighted. + * + * @param {TextPropertyEditor} editor + * The rule property TextPropertyEditor object. + * @return {Boolean} true if the rule property was highlighted, + * false otherwise. + */ + _highlightRuleProperty: function (editor) { + // Get the actual property value displayed in the rule view + let propertyName = editor.prop.name.toLowerCase(); + let propertyValue = editor.valueSpan.textContent.toLowerCase(); + + return this._highlightMatches(editor.container, propertyName, + propertyValue); + }, + + /** + * Highlights the computed property that matches the filter search value and + * returns a boolean indicating whether or not the computed property was + * highlighted. + * + * @param {TextPropertyEditor} editor + * The rule property TextPropertyEditor object. + * @return {Boolean} true if the computed property was highlighted, false + * otherwise. + */ + _highlightComputedProperty: function (editor) { + let isComputedHighlighted = false; + + // Highlight search matches in the computed list of properties + editor._populateComputed(); + for (let computed of editor.prop.computed) { + if (computed.element) { + // Get the actual property value displayed in the computed list + let computedName = computed.name.toLowerCase(); + let computedValue = computed.parsedValue.toLowerCase(); + + isComputedHighlighted = this._highlightMatches(computed.element, + computedName, computedValue) ? true : isComputedHighlighted; + } + } + + return isComputedHighlighted; + }, + + /** + * Helper function for highlightRules that carries out highlighting the given + * element if the search terms match the property, and returns a boolean + * indicating whether or not the search terms match. + * + * @param {DOMNode} element + * The node to highlight if search terms match + * @param {String} propertyName + * The property name of a rule + * @param {String} propertyValue + * The property value of a rule + * @return {Boolean} true if the given search terms match the property, false + * otherwise. + */ + _highlightMatches: function (element, propertyName, propertyValue) { + let { + searchPropertyName, + searchPropertyValue, + searchPropertyMatch, + strictSearchPropertyName, + strictSearchPropertyValue, + strictSearchAllValues, + } = this.searchData; + let matches = false; + + // If the inputted search value matches a property line like + // `font-family: arial`, then check to make sure the name and value match. + // Otherwise, just compare the inputted search string directly against the + // name and value of the rule property. + let hasNameAndValue = searchPropertyMatch && + searchPropertyName && + searchPropertyValue; + let isMatch = (value, query, isStrict) => { + return isStrict ? value === query : query && value.includes(query); + }; + + if (hasNameAndValue) { + matches = + isMatch(propertyName, searchPropertyName, strictSearchPropertyName) && + isMatch(propertyValue, searchPropertyValue, strictSearchPropertyValue); + } else { + matches = + isMatch(propertyName, searchPropertyName, + strictSearchPropertyName || strictSearchAllValues) || + isMatch(propertyValue, searchPropertyValue, + strictSearchPropertyValue || strictSearchAllValues); + } + + if (matches) { + element.classList.add("ruleview-highlight"); + } + + return matches; + }, + + /** + * Clear all search filter highlights in the panel, and close the computed + * list if toggled opened + */ + _clearHighlight: function (element) { + for (let el of element.querySelectorAll(".ruleview-highlight")) { + el.classList.remove("ruleview-highlight"); + } + + for (let computed of element.querySelectorAll( + ".ruleview-computedlist[filter-open]")) { + computed.parentNode._textPropertyEditor.collapseForFilter(); + } + }, + + /** + * Called when the pseudo class panel button is clicked and toggles + * the display of the pseudo class panel. + */ + _onTogglePseudoClassPanel: function () { + if (this.pseudoClassPanel.hidden) { + this.pseudoClassToggle.setAttribute("checked", "true"); + this.hoverCheckbox.setAttribute("tabindex", "0"); + this.activeCheckbox.setAttribute("tabindex", "0"); + this.focusCheckbox.setAttribute("tabindex", "0"); + } else { + this.pseudoClassToggle.removeAttribute("checked"); + this.hoverCheckbox.setAttribute("tabindex", "-1"); + this.activeCheckbox.setAttribute("tabindex", "-1"); + this.focusCheckbox.setAttribute("tabindex", "-1"); + } + + this.pseudoClassPanel.hidden = !this.pseudoClassPanel.hidden; + }, + + /** + * Called when a pseudo class checkbox is clicked and toggles + * the pseudo class for the current selected element. + */ + _onTogglePseudoClass: function (event) { + let target = event.currentTarget; + this.inspector.togglePseudoClass(target.value); + }, + + /** + * Handle the keypress event in the rule view. + */ + _onShortcut: function (name, event) { + if (!event.target.closest("#sidebar-panel-ruleview")) { + return; + } + + if (name === "CmdOrCtrl+F") { + this.searchField.focus(); + event.preventDefault(); + } else if ((name === "Return" || name === "Space") && + this.element.classList.contains("non-interactive")) { + event.preventDefault(); + } else if (name === "Escape" && + event.target === this.searchField && + this._onClearSearch()) { + // Handle the search box's keypress event. If the escape key is pressed, + // clear the search box field. + event.preventDefault(); + event.stopPropagation(); + } + } + +}; + +/** + * Helper functions + */ + +/** + * Walk up the DOM from a given node until a parent property holder is found. + * For elements inside the computed property list, the non-computed parent + * property holder will be returned + * + * @param {DOMNode} node + * The node to start from + * @return {DOMNode} The parent property holder node, or null if not found + */ +function getParentTextPropertyHolder(node) { + while (true) { + if (!node || !node.classList) { + return null; + } + if (node.classList.contains("ruleview-property")) { + return node; + } + node = node.parentNode; + } +} + +/** + * For any given node, find the TextProperty it is in if any + * @param {DOMNode} node + * The node to start from + * @return {TextProperty} + */ +function getParentTextProperty(node) { + let parent = getParentTextPropertyHolder(node); + if (!parent) { + return null; + } + + let propValue = parent.querySelector(".ruleview-propertyvalue"); + if (!propValue) { + return null; + } + + return propValue.textProperty; +} + +/** + * Walker up the DOM from a given node until a parent property holder is found, + * and return the textContent for the name and value nodes. + * Stops at the first property found, so if node is inside the computed property + * list, the computed property will be returned + * + * @param {DOMNode} node + * The node to start from + * @return {Object} {name, value} + */ +function getPropertyNameAndValue(node) { + while (true) { + if (!node || !node.classList) { + return null; + } + // Check first for ruleview-computed since it's the deepest + if (node.classList.contains("ruleview-computed") || + node.classList.contains("ruleview-property")) { + return { + name: node.querySelector(".ruleview-propertyname").textContent, + value: node.querySelector(".ruleview-propertyvalue").textContent + }; + } + node = node.parentNode; + } +} + +function RuleViewTool(inspector, window) { + this.inspector = inspector; + this.document = window.document; + + this.view = new CssRuleView(this.inspector, this.document); + + this.clearUserProperties = this.clearUserProperties.bind(this); + this.refresh = this.refresh.bind(this); + this.onLinkClicked = this.onLinkClicked.bind(this); + this.onMutations = this.onMutations.bind(this); + this.onPanelSelected = this.onPanelSelected.bind(this); + this.onPropertyChanged = this.onPropertyChanged.bind(this); + this.onResized = this.onResized.bind(this); + this.onSelected = this.onSelected.bind(this); + this.onViewRefreshed = this.onViewRefreshed.bind(this); + + this.view.on("ruleview-changed", this.onPropertyChanged); + this.view.on("ruleview-refreshed", this.onViewRefreshed); + this.view.on("ruleview-linked-clicked", this.onLinkClicked); + + this.inspector.selection.on("detached-front", this.onSelected); + this.inspector.selection.on("new-node-front", this.onSelected); + 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.inspector.walker.on("mutations", this.onMutations); + this.inspector.walker.on("resize", this.onResized); + + this.onSelected(); +} + +RuleViewTool.prototype = { + isSidebarActive: function () { + if (!this.view) { + return false; + } + return this.inspector.sidebar.getCurrentTabID() == "ruleview"; + }, + + onSelected: function (event) { + // Ignore the event if the view has been destroyed, or if it's inactive. + // But only if the current selection isn't null. If it's been set to null, + // let the update go through as this is needed to empty the view on + // navigation. + if (!this.view) { + return; + } + + let isInactive = !this.isSidebarActive() && + this.inspector.selection.nodeFront; + if (isInactive) { + return; + } + + this.view.setPageStyle(this.inspector.pageStyle); + + if (!this.inspector.selection.isConnected() || + !this.inspector.selection.isElementNode()) { + this.view.selectElement(null); + return; + } + + if (!event || event == "new-node-front") { + let done = this.inspector.updating("rule-view"); + this.view.selectElement(this.inspector.selection.nodeFront) + .then(done, done); + } + }, + + refresh: function () { + if (this.isSidebarActive()) { + this.view.refreshPanel(); + } + }, + + clearUserProperties: function () { + if (this.view && this.view.store && this.view.store.userProperties) { + this.view.store.userProperties.clear(); + } + }, + + onPanelSelected: function () { + if (this.inspector.selection.nodeFront === this.view._viewedElement) { + this.refresh(); + } else { + this.onSelected(); + } + }, + + onLinkClicked: function (e, rule) { + let sheet = rule.parentStyleSheet; + + // Chrome stylesheets are not listed in the style editor, so show + // these sheets in the view source window instead. + if (!sheet || sheet.isSystem) { + let href = rule.nodeHref || rule.href; + let toolbox = gDevTools.getToolbox(this.inspector.target); + toolbox.viewSource(href, rule.line); + return; + } + + let location = promise.resolve(rule.location); + if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { + location = rule.getOriginalLocation(); + } + location.then(({ source, href, line, column }) => { + let target = this.inspector.target; + if (Tools.styleEditor.isTargetSupported(target)) { + gDevTools.showToolbox(target, "styleeditor").then(function (toolbox) { + let url = source || href; + toolbox.getCurrentPanel().selectStyleSheet(url, line, column); + }); + } + return; + }); + }, + + onPropertyChanged: function () { + this.inspector.markDirty(); + }, + + onViewRefreshed: function () { + this.inspector.emit("rule-view-refreshed"); + }, + + /** + * When markup mutations occur, if an attribute of the selected node changes, + * we need to refresh the view as that might change the node's styles. + */ + onMutations: function (mutations) { + for (let {type, target} of mutations) { + if (target === this.inspector.selection.nodeFront && + type === "attributes") { + this.refresh(); + break; + } + } + }, + + /** + * When the window gets resized, this may cause media-queries to match, and + * therefore, different styles may apply. + */ + onResized: function () { + this.refresh(); + }, + + destroy: function () { + this.inspector.walker.off("mutations", this.onMutations); + this.inspector.walker.off("resize", this.onResized); + this.inspector.selection.off("detached-front", this.onSelected); + 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; + } +}; + +exports.CssRuleView = CssRuleView; +exports.RuleViewTool = RuleViewTool; diff --git a/devtools/client/inspector/rules/test/.eslintrc.js b/devtools/client/inspector/rules/test/.eslintrc.js new file mode 100644 index 000000000..698ae9181 --- /dev/null +++ b/devtools/client/inspector/rules/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../../.eslintrc.mochitests.js" +}; diff --git a/devtools/client/inspector/rules/test/browser.ini b/devtools/client/inspector/rules/test/browser.ini new file mode 100644 index 000000000..2c11219fb --- /dev/null +++ b/devtools/client/inspector/rules/test/browser.ini @@ -0,0 +1,221 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + doc_author-sheet.html + doc_blob_stylesheet.html + doc_content_stylesheet.html + doc_content_stylesheet_imported.css + doc_content_stylesheet_imported2.css + doc_content_stylesheet_linked.css + doc_content_stylesheet_script.css + doc_copystyles.css + doc_copystyles.html + doc_cssom.html + doc_custom.html + doc_filter.html + doc_frame_script.js + doc_inline_sourcemap.html + doc_invalid_sourcemap.css + doc_invalid_sourcemap.html + doc_keyframeanimation.css + doc_keyframeanimation.html + doc_keyframeLineNumbers.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 + head.js + !/devtools/client/commandline/test/helpers.js + !/devtools/client/framework/test/shared-head.js + !/devtools/client/inspector/test/head.js + !/devtools/client/inspector/test/shared-head.js + !/devtools/client/shared/test/test-actor.js + !/devtools/client/shared/test/test-actor-registry.js + +[browser_rules_add-property-and-reselect.js] +[browser_rules_add-property-cancel_01.js] +[browser_rules_add-property-cancel_02.js] +[browser_rules_add-property-cancel_03.js] +[browser_rules_add-property-commented.js] +[browser_rules_add-property_01.js] +[browser_rules_add-property_02.js] +[browser_rules_add-property-svg.js] +[browser_rules_add-rule-and-property.js] +[browser_rules_add-rule-button-state.js] +[browser_rules_add-rule-edit-selector.js] +[browser_rules_add-rule-iframes.js] +[browser_rules_add-rule-namespace-elements.js] +[browser_rules_add-rule-pseudo-class.js] +[browser_rules_add-rule-then-property-edit-selector.js] +[browser_rules_add-rule-with-menu.js] +[browser_rules_add-rule.js] +[browser_rules_authored.js] +[browser_rules_authored_color.js] +[browser_rules_authored_override.js] +[browser_rules_blob_stylesheet.js] +[browser_rules_colorpicker-and-image-tooltip_01.js] +[browser_rules_colorpicker-and-image-tooltip_02.js] +[browser_rules_colorpicker-appears-on-swatch-click.js] +[browser_rules_colorpicker-commit-on-ENTER.js] +[browser_rules_colorpicker-edit-gradient.js] +[browser_rules_colorpicker-hides-on-tooltip.js] +[browser_rules_colorpicker-multiple-changes.js] +[browser_rules_colorpicker-release-outside-frame.js] +[browser_rules_colorpicker-revert-on-ESC.js] +[browser_rules_colorpicker-swatch-displayed.js] +[browser_rules_colorUnit.js] +[browser_rules_completion-existing-property_01.js] +[browser_rules_completion-existing-property_02.js] +[browser_rules_completion-new-property_01.js] +[browser_rules_completion-new-property_02.js] +[browser_rules_completion-new-property_03.js] +[browser_rules_completion-new-property_04.js] +[browser_rules_completion-new-property_multiline.js] +[browser_rules_computed-lists_01.js] +[browser_rules_computed-lists_02.js] +[browser_rules_completion-popup-hidden-after-navigation.js] +[browser_rules_content_01.js] +[browser_rules_content_02.js] +skip-if = e10s && debug # Bug 1250058 - Docshell leak on debug e10s +[browser_rules_context-menu-show-mdn-docs-01.js] +[browser_rules_context-menu-show-mdn-docs-02.js] +[browser_rules_context-menu-show-mdn-docs-03.js] +[browser_rules_copy_styles.js] +subsuite = clipboard +[browser_rules_cssom.js] +[browser_rules_cubicbezier-appears-on-swatch-click.js] +[browser_rules_cubicbezier-commit-on-ENTER.js] +[browser_rules_cubicbezier-revert-on-ESC.js] +[browser_rules_custom.js] +[browser_rules_cycle-angle.js] +[browser_rules_cycle-color.js] +[browser_rules_edit-display-grid-property.js] +[browser_rules_edit-property-cancel.js] +[browser_rules_edit-property-click.js] +[browser_rules_edit-property-commit.js] +[browser_rules_edit-property-computed.js] +[browser_rules_edit-property-increments.js] +[browser_rules_edit-property-order.js] +[browser_rules_edit-property-remove_01.js] +[browser_rules_edit-property-remove_02.js] +[browser_rules_edit-property-remove_03.js] +[browser_rules_edit-property_01.js] +[browser_rules_edit-property_02.js] +[browser_rules_edit-property_03.js] +[browser_rules_edit-property_04.js] +[browser_rules_edit-property_05.js] +[browser_rules_edit-property_06.js] +[browser_rules_edit-property_07.js] +[browser_rules_edit-property_08.js] +[browser_rules_edit-property_09.js] +[browser_rules_edit-selector-click.js] +[browser_rules_edit-selector-click-on-scrollbar.js] +skip-if = os == "mac" # Bug 1245996 : click on scrollbar not working on OSX +[browser_rules_edit-selector-commit.js] +[browser_rules_edit-selector_01.js] +[browser_rules_edit-selector_02.js] +[browser_rules_edit-selector_03.js] +[browser_rules_edit-selector_04.js] +[browser_rules_edit-selector_05.js] +[browser_rules_edit-selector_06.js] +[browser_rules_edit-selector_07.js] +[browser_rules_edit-selector_08.js] +[browser_rules_edit-selector_09.js] +[browser_rules_edit-selector_10.js] +[browser_rules_edit-selector_11.js] +[browser_rules_edit-value-after-name_01.js] +[browser_rules_edit-value-after-name_02.js] +[browser_rules_edit-value-after-name_03.js] +[browser_rules_edit-value-after-name_04.js] +[browser_rules_editable-field-focus_01.js] +[browser_rules_editable-field-focus_02.js] +[browser_rules_eyedropper.js] +[browser_rules_filtereditor-appears-on-swatch-click.js] +[browser_rules_filtereditor-commit-on-ENTER.js] +[browser_rules_filtereditor-revert-on-ESC.js] +skip-if = (os == "win" && debug) # bug 963492: win. +[browser_rules_grid-highlighter-on-navigate.js] +[browser_rules_grid-highlighter-on-reload.js] +[browser_rules_grid-toggle_01.js] +[browser_rules_grid-toggle_02.js] +[browser_rules_grid-toggle_03.js] +[browser_rules_guessIndentation.js] +[browser_rules_inherited-properties_01.js] +[browser_rules_inherited-properties_02.js] +[browser_rules_inherited-properties_03.js] +[browser_rules_inline-source-map.js] +[browser_rules_invalid.js] +[browser_rules_invalid-source-map.js] +[browser_rules_keybindings.js] +[browser_rules_keyframes-rule_01.js] +[browser_rules_keyframes-rule_02.js] +[browser_rules_keyframeLineNumbers.js] +[browser_rules_lineNumbers.js] +[browser_rules_livepreview.js] +[browser_rules_mark_overridden_01.js] +[browser_rules_mark_overridden_02.js] +[browser_rules_mark_overridden_03.js] +[browser_rules_mark_overridden_04.js] +[browser_rules_mark_overridden_05.js] +[browser_rules_mark_overridden_06.js] +[browser_rules_mark_overridden_07.js] +[browser_rules_mathml-element.js] +[browser_rules_media-queries.js] +[browser_rules_multiple-properties-duplicates.js] +[browser_rules_multiple-properties-priority.js] +[browser_rules_multiple-properties-unfinished_01.js] +[browser_rules_multiple-properties-unfinished_02.js] +[browser_rules_multiple_properties_01.js] +[browser_rules_multiple_properties_02.js] +[browser_rules_original-source-link.js] +[browser_rules_pseudo-element_01.js] +[browser_rules_pseudo-element_02.js] +[browser_rules_pseudo_lock_options.js] +[browser_rules_refresh-no-flicker.js] +[browser_rules_refresh-on-attribute-change_01.js] +[browser_rules_refresh-on-attribute-change_02.js] +[browser_rules_refresh-on-style-change.js] +[browser_rules_search-filter-computed-list_01.js] +[browser_rules_search-filter-computed-list_02.js] +[browser_rules_search-filter-computed-list_03.js] +[browser_rules_search-filter-computed-list_04.js] +[browser_rules_search-filter-computed-list_expander.js] +[browser_rules_search-filter-overridden-property.js] +[browser_rules_search-filter_01.js] +[browser_rules_search-filter_02.js] +[browser_rules_search-filter_03.js] +[browser_rules_search-filter_04.js] +[browser_rules_search-filter_05.js] +[browser_rules_search-filter_06.js] +[browser_rules_search-filter_07.js] +[browser_rules_search-filter_08.js] +[browser_rules_search-filter_09.js] +[browser_rules_search-filter_10.js] +[browser_rules_search-filter_context-menu.js] +subsuite = clipboard +[browser_rules_search-filter_escape-keypress.js] +[browser_rules_select-and-copy-styles.js] +subsuite = clipboard +[browser_rules_selector-highlighter-on-navigate.js] +[browser_rules_selector-highlighter_01.js] +[browser_rules_selector-highlighter_02.js] +[browser_rules_selector-highlighter_03.js] +[browser_rules_selector-highlighter_04.js] +[browser_rules_selector_highlight.js] +[browser_rules_strict-search-filter-computed-list_01.js] +[browser_rules_strict-search-filter_01.js] +[browser_rules_strict-search-filter_02.js] +[browser_rules_strict-search-filter_03.js] +[browser_rules_style-editor-link.js] +[browser_rules_urls-clickable.js] +[browser_rules_user-agent-styles.js] +[browser_rules_user-agent-styles-uneditable.js] +[browser_rules_user-property-reset.js] diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js b/devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js new file mode 100644 index 000000000..492739abe --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js @@ -0,0 +1,44 @@ +/* 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 adding properties to rules work and reselecting the element still +// show them. + +const TEST_URI = URL_ROOT + "doc_content_stylesheet.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield selectNode("#target", inspector); + + info("Setting a font-weight property on all rules"); + yield setPropertyOnAllRules(view); + + info("Reselecting the element"); + yield selectNode("body", inspector); + yield selectNode("#target", inspector); + + checkPropertyOnAllRules(view); +}); + +function* setPropertyOnAllRules(view) { + // Wait for the properties to be properly created on the backend and for the + // view to be updated. + let onRefreshed = view.once("ruleview-refreshed"); + for (let rule of view._elementStyle.rules) { + rule.editor.addProperty("font-weight", "bold", "", true); + } + yield onRefreshed; +} + +function checkPropertyOnAllRules(view) { + for (let rule of view._elementStyle.rules) { + let lastRule = rule.textProps[rule.textProps.length - 1]; + + is(lastRule.name, "font-weight", "Last rule name is font-weight"); + is(lastRule.value, "bold", "Last rule value is bold"); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_01.js b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_01.js new file mode 100644 index 000000000..78b3a4c91 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_01.js @@ -0,0 +1,44 @@ +/* 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 adding a new property and escapes the new empty property name editor. + +const TEST_URI = ` + <style type="text/css"> + #testid { + background-color: blue; + } + .testclass { + background-color: green; + } + </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); + + let elementRuleEditor = getRuleViewRuleEditor(view, 0); + let editor = yield focusNewRuleViewProperty(elementRuleEditor); + is(inplaceEditor(elementRuleEditor.newPropSpan), editor, + "The new property editor got focused"); + + info("Escape the new property editor"); + let onBlur = once(editor.input, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onBlur; + + info("Checking the state of cancelling a new property name editor"); + is(elementRuleEditor.rule.textProps.length, 0, + "Should have cancelled creating a new text property."); + ok(!elementRuleEditor.propertyList.hasChildNodes(), + "Should not have any properties."); + + is(view.styleDocument.activeElement, view.styleDocument.body, + "Correct element has focus"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js new file mode 100644 index 000000000..7f4d1564c --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js @@ -0,0 +1,34 @@ +/* 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 adding a new property and escapes the new empty property value editor. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + } + </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(); + yield selectNode("#testid", inspector); + + info("Test creating a new property and escaping"); + yield addProperty(view, 1, "color", "red", "VK_ESCAPE", false); + + is(view.styleDocument.activeElement, view.styleDocument.body, + "Correct element has focus"); + + let elementRuleEditor = getRuleViewRuleEditor(view, 1); + is(elementRuleEditor.rule.textProps.length, 1, + "Removed the new text property."); + is(elementRuleEditor.propertyList.children.length, 1, + "Removed the property editor."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.js b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.js new file mode 100644 index 000000000..4f8b42009 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.js @@ -0,0 +1,43 @@ +/* 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 adding a new property and escapes the property name editor with a +// value. + +const TEST_URI = ` + <style type='text/css'> + div { + background-color: blue; + } + </style> + <div>Test node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + + // Add a property to the element's style declaration, add some text, + // then press escape. + + let elementRuleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusNewRuleViewProperty(elementRuleEditor); + + is(inplaceEditor(elementRuleEditor.newPropSpan), editor, + "Next focused editor should be the new property editor."); + + EventUtils.sendString("background", view.styleWindow); + + let onBlur = once(editor.input, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield onBlur; + + is(elementRuleEditor.rule.textProps.length, 1, + "Should have canceled creating a new text property."); + is(view.styleDocument.activeElement, view.styleDocument.body, + "Correct element has focus"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-commented.js b/devtools/client/inspector/rules/test/browser_rules_add-property-commented.js new file mode 100644 index 000000000..eacf5db5a --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-property-commented.js @@ -0,0 +1,47 @@ +/* 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 commented properties can be added and are disabled. + +const TEST_URI = "<div id='testid'></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 testCreateNewSetOfCommentedAndUncommentedProperties(view); +}); + +function* testCreateNewSetOfCommentedAndUncommentedProperties(view) { + info("Test creating a new set of commented and uncommented properties"); + + info("Focusing a new property name in the rule-view"); + let ruleEditor = getRuleViewRuleEditor(view, 0); + let editor = yield focusEditableField(view, ruleEditor.closeBrace); + is(inplaceEditor(ruleEditor.newPropSpan), editor, + "The new property editor has focus"); + + info( + "Entering a commented property/value pair into the property name editor"); + let input = editor.input; + input.value = `color: blue; + /* background-color: yellow; */ + width: 200px; + height: 100px; + /* padding-bottom: 1px; */`; + + info("Pressing return to commit and focus the new value field"); + let onModifications = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onModifications; + + let textProps = ruleEditor.rule.textProps; + ok(textProps[0].enabled, "The 'color' property is enabled."); + ok(!textProps[1].enabled, "The 'background-color' property is disabled."); + ok(textProps[2].enabled, "The 'width' property is enabled."); + ok(textProps[3].enabled, "The 'height' property is enabled."); + ok(!textProps[4].enabled, "The 'padding-bottom' property is disabled."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-svg.js b/devtools/client/inspector/rules/test/browser_rules_add-property-svg.js new file mode 100644 index 000000000..a53421db3 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-property-svg.js @@ -0,0 +1,22 @@ +/* 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 editing SVG styles using the rules view. + +var TEST_URL = "chrome://global/skin/icons/warning.svg"; +var TEST_SELECTOR = "path"; + +add_task(function* () { + yield addTab(TEST_URL); + let {inspector, view} = yield openRuleView(); + yield selectNode(TEST_SELECTOR, inspector); + + info("Test creating a new property"); + yield addProperty(view, 0, "fill", "red"); + + is((yield getComputedStyleProperty(TEST_SELECTOR, null, "fill")), + "rgb(255, 0, 0)", "The fill was changed to red"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property_01.js b/devtools/client/inspector/rules/test/browser_rules_add-property_01.js new file mode 100644 index 000000000..1d7068d54 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-property_01.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"; + +// Test adding an invalid property. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + } + .testclass { + background-color: green; + } + </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); + + info("Test creating a new property"); + let textProp = yield addProperty(view, 0, "background-color", "#XYZ"); + + is(textProp.value, "#XYZ", "Text prop should have been changed."); + is(textProp.overridden, true, "Property should be overridden"); + is(textProp.editor.isValid(), false, "#XYZ should not be a valid entry"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property_02.js b/devtools/client/inspector/rules/test/browser_rules_add-property_02.js new file mode 100644 index 000000000..6f6bef0f7 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-property_02.js @@ -0,0 +1,65 @@ +/* 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 adding a valid property to a CSS rule, and navigating through the fields +// by pressing ENTER. + +const TEST_URI = ` + <style type="text/css"> + #testid { + color: blue; + } + </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(); + yield selectNode("#testid", inspector); + + info("Focus the new property name field"); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusNewRuleViewProperty(ruleEditor); + let input = editor.input; + + is(inplaceEditor(ruleEditor.newPropSpan), editor, + "Next focused editor should be the new property editor."); + ok(input.selectionStart === 0 && input.selectionEnd === input.value.length, + "Editor contents are selected."); + + // Try clicking on the editor's input again, shouldn't cause trouble + // (see bug 761665). + EventUtils.synthesizeMouse(input, 1, 1, {}, view.styleWindow); + input.select(); + + info("Entering the property name"); + editor.input.value = "background-color"; + + info("Pressing RETURN and waiting for the value field focus"); + let onNameAdded = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + + yield onNameAdded; + + editor = inplaceEditor(view.styleDocument.activeElement); + + is(ruleEditor.rule.textProps.length, 2, + "Should have created a new text property."); + is(ruleEditor.propertyList.children.length, 2, + "Should have created a property editor."); + let textProp = ruleEditor.rule.textProps[1]; + is(editor, inplaceEditor(textProp.editor.valueSpan), + "Should be editing the value span now."); + + info("Entering the property value"); + let onValueAdded = view.once("ruleview-changed"); + editor.input.value = "purple"; + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onValueAdded; + + is(textProp.value, "purple", "Text prop should have been changed."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js new file mode 100644 index 000000000..1cf04a275 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js @@ -0,0 +1,30 @@ +/* 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 adding a new rule and a new property in this rule. + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8,<div id='testid'>Styled Node</div>"); + let {inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("#testid", inspector); + + info("Adding a new rule for this node and blurring the new selector field"); + yield addNewRuleAndDismissEditor(inspector, view, "#testid", 1); + + info("Adding a new property for this rule"); + let ruleEditor = getRuleViewRuleEditor(view, 1); + + let onRuleViewChanged = view.once("ruleview-changed"); + ruleEditor.addProperty("font-weight", "bold", "", true); + yield onRuleViewChanged; + + let textProps = ruleEditor.rule.textProps; + let prop = textProps[textProps.length - 1]; + is(prop.name, "font-weight", "The last property name is font-weight"); + is(prop.value, "bold", "The last property value is bold"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.js new file mode 100644 index 000000000..1441213b3 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.js @@ -0,0 +1,51 @@ +/* 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 if the `Add rule` button disables itself properly for non-element nodes +// and anonymous element. + +const TEST_URI = ` + <style type="text/css"> + #pseudo::before { + content: "before"; + } + </style> + <div id="pseudo"></div> + <div id="testid">Test Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield testDisabledButton(inspector, view); +}); + +function* testDisabledButton(inspector, view) { + let node = "#testid"; + + info("Selecting a real element"); + yield selectNode(node, inspector); + ok(!view.addRuleButton.disabled, "Add rule button should be enabled"); + + info("Select a null element"); + yield view.selectElement(null); + ok(view.addRuleButton.disabled, "Add rule button should be disabled"); + + info("Selecting a real element"); + yield selectNode(node, inspector); + ok(!view.addRuleButton.disabled, "Add rule button should be enabled"); + + info("Selecting a pseudo element"); + let pseudo = yield getNodeFront("#pseudo", inspector); + let children = yield inspector.walker.children(pseudo); + let before = children.nodes[0]; + yield selectNode(before, inspector); + ok(view.addRuleButton.disabled, "Add rule button should be disabled"); + + info("Selecting a real element"); + yield selectNode(node, inspector); + ok(!view.addRuleButton.disabled, "Add rule button should be enabled"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.js new file mode 100644 index 000000000..b59f317a5 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.js @@ -0,0 +1,55 @@ +/* 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 the behaviour of adding a new rule to the rule view and editing +// its selector. + +const TEST_URI = ` + <style type="text/css"> + #testid { + text-align: center; + } + </style> + <div id="testid">Styled Node</div> + <span>This is a span</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + yield addNewRule(inspector, view); + yield testEditSelector(view, "span"); + + info("Selecting the modified element with the new rule"); + yield selectNode("span", inspector); + yield checkModifiedElement(view, "span"); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector field"); + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let editor = idRuleEditor.selectorText.ownerDocument.activeElement; + + info("Entering a new selector name and committing"); + editor.value = name; + + info("Waiting for rule view to update"); + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + is(view._elementStyle.rules.length, 3, "Should have 3 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); +} + +function* checkModifiedElement(view, name) { + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js new file mode 100644 index 000000000..7b0ba7812 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js @@ -0,0 +1,57 @@ +/* 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 adding a rule on elements nested in iframes. + +const TEST_URI = + `<div>outer</div> + <iframe id="frame1" src="data:text/html;charset=utf-8,<div>inner1</div>"> + </iframe> + <iframe id="frame2" src="data:text/html;charset=utf-8,<div>inner2</div>"> + </iframe>`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + yield addNewRuleAndDismissEditor(inspector, view, "div", 1); + yield addNewProperty(view, 1, "color", "red"); + + let innerFrameDiv1 = yield getNodeFrontInFrame("div", "#frame1", inspector); + yield selectNode(innerFrameDiv1, inspector); + yield addNewRuleAndDismissEditor(inspector, view, "div", 1); + yield addNewProperty(view, 1, "color", "blue"); + + let innerFrameDiv2 = yield getNodeFrontInFrame("div", "#frame2", inspector); + yield selectNode(innerFrameDiv2, inspector); + yield addNewRuleAndDismissEditor(inspector, view, "div", 1); + yield addNewProperty(view, 1, "color", "green"); +}); + +/** + * Add a new property in the rule at the provided index in the rule view. + * + * @param {RuleView} view + * @param {Number} index + * The index of the rule in which we should add a new property. + * @param {String} name + * The name of the new property. + * @param {String} value + * The value of the new property. + */ +function* addNewProperty(view, index, name, value) { + let idRuleEditor = getRuleViewRuleEditor(view, index); + info(`Adding new property "${name}: ${value};"`); + + let onRuleViewChanged = view.once("ruleview-changed"); + idRuleEditor.addProperty(name, value, "", true); + yield onRuleViewChanged; + + let textProps = idRuleEditor.rule.textProps; + let lastProperty = textProps[textProps.length - 1]; + is(lastProperty.name, name, "Last property has the expected name"); + is(lastProperty.value, value, "Last property has the expected value"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.js new file mode 100644 index 000000000..98e34e69f --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.js @@ -0,0 +1,41 @@ +/* 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 the behaviour of adding a new rule using the add rule button +// on namespaced elements. + +const XHTML = ` + <!DOCTYPE html> + <html xmlns="http://www.w3.org/1999/xhtml" + xmlns:svg="http://www.w3.org/2000/svg"> + <body> + <svg:svg width="100" height="100"> + <svg:clipPath> + <svg:rect x="0" y="0" width="10" height="5"></svg:rect> + </svg:clipPath> + <svg:circle cx="0" cy="0" r="5"></svg:circle> + </svg:svg> + </body> + </html> +`; +const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML); + +const TEST_DATA = [ + { node: "clipPath", expected: "clipPath" }, + { node: "rect", expected: "rect" }, + { node: "circle", expected: "circle" } +]; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + + for (let data of TEST_DATA) { + let {node, expected} = data; + yield selectNode(node, inspector); + yield addNewRuleAndDismissEditor(inspector, view, expected, 1); + } +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js new file mode 100644 index 000000000..39f773c13 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js @@ -0,0 +1,82 @@ +/* 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 adding a rule with pseudo class locks on. + +const TEST_URI = "<p id='element'>Test element</p>"; + +const EXPECTED_SELECTOR = "#element"; +const TEST_DATA = [ + [], + [":hover"], + [":hover", ":active"], + [":hover", ":active", ":focus"], + [":active"], + [":active", ":focus"], + [":focus"] +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#element", inspector); + + for (let data of TEST_DATA) { + yield runTestData(inspector, view, data); + } +}); + +function* runTestData(inspector, view, pseudoClasses) { + yield setPseudoLocks(inspector, view, pseudoClasses); + + let expected = EXPECTED_SELECTOR + pseudoClasses.join(""); + yield addNewRuleAndDismissEditor(inspector, view, expected, 1); + + yield resetPseudoLocks(inspector, view); +} + +function* setPseudoLocks(inspector, view, pseudoClasses) { + if (pseudoClasses.length == 0) { + return; + } + + for (let pseudoClass of pseudoClasses) { + switch (pseudoClass) { + case ":hover": + view.hoverCheckbox.click(); + yield inspector.once("rule-view-refreshed"); + break; + case ":active": + view.activeCheckbox.click(); + yield inspector.once("rule-view-refreshed"); + break; + case ":focus": + view.focusCheckbox.click(); + yield inspector.once("rule-view-refreshed"); + break; + } + } +} + +function* resetPseudoLocks(inspector, view) { + if (!view.hoverCheckbox.checked && + !view.activeCheckbox.checked && + !view.focusCheckbox.checked) { + return; + } + if (view.hoverCheckbox.checked) { + view.hoverCheckbox.click(); + yield inspector.once("rule-view-refreshed"); + } + if (view.activeCheckbox.checked) { + view.activeCheckbox.click(); + yield inspector.once("rule-view-refreshed"); + } + if (view.focusCheckbox.checked) { + view.focusCheckbox.click(); + yield inspector.once("rule-view-refreshed"); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js new file mode 100644 index 000000000..294eb67e4 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js @@ -0,0 +1,80 @@ +/* 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 the behaviour of adding a new rule to the rule view, adding a new +// property and editing the selector. + +const TEST_URI = ` + <style type="text/css"> + #testid { + text-align: center; + } + </style> + <div id="testid">Styled Node</div> + <span>This is a span</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + yield addNewRuleAndDismissEditor(inspector, view, "#testid", 1); + + info("Adding a new property to the new rule"); + yield testAddingProperty(view, 1); + + info("Editing existing selector field"); + yield testEditSelector(view, "span"); + + info("Selecting the modified element"); + yield selectNode("span", inspector); + + info("Check new rule and property exist in the modified element"); + yield checkModifiedElement(view, "span", 1); +}); + +function* testAddingProperty(view, index) { + let ruleEditor = getRuleViewRuleEditor(view, index); + ruleEditor.addProperty("font-weight", "bold", "", true); + let textProps = ruleEditor.rule.textProps; + let lastRule = textProps[textProps.length - 1]; + is(lastRule.name, "font-weight", "Last rule name is font-weight"); + is(lastRule.value, "bold", "Last rule value is bold"); +} + +function* testEditSelector(view, name) { + let idRuleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, idRuleEditor.selectorText); + + is(inplaceEditor(idRuleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name: " + name); + editor.input.value = name; + + info("Waiting for rule view to update"); + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + is(view._elementStyle.rules.length, 3, "Should have 3 rules."); +} + +function* checkModifiedElement(view, name, index) { + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); + + let idRuleEditor = getRuleViewRuleEditor(view, index); + let textProps = idRuleEditor.rule.textProps; + let lastRule = textProps[textProps.length - 1]; + is(lastRule.name, "font-weight", "Last rule name is font-weight"); + is(lastRule.value, "bold", "Last rule value is bold"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js new file mode 100644 index 000000000..976fc9643 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js @@ -0,0 +1,42 @@ +/* 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 the a new CSS rule can be added using the context menu. + +const TEST_URI = '<div id="testid">Test 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 addNewRuleFromContextMenu(inspector, view); + yield testNewRule(view); +}); + +function* addNewRuleFromContextMenu(inspector, view) { + info("Waiting for context menu to be shown"); + + let allMenuItems = openStyleContextMenuAndGetAllItems(view, view.element); + let menuitemAddRule = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.addNewRule")); + + ok(menuitemAddRule.visible, "Add rule is visible"); + + info("Adding the new rule and expecting a ruleview-changed event"); + let onRuleViewChanged = view.once("ruleview-changed"); + menuitemAddRule.click(); + yield onRuleViewChanged; +} + +function* testNewRule(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = ruleEditor.selectorText.ownerDocument.activeElement; + is(editor.value, "#testid", "Selector editor value is as expected"); + + info("Escaping from the selector field the change"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule.js b/devtools/client/inspector/rules/test/browser_rules_add-rule.js new file mode 100644 index 000000000..296105c85 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule.js @@ -0,0 +1,47 @@ +/* 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 adding a new rule using the add rule button. + +const TEST_URI = ` + <style type="text/css"> + .testclass { + text-align: center; + } + </style> + <div id="testid" class="testclass">Styled Node</div> + <span class="testclass2">This is a span</span> + <span class="class1 class2">Multiple classes</span> + <span class="class3 class4">Multiple classes</span> + <p>Empty<p> + <h1 class="asd@@@@a!!!!:::@asd">Invalid characters in class</h1> + <h2 id="asd@@@a!!2a">Invalid characters in id</h2> + <svg viewBox="0 0 10 10"> + <circle cx="5" cy="5" r="5" fill="blue"></circle> + </svg> +`; + +const TEST_DATA = [ + { node: "#testid", expected: "#testid" }, + { node: ".testclass2", expected: ".testclass2" }, + { node: ".class1.class2", expected: ".class1.class2" }, + { node: ".class3.class4", expected: ".class3.class4" }, + { node: "p", expected: "p" }, + { node: "h1", expected: ".asd\\@\\@\\@\\@a\\!\\!\\!\\!\\:\\:\\:\\@asd" }, + { node: "h2", expected: "#asd\\@\\@\\@a\\!\\!2a" }, + { node: "circle", expected: "circle" } +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + for (let data of TEST_DATA) { + let {node, expected} = data; + yield selectNode(node, inspector); + yield addNewRuleAndDismissEditor(inspector, view, expected, 1); + } +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_authored.js b/devtools/client/inspector/rules/test/browser_rules_authored.js new file mode 100644 index 000000000..cb0dd1186 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_authored.js @@ -0,0 +1,49 @@ +/* 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. + +function* createTestContent(style) { + let html = `<style type="text/css"> + ${style} + </style> + <div id="testid" class="testclass">Styled Node</div>`; + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(html)); + + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + return view; +} + +add_task(function* () { + 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); + } +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_authored_color.js b/devtools/client/inspector/rules/test/browser_rules_authored_color.js new file mode 100644 index 000000000..4c5cab206 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_authored_color.js @@ -0,0 +1,67 @@ +/* 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 color styles. + +/** + * Array of test color objects: + * {String} name: name of the used & expected color format. + * {String} id: id of the element that will be created to test this color. + * {String} color: initial value of the color property applied to the test element. + * {String} result: expected value of the color property after edition. + */ +const colors = [ + {name: "hex", id: "test1", color: "#f0c", result: "#0f0"}, + {name: "rgb", id: "test2", color: "rgb(0,128,250)", result: "rgb(0, 255, 0)"}, + // Test case preservation. + {name: "hex", id: "test3", color: "#F0C", result: "#0F0"}, +]; + +add_task(function* () { + Services.prefs.setCharPref("devtools.defaultColorUnit", "authored"); + + let html = ""; + for (let {color, id} of colors) { + html += `<div id="${id}" style="color: ${color}">Styled Node</div>`; + } + + let tab = yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(html)); + + let {inspector, view} = yield openRuleView(); + + for (let color of colors) { + let cPicker = view.tooltips.colorPicker; + let selector = "#" + color.id; + yield selectNode(selector, inspector); + + let swatch = getRuleViewProperty(view, "element", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + let onColorPickerReady = cPicker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + yield simulateColorPickerChange(view, cPicker, [0, 255, 0, 1], { + selector, + name: "color", + value: "rgb(0, 255, 0)" + }); + + let spectrum = cPicker.spectrum; + let onHidden = cPicker.tooltip.once("hidden"); + // Validating the color change ends up updating the rule view twice + let onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2); + focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN"); + yield onHidden; + yield onRuleViewChanged; + + is(getRuleViewPropertyValue(view, "element", "color"), color.result, + "changing the color preserved the unit for " + color.name); + } + + let target = TargetFactory.forTab(tab); + yield gDevTools.closeToolbox(target); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_authored_override.js b/devtools/client/inspector/rules/test/browser_rules_authored_override.js new file mode 100644 index 000000000..7305e5712 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_authored_override.js @@ -0,0 +1,53 @@ +/* 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. + +function* createTestContent(style) { + let html = `<style type="text/css"> + ${style} + </style> + <div id="testid" class="testclass">Styled Node</div>`; + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(html)); + + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + return view; +} + +add_task(function* () { + let gradientText1 = "(orange, blue);"; + let gradientText2 = "(pink, teal);"; + + let view = + yield createTestContent("#testid {" + + " background-image: linear-gradient" + + gradientText1 + + " background-image: -ms-linear-gradient" + + gradientText2 + + " background-image: linear-gradient" + + gradientText2 + + "} "); + + 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); + } + + yield togglePropStatus(view, rule.textProps[2]); + + // 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); + } +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js b/devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js new file mode 100644 index 000000000..adc8eb2ee --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the rule-view content is correct for stylesheet generated +// with createObjectURL(cssBlob) +const TEST_URL = URL_ROOT + "doc_blob_stylesheet.html"; + +add_task(function* () { + yield addTab(TEST_URL); + let {inspector, view} = yield openRuleView(); + + yield selectNode("h1", inspector); + is(view.element.querySelectorAll("#noResults").length, 0, + "The no-results element is not displayed"); + + is(view.element.querySelectorAll(".ruleview-rule").length, 2, + "There are 2 displayed rules"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_colorUnit.js b/devtools/client/inspector/rules/test/browser_rules_colorUnit.js new file mode 100644 index 000000000..138f68365 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorUnit.js @@ -0,0 +1,65 @@ +/* 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 color selection respects the user pref. + +const TEST_URI = ` + <style type='text/css'> + #testid { + color: blue; + } + </style> + <div id='testid' class='testclass'>Styled Node</div> +`; + +add_task(function* () { + let TESTS = [ + {name: "hex", result: "#0f0"}, + {name: "rgb", result: "rgb(0, 255, 0)"} + ]; + + for (let {name, result} of TESTS) { + info("starting test for " + name); + Services.prefs.setCharPref("devtools.defaultColorUnit", name); + + let tab = yield addTab("data:text/html;charset=utf-8," + + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + yield selectNode("#testid", inspector); + yield basicTest(view, name, result); + + let target = TargetFactory.forTab(tab); + yield gDevTools.closeToolbox(target); + gBrowser.removeCurrentTab(); + } +}); + +function* basicTest(view, name, result) { + let cPicker = view.tooltips.colorPicker; + let swatch = getRuleViewProperty(view, "#testid", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + let onColorPickerReady = cPicker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + yield simulateColorPickerChange(view, cPicker, [0, 255, 0, 1], { + selector: "#testid", + name: "color", + value: "rgb(0, 255, 0)" + }); + + let spectrum = cPicker.spectrum; + let onHidden = cPicker.tooltip.once("hidden"); + // Validating the color change ends up updating the rule view twice + let onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2); + focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN"); + yield onHidden; + yield onRuleViewChanged; + + is(getRuleViewPropertyValue(view, "#testid", "color"), result, + "changing the color used the " + name + " unit"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.js new file mode 100644 index 000000000..a8d2fd5f1 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.js @@ -0,0 +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"; + +// Tests that after a color change, the image preview tooltip in the same +// property is displayed and positioned correctly. +// See bug 979292 + +const TEST_URI = ` + <style type="text/css"> + body { + background: url("chrome://global/skin/icons/warning-64.png"), linear-gradient(white, #F06 400px); + } + </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")[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); + ok(anchor, "The image preview tooltip is shown on the url span"); + is(anchor, url, "The anchor returned by the showOnHover callback is correct"); + + info("Open the color picker tooltip and change the color"); + let picker = ruleView.tooltips.colorPicker; + let onColorPickerReady = picker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + yield simulateColorPickerChange(ruleView, picker, [0, 0, 0, 1], { + selector: "body", + name: "background-image", + value: 'url("chrome://global/skin/icons/warning-64.png"), linear-gradient(rgb(0, 0, 0), rgb(255, 0, 102) 400px)' + }); + + let spectrum = picker.spectrum; + let onHidden = picker.tooltip.once("hidden"); + let onModifications = ruleView.once("ruleview-changed"); + focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN"); + yield onHidden; + yield onModifications; + + info("Verify again that the image preview tooltip works"); + // After a color change, the property is re-populated, we need to get the new + // dom node + url = getRuleViewProperty(ruleView, "body", "background").valueSpan + .querySelector(".theme-link"); + anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip, url); + ok(anchor, "The image preview tooltip is shown on the url span"); + is(anchor, url, "The anchor returned by the showOnHover callback is correct"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js new file mode 100644 index 000000000..743ad5180 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js @@ -0,0 +1,66 @@ +/* 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 after a color change, opening another tooltip, like the image +// preview doesn't revert the color change in the rule view. +// This used to happen when the activeSwatch wasn't reset when the colorpicker +// would hide. +// See bug 979292 + +const TEST_URI = ` + <style type="text/css"> + body { + background: red url("chrome://global/skin/icons/warning-64.png") + no-repeat center center; + } + </style> + Testing the color picker tooltip! +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + yield testColorChangeIsntRevertedWhenOtherTooltipIsShown(view); +}); + +function* testColorChangeIsntRevertedWhenOtherTooltipIsShown(ruleView) { + let swatch = getRuleViewProperty(ruleView, "body", "background").valueSpan + .querySelector(".ruleview-colorswatch"); + + info("Open the color picker tooltip and change the color"); + let picker = ruleView.tooltips.colorPicker; + let onColorPickerReady = picker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + yield simulateColorPickerChange(ruleView, picker, [0, 0, 0, 1], { + selector: "body", + name: "background-color", + value: "rgb(0, 0, 0)" + }); + + let spectrum = picker.spectrum; + + let onModifications = waitForNEvents(ruleView, "ruleview-changed", 2); + let onHidden = picker.tooltip.once("hidden"); + focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN"); + yield onHidden; + yield onModifications; + + info("Open the image preview tooltip"); + let value = getRuleViewProperty(ruleView, "body", "background").valueSpan; + let url = value.querySelector(".theme-link"); + let 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, "black", + "The swatch's color is correct"); + is(swatch.nextSibling.textContent, "black", "The color name is correct"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click.js new file mode 100644 index 000000000..383ffed6c --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click.js @@ -0,0 +1,51 @@ +/* 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 color pickers appear when clicking on color swatches. + +const TEST_URI = ` + <style type="text/css"> + body { + color: red; + background-color: #ededed; + background-image: url(chrome://global/skin/icons/warning-64.png); + border: 2em solid rgba(120, 120, 120, .5); + } + </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 propertiesToTest = ["color", "background-color", "border"]; + + for (let property of propertiesToTest) { + info("Testing that the colorpicker appears on swatch click"); + let value = getRuleViewProperty(view, "body", property).valueSpan; + let swatch = value.querySelector(".ruleview-colorswatch"); + yield testColorPickerAppearsOnColorSwatchClick(view, swatch); + } +}); + +function* testColorPickerAppearsOnColorSwatchClick(view, swatch) { + let cPicker = view.tooltips.colorPicker; + ok(cPicker, "The rule-view has the expected colorPicker property"); + + let cPickerPanel = cPicker.tooltip.panel; + ok(cPickerPanel, "The XUL panel for the color picker exists"); + + let onColorPickerReady = cPicker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + ok(true, "The color picker was shown on click of the color swatch"); + ok(!inplaceEditor(swatch.parentNode), + "The inplace editor wasn't shown as a result of the color swatch click"); + + yield hideTooltipAndWaitForRuleViewChanged(cPicker, view); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-commit-on-ENTER.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-commit-on-ENTER.js new file mode 100644 index 000000000..129e8f245 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-commit-on-ENTER.js @@ -0,0 +1,61 @@ +/* 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 a color change in the color picker is committed when ENTER is +// pressed. + +const TEST_URI = ` + <style type="text/css"> + body { + border: 2em solid rgba(120, 120, 120, .5); + } + </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 swatch = getRuleViewProperty(view, "body", "border").valueSpan + .querySelector(".ruleview-colorswatch"); + yield testPressingEnterCommitsChanges(swatch, view); +}); + +function* testPressingEnterCommitsChanges(swatch, ruleView) { + let cPicker = ruleView.tooltips.colorPicker; + + let onColorPickerReady = cPicker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + yield simulateColorPickerChange(ruleView, cPicker, [0, 255, 0, .5], { + selector: "body", + name: "border-left-color", + value: "rgba(0, 255, 0, 0.5)" + }); + + is(swatch.style.backgroundColor, "rgba(0, 255, 0, 0.5)", + "The color swatch's background was updated"); + is(getRuleViewProperty(ruleView, "body", "border").valueSpan.textContent, + "2em solid rgba(0, 255, 0, 0.5)", + "The text of the border css property was updated"); + + let onModified = ruleView.once("ruleview-changed"); + let spectrum = cPicker.spectrum; + let onHidden = cPicker.tooltip.once("hidden"); + focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN"); + yield onHidden; + yield onModified; + + is((yield getComputedStyleProperty("body", null, "border-left-color")), + "rgba(0, 255, 0, 0.5)", "The element's border was kept after RETURN"); + is(swatch.style.backgroundColor, "rgba(0, 255, 0, 0.5)", + "The color swatch's background was kept after RETURN"); + is(getRuleViewProperty(ruleView, "body", "border").valueSpan.textContent, + "2em solid rgba(0, 255, 0, 0.5)", + "The text of the border css property was kept after RETURN"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.js new file mode 100644 index 000000000..71ceb14c3 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.js @@ -0,0 +1,77 @@ +/* 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 changing a color in a gradient css declaration using the tooltip +// color picker works. + +const TEST_URI = ` + <style type="text/css"> + body { + background-image: linear-gradient(to left, #f06 25%, #333 95%, #000 100%); + } + </style> + Updating a gradient declaration with the color picker tooltip +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + + info("Testing that the colors in gradient properties are parsed correctly"); + testColorParsing(view); + + info("Testing that changing one of the colors of a gradient property works"); + yield testPickingNewColor(view); +}); + +function testColorParsing(view) { + let ruleEl = getRuleViewProperty(view, "body", "background-image"); + ok(ruleEl, "The background-image gradient declaration was found"); + + 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"]; + 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"); + let swatchEl = ruleEl.valueSpan.querySelector(".ruleview-colorswatch"); + let colorEl = ruleEl.valueSpan.querySelector(".ruleview-color"); + + info("Get the color picker tooltip and clicking on the swatch to show it"); + let cPicker = view.tooltips.colorPicker; + let onColorPickerReady = cPicker.once("ready"); + swatchEl.click(); + yield onColorPickerReady; + + let change = { + selector: "body", + name: "background-image", + value: "linear-gradient(to left, rgb(1, 1, 1) 25%, " + + "rgb(51, 51, 51) 95%, rgb(0, 0, 0) 100%)" + }; + yield simulateColorPickerChange(view, cPicker, [1, 1, 1, 1], change); + + is(swatchEl.style.backgroundColor, "rgb(1, 1, 1)", + "The color swatch's background was updated"); + is(colorEl.textContent, "#010101", "The color text was updated"); + is((yield getComputedStyleProperty("body", null, "background-image")), + "linear-gradient(to left, rgb(1, 1, 1) 25%, rgb(51, 51, 51) 95%, " + + "rgb(0, 0, 0) 100%)", + "The gradient has been updated correctly"); + + yield hideTooltipAndWaitForRuleViewChanged(cPicker, view); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js new file mode 100644 index 000000000..b50c63605 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js @@ -0,0 +1,46 @@ +/* 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 color picker tooltip hides when an image tooltip appears. + +const TEST_URI = ` + <style type="text/css"> + body { + color: red; + background-color: #ededed; + background-image: url(chrome://global/skin/icons/warning-64.png); + border: 2em solid rgba(120, 120, 120, .5); + } + </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 swatch = getRuleViewProperty(view, "body", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + + let bgImageSpan = getRuleViewProperty(view, "body", "background-image").valueSpan; + let uriSpan = bgImageSpan.querySelector(".theme-link"); + + let colorPicker = view.tooltips.colorPicker; + info("Showing the color picker tooltip by clicking on the color swatch"); + let onColorPickerReady = colorPicker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + info("Now showing the image preview tooltip to hide the color picker"); + let onHidden = colorPicker.tooltip.once("hidden"); + // Hiding the color picker refreshes the value. + let onRuleViewChanged = view.once("ruleview-changed"); + yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan); + yield onHidden; + yield onRuleViewChanged; + + ok(true, "The color picker closed when the image preview tooltip appeared"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js new file mode 100644 index 000000000..06fab72d6 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js @@ -0,0 +1,124 @@ +/* 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 color in the colorpicker tooltip can be changed several times. +// without causing error in various cases: +// - simple single-color property (color) +// - color and image property (background-image) +// - overridden property +// See bug 979292 and bug 980225 + +const TEST_URI = ` + <style type="text/css"> + body { + color: green; + background: red url("chrome://global/skin/icons/warning-64.png") + no-repeat center center; + } + p { + color: blue; + } + </style> + <p>Testing the color picker tooltip!</p> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + yield testSimpleMultipleColorChanges(inspector, view); + yield testComplexMultipleColorChanges(inspector, view); + yield testOverriddenMultipleColorChanges(inspector, view); +}); + +function* testSimpleMultipleColorChanges(inspector, ruleView) { + yield selectNode("p", inspector); + + info("Getting the <p> tag's color property"); + let swatch = getRuleViewProperty(ruleView, "p", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + + info("Opening the color picker"); + let picker = ruleView.tooltips.colorPicker; + let onColorPickerReady = picker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + info("Changing the color several times"); + let colors = [ + {rgba: [0, 0, 0, 1], computed: "rgb(0, 0, 0)"}, + {rgba: [100, 100, 100, 1], computed: "rgb(100, 100, 100)"}, + {rgba: [200, 200, 200, 1], computed: "rgb(200, 200, 200)"} + ]; + for (let {rgba, computed} of colors) { + yield simulateColorPickerChange(ruleView, picker, rgba, { + selector: "p", + name: "color", + value: computed + }); + } +} + +function* testComplexMultipleColorChanges(inspector, ruleView) { + yield selectNode("body", inspector); + + info("Getting the <body> tag's color property"); + let swatch = getRuleViewProperty(ruleView, "body", "background").valueSpan + .querySelector(".ruleview-colorswatch"); + + info("Opening the color picker"); + let picker = ruleView.tooltips.colorPicker; + let onColorPickerReady = picker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + info("Changing the color several times"); + let colors = [ + {rgba: [0, 0, 0, 1], computed: "rgb(0, 0, 0)"}, + {rgba: [100, 100, 100, 1], computed: "rgb(100, 100, 100)"}, + {rgba: [200, 200, 200, 1], computed: "rgb(200, 200, 200)"} + ]; + for (let {rgba, computed} of colors) { + yield simulateColorPickerChange(ruleView, picker, rgba, { + selector: "body", + name: "background-color", + value: computed + }); + } + + info("Closing the color picker"); + yield hideTooltipAndWaitForRuleViewChanged(picker, ruleView); +} + +function* testOverriddenMultipleColorChanges(inspector, ruleView) { + yield selectNode("p", inspector); + + info("Getting the <body> tag's color property"); + let swatch = getRuleViewProperty(ruleView, "body", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + + info("Opening the color picker"); + let picker = ruleView.tooltips.colorPicker; + let onColorPickerReady = picker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + info("Changing the color several times"); + let colors = [ + {rgba: [0, 0, 0, 1], computed: "rgb(0, 0, 0)"}, + {rgba: [100, 100, 100, 1], computed: "rgb(100, 100, 100)"}, + {rgba: [200, 200, 200, 1], computed: "rgb(200, 200, 200)"} + ]; + for (let {rgba, computed} of colors) { + yield simulateColorPickerChange(ruleView, picker, rgba, { + selector: "body", + name: "color", + value: computed + }); + is((yield getComputedStyleProperty("p", null, "color")), + "rgb(200, 200, 200)", "The color of the P tag is still correct"); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.js new file mode 100644 index 000000000..ef6ca02b1 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.js @@ -0,0 +1,67 @@ +/* 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 color pickers stops following the pointer if the pointer is +// released outside the tooltip frame (bug 1160720). + +const TEST_URI = "<body style='color: red'>Test page for bug 1160720"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + + let cSwatch = getRuleViewProperty(view, "element", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + + let picker = yield openColorPickerForSwatch(cSwatch, view); + let spectrum = picker.spectrum; + let change = spectrum.once("changed"); + + info("Pressing mouse down over color picker."); + let onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.synthesizeMouseAtCenter(spectrum.dragger, { + type: "mousedown", + }, spectrum.dragger.ownerDocument.defaultView); + yield onRuleViewChanged; + + let value = yield change; + info(`Color changed to ${value} on mousedown.`); + + // If the mousemove below fails to detect that the button is no longer pressed + // the spectrum will update and emit changed event synchronously after calling + // synthesizeMouse so this handler is executed before the test ends. + spectrum.once("changed", (event, newValue) => { + is(newValue, value, "Value changed on mousemove without a button pressed."); + }); + + // Releasing the button pressed by mousedown above on top of a different frame + // does not make sense in this test as EventUtils doesn't preserve the context + // i.e. the buttons that were pressed down between events. + + info("Moving mouse over color picker without any buttons pressed."); + + EventUtils.synthesizeMouse(spectrum.dragger, 10, 10, { + // -1 = no buttons are pressed down + button: -1, + type: "mousemove", + }, spectrum.dragger.ownerDocument.defaultView); +}); + +function* openColorPickerForSwatch(swatch, view) { + let cPicker = view.tooltips.colorPicker; + ok(cPicker, "The rule-view has the expected colorPicker property"); + + let cPickerPanel = cPicker.tooltip.panel; + ok(cPickerPanel, "The XUL panel for the color picker exists"); + + let onColorPickerReady = cPicker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + ok(true, "The color picker was shown on click of the color swatch"); + + return cPicker; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js new file mode 100644 index 000000000..e244d429c --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js @@ -0,0 +1,109 @@ +/* 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 a color change in the color picker is reverted when ESC is +// pressed. + +const TEST_URI = ` + <style type="text/css"> + body { + background-color: #EDEDED; + } + </style> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + yield testPressingEscapeRevertsChanges(view); + yield testPressingEscapeRevertsChangesAndDisables(view); +}); + +function* testPressingEscapeRevertsChanges(view) { + let {swatch, propEditor, cPicker} = yield openColorPickerAndSelectColor(view, + 1, 0, [0, 0, 0, 1], { + selector: "body", + name: "background-color", + value: "rgb(0, 0, 0)" + }); + + is(swatch.style.backgroundColor, "rgb(0, 0, 0)", + "The color swatch's background was updated"); + is(propEditor.valueSpan.textContent, "#000", + "The text of the background-color css property was updated"); + + let spectrum = cPicker.spectrum; + + info("Pressing ESCAPE to close the tooltip"); + let onHidden = cPicker.tooltip.once("hidden"); + let onModifications = view.once("ruleview-changed"); + EventUtils.sendKey("ESCAPE", spectrum.element.ownerDocument.defaultView); + yield onHidden; + yield onModifications; + + yield waitForComputedStyleProperty("body", null, "background-color", + "rgb(237, 237, 237)"); + is(propEditor.valueSpan.textContent, "#EDEDED", + "Got expected property value."); +} + +function* testPressingEscapeRevertsChangesAndDisables(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Disabling background-color property"); + let textProp = ruleEditor.rule.textProps[0]; + yield togglePropStatus(view, textProp); + + ok(textProp.editor.element.classList.contains("ruleview-overridden"), + "property is overridden."); + is(textProp.editor.enable.style.visibility, "visible", + "property enable checkbox is visible."); + ok(!textProp.editor.enable.getAttribute("checked"), + "property enable checkbox is not checked."); + ok(!textProp.editor.prop.enabled, + "background-color property is disabled."); + let newValue = yield getRulePropertyValue("background-color"); + is(newValue, "", "background-color should have been unset."); + + let {cPicker} = yield openColorPickerAndSelectColor(view, + 1, 0, [0, 0, 0, 1]); + + ok(!textProp.editor.element.classList.contains("ruleview-overridden"), + "property overridden is not displayed."); + is(textProp.editor.enable.style.visibility, "hidden", + "property enable checkbox is hidden."); + + let spectrum = cPicker.spectrum; + + info("Pressing ESCAPE to close the tooltip"); + let onHidden = cPicker.tooltip.once("hidden"); + let onModifications = view.once("ruleview-changed"); + EventUtils.sendKey("ESCAPE", spectrum.element.ownerDocument.defaultView); + yield onHidden; + yield onModifications; + + ok(textProp.editor.element.classList.contains("ruleview-overridden"), + "property is overridden."); + is(textProp.editor.enable.style.visibility, "visible", + "property enable checkbox is visible."); + ok(!textProp.editor.enable.getAttribute("checked"), + "property enable checkbox is not checked."); + ok(!textProp.editor.prop.enabled, + "background-color property is disabled."); + newValue = yield getRulePropertyValue("background-color"); + is(newValue, "", "background-color should have been unset."); + is(textProp.editor.valueSpan.textContent, "#EDEDED", + "Got expected property value."); +} + +function* getRulePropertyValue(name) { + let propValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: name + }); + return propValue; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.js new file mode 100644 index 000000000..b06ff37df --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.js @@ -0,0 +1,73 @@ +/* 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 color swatches are displayed next to colors in the rule-view. + +const TEST_URI = ` + <style type="text/css"> + body { + color: red; + background-color: #ededed; + background-image: url(chrome://global/skin/icons/warning-64.png); + border: 2em solid rgba(120, 120, 120, .5); + } + * { + color: blue; + background: linear-gradient( + to right, + #f00, + #f008, + #00ff00, + #00ff0080, + rgb(31,170,217), + rgba(31,170,217,.5), + hsl(5, 5%, 5%), + hsla(5, 5%, 5%, 0.25), + #F00, + #F008, + #00FF00, + #00FF0080, + RGB(31,170,217), + RGBA(31,170,217,.5), + HSL(5, 5%, 5%), + HSLA(5, 5%, 5%, 0.25)); + box-shadow: inset 0 0 2px 20px red, inset 0 0 2px 40px blue; + } + </style> + Testing the color picker tooltip! +`; + +// Tests that properties in the rule-view contain color swatches. +// Each entry in the test array should contain: +// { +// selector: the rule-view selector to look for the property in +// propertyName: the property to test +// nb: the number of color swatches this property should have +// } +const TESTS = [ + {selector: "body", propertyName: "color", nb: 1}, + {selector: "body", propertyName: "background-color", nb: 1}, + {selector: "body", propertyName: "border", nb: 1}, + {selector: "*", propertyName: "color", nb: 1}, + {selector: "*", propertyName: "background", nb: 16}, + {selector: "*", propertyName: "box-shadow", nb: 2}, +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + + for (let {selector, propertyName, nb} of TESTS) { + info("Looking for color swatches in property " + propertyName + + " in selector " + selector); + + let prop = getRuleViewProperty(view, selector, propertyName).valueSpan; + let swatches = prop.querySelectorAll(".ruleview-colorswatch"); + + ok(swatches.length, "Swatches found in the property"); + is(swatches.length, nb, "Correct number of swatches found in the property"); + } +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js new file mode 100644 index 000000000..566bae259 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js @@ -0,0 +1,139 @@ +/* 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 CSS property names are autocompleted and cycled correctly when +// editing an existing property in the rule view. + +// format : +// [ +// what key to press, +// expected input box value after keypress, +// is the popup open, +// is a suggestion selected in the popup, +// ] + +const OPEN = true, SELECTED = true; +var testData = [ + ["VK_RIGHT", "font", !OPEN, !SELECTED], + ["-", "font-size", OPEN, SELECTED], + ["f", "font-family", OPEN, SELECTED], + ["VK_BACK_SPACE", "font-f", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "font-", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "font", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "fon", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "fo", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "f", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "", !OPEN, !SELECTED], + ["d", "display", OPEN, SELECTED], + ["VK_DOWN", "dominant-baseline", OPEN, SELECTED], + ["VK_DOWN", "direction", OPEN, SELECTED], + ["VK_DOWN", "display", OPEN, SELECTED], + ["VK_UP", "direction", OPEN, SELECTED], + ["VK_UP", "dominant-baseline", OPEN, SELECTED], + ["VK_UP", "display", OPEN, SELECTED], + ["VK_BACK_SPACE", "d", !OPEN, !SELECTED], + ["i", "display", OPEN, SELECTED], + ["s", "display", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "dis", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "di", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "d", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "", !OPEN, !SELECTED], + ["VK_HOME", "", !OPEN, !SELECTED], + ["VK_END", "", !OPEN, !SELECTED], + ["VK_PAGE_UP", "", !OPEN, !SELECTED], + ["VK_PAGE_DOWN", "", !OPEN, !SELECTED], + ["d", "display", OPEN, SELECTED], + ["VK_HOME", "display", !OPEN, !SELECTED], + ["VK_END", "display", !OPEN, !SELECTED], + // Press right key to ensure caret move to end of the input on Mac OS since + // Mac OS doesn't move caret after pressing HOME / END. + ["VK_RIGHT", "display", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "displa", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "displ", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "disp", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "dis", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "di", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "d", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "", !OPEN, !SELECTED], + ["f", "font-size", OPEN, SELECTED], + ["i", "filter", OPEN, SELECTED], + ["VK_LEFT", "filter", !OPEN, !SELECTED], + ["VK_LEFT", "filter", !OPEN, !SELECTED], + ["i", "fiilter", !OPEN, !SELECTED], + ["VK_ESCAPE", null, !OPEN, !SELECTED], +]; + +const TEST_URI = "<h1 style='font: 24px serif'>Header</h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view, testActor} = yield openRuleView(); + + info("Test autocompletion after 1st page load"); + yield runAutocompletionTest(toolbox, inspector, view); + + info("Test autocompletion after page navigation"); + yield reloadPage(inspector, testActor); + yield runAutocompletionTest(toolbox, inspector, view); +}); + +function* runAutocompletionTest(toolbox, inspector, view) { + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing the css property editable field"); + let propertyName = view.styleDocument.querySelectorAll(".ruleview-propertyname")[0]; + let editor = yield focusEditableField(view, propertyName); + + info("Starting to test for css property completion"); + for (let i = 0; i < testData.length; i++) { + yield testCompletion(testData[i], editor, view); + } +} + +function* testCompletion([key, completion, open, selected], + editor, view) { + info("Pressing key " + key); + info("Expecting " + completion); + info("Is popup opened: " + open); + info("Is item selected: " + selected); + + // Listening for the right event that will tell us when the key has been + // entered and processed. + let onSuggest; + if (/(left|right|back_space|escape|home|end|page_up|page_down)/ig.test(key)) { + info("Adding event listener for " + + "left|right|back_space|escape|home|end|page_up|page_down keys"); + onSuggest = once(editor.input, "keypress"); + } else { + info("Waiting for after-suggest event on the editor"); + onSuggest = editor.once("after-suggest"); + } + + // Also listening for popup opened/closed events if needed. + let popupEvent = open ? "popup-opened" : "popup-closed"; + let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null; + + info("Synthesizing key " + key); + EventUtils.synthesizeKey(key, {}, view.styleWindow); + + // Flush the throttle for the preview text. + view.throttle.flush(); + + yield onSuggest; + yield onPopupEvent; + + info("Checking the state"); + if (completion !== null) { + is(editor.input.value, completion, "Correct value is autocompleted"); + } + if (!open) { + ok(!(editor.popup && editor.popup.isOpen), "Popup is closed"); + } else { + ok(editor.popup.isOpen, "Popup is open"); + is(editor.popup.selectedIndex !== -1, selected, "An item is selected"); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js new file mode 100644 index 000000000..fde8f5d12 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js @@ -0,0 +1,123 @@ +/* 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 CSS property names and values are autocompleted and cycled +// correctly when editing existing properties in the rule view. + +// format : +// [ +// what key to press, +// modifers, +// expected input box value after keypress, +// is the popup open, +// is a suggestion selected in the popup, +// expect ruleview-changed, +// ] + +const OPEN = true, SELECTED = true, CHANGE = true; +var testData = [ + ["b", {}, "beige", OPEN, SELECTED, CHANGE], + ["l", {}, "black", OPEN, SELECTED, CHANGE], + ["VK_DOWN", {}, "blanchedalmond", OPEN, SELECTED, CHANGE], + ["VK_DOWN", {}, "blue", OPEN, SELECTED, CHANGE], + ["VK_RIGHT", {}, "blue", !OPEN, !SELECTED, !CHANGE], + [" ", {}, "blue aliceblue", OPEN, SELECTED, CHANGE], + ["!", {}, "blue !important", !OPEN, !SELECTED, CHANGE], + ["VK_BACK_SPACE", {}, "blue !", !OPEN, !SELECTED, CHANGE], + ["VK_BACK_SPACE", {}, "blue ", !OPEN, !SELECTED, CHANGE], + ["VK_BACK_SPACE", {}, "blue", !OPEN, !SELECTED, CHANGE], + ["VK_TAB", {shiftKey: true}, "color", !OPEN, !SELECTED, CHANGE], + ["VK_BACK_SPACE", {}, "", !OPEN, !SELECTED, !CHANGE], + ["d", {}, "display", OPEN, SELECTED, !CHANGE], + ["VK_TAB", {}, "blue", !OPEN, !SELECTED, CHANGE], + ["n", {}, "none", !OPEN, !SELECTED, CHANGE], + ["VK_RETURN", {}, null, !OPEN, !SELECTED, CHANGE] +]; + +const TEST_URI = "<h1 style='color: red'>Header</h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view, testActor} = yield openRuleView(); + + info("Test autocompletion after 1st page load"); + yield runAutocompletionTest(toolbox, inspector, view); + + info("Test autocompletion after page navigation"); + yield reloadPage(inspector, testActor); + yield runAutocompletionTest(toolbox, inspector, view); +}); + +function* runAutocompletionTest(toolbox, inspector, view) { + info("Selecting the test node"); + yield selectNode("h1", inspector); + + let rule = getRuleViewRuleEditor(view, 0).rule; + let prop = rule.textProps[0]; + + info("Focusing the css property editable value"); + let editor = yield focusEditableField(view, prop.editor.valueSpan); + + info("Starting to test for css property completion"); + for (let i = 0; i < testData.length; i++) { + // Re-define the editor at each iteration, because the focus may have moved + // from property to value and back + editor = inplaceEditor(view.styleDocument.activeElement); + yield testCompletion(testData[i], editor, view); + } +} + +function* testCompletion([key, modifiers, completion, open, selected, change], + editor, view) { + info("Pressing key " + key); + info("Expecting " + completion); + info("Is popup opened: " + open); + info("Is item selected: " + selected); + + let onDone; + if (change) { + // If the key triggers a ruleview-changed, wait for that event, it will + // always be the last to be triggered and tells us when the preview has + // been done. + onDone = view.once("ruleview-changed"); + } else { + // Otherwise, expect an after-suggest event (except if the popup gets + // closed). + onDone = key !== "VK_RIGHT" && key !== "VK_BACK_SPACE" + ? editor.once("after-suggest") + : null; + } + + info("Synthesizing key " + key + ", modifiers: " + Object.keys(modifiers)); + + // Also listening for popup opened/closed events if needed. + let popupEvent = open ? "popup-opened" : "popup-closed"; + let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null; + + EventUtils.synthesizeKey(key, modifiers, view.styleWindow); + + // Flush the throttle for the preview text. + view.throttle.flush(); + + yield onDone; + yield onPopupEvent; + + // The key might have been a TAB or shift-TAB, in which case the editor will + // be a new one + editor = inplaceEditor(view.styleDocument.activeElement); + + info("Checking the state"); + if (completion !== null) { + is(editor.input.value, completion, "Correct value is autocompleted"); + } + + if (!open) { + ok(!(editor.popup && editor.popup.isOpen), "Popup is closed"); + } else { + ok(editor.popup.isOpen, "Popup is open"); + is(editor.popup.selectedIndex !== -1, selected, "An item is selected"); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.js new file mode 100644 index 000000000..86ff9ca03 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.js @@ -0,0 +1,102 @@ +/* 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 CSS property names are autocompleted and cycled correctly when +// creating a new property in the rule view. + +// format : +// [ +// what key to press, +// expected input box value after keypress, +// is the popup open, +// is a suggestion selected in the popup, +// ] +const OPEN = true, SELECTED = true; +var testData = [ + ["d", "display", OPEN, SELECTED], + ["VK_DOWN", "dominant-baseline", OPEN, SELECTED], + ["VK_DOWN", "direction", OPEN, SELECTED], + ["VK_DOWN", "display", OPEN, SELECTED], + ["VK_UP", "direction", OPEN, SELECTED], + ["VK_UP", "dominant-baseline", OPEN, SELECTED], + ["VK_UP", "display", OPEN, SELECTED], + ["VK_BACK_SPACE", "d", !OPEN, !SELECTED], + ["i", "display", OPEN, SELECTED], + ["s", "display", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "dis", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "di", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "d", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "", !OPEN, !SELECTED], + ["f", "font-size", OPEN, SELECTED], + ["i", "filter", OPEN, SELECTED], + ["VK_ESCAPE", null, !OPEN, !SELECTED], +]; + +const TEST_URI = "<h1 style='border: 1px solid red'>Header</h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view, testActor} = yield openRuleView(); + + info("Test autocompletion after 1st page load"); + yield runAutocompletionTest(toolbox, inspector, view); + + info("Test autocompletion after page navigation"); + yield reloadPage(inspector, testActor); + yield runAutocompletionTest(toolbox, inspector, view); +}); + +function* runAutocompletionTest(toolbox, inspector, view) { + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing the css property editable field"); + let ruleEditor = getRuleViewRuleEditor(view, 0); + let editor = yield focusNewRuleViewProperty(ruleEditor); + + info("Starting to test for css property completion"); + for (let i = 0; i < testData.length; i++) { + yield testCompletion(testData[i], editor, view); + } +} + +function* testCompletion([key, completion, open, isSelected], editor, view) { + info("Pressing key " + key); + info("Expecting " + completion); + info("Is popup opened: " + open); + info("Is item selected: " + isSelected); + + let onSuggest; + + if (/(right|back_space|escape)/ig.test(key)) { + info("Adding event listener for right|back_space|escape keys"); + onSuggest = once(editor.input, "keypress"); + } else { + info("Waiting for after-suggest event on the editor"); + onSuggest = editor.once("after-suggest"); + } + + // Also listening for popup opened/closed events if needed. + let popupEvent = open ? "popup-opened" : "popup-closed"; + let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null; + + info("Synthesizing key " + key); + EventUtils.synthesizeKey(key, {}, view.styleWindow); + + yield onSuggest; + yield onPopupEvent; + + info("Checking the state"); + if (completion !== null) { + is(editor.input.value, completion, "Correct value is autocompleted"); + } + if (!open) { + ok(!(editor.popup && editor.popup.isOpen), "Popup is closed"); + } else { + ok(editor.popup.isOpen, "Popup is open"); + is(editor.popup.selectedIndex !== -1, isSelected, "An item is selected"); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js new file mode 100644 index 000000000..d89e5129d --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js @@ -0,0 +1,129 @@ +/* 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 CSS property names and values are autocompleted and cycled +// correctly when editing new properties in the rule view. + +// format : +// [ +// what key to press, +// modifers, +// expected input box value after keypress, +// is the popup open, +// is a suggestion selected in the popup, +// expect ruleview-changed, +// ] + +const OPEN = true, SELECTED = true, CHANGE = true; +const testData = [ + ["d", {}, "display", OPEN, SELECTED, !CHANGE], + ["VK_TAB", {}, "", OPEN, !SELECTED, CHANGE], + ["VK_DOWN", {}, "block", OPEN, SELECTED, CHANGE], + ["n", {}, "none", !OPEN, !SELECTED, CHANGE], + ["VK_TAB", {shiftKey: true}, "display", !OPEN, !SELECTED, CHANGE], + ["VK_BACK_SPACE", {}, "", !OPEN, !SELECTED, !CHANGE], + ["o", {}, "overflow", OPEN, SELECTED, !CHANGE], + ["u", {}, "outline", OPEN, SELECTED, !CHANGE], + ["VK_DOWN", {}, "outline-color", OPEN, SELECTED, !CHANGE], + ["VK_TAB", {}, "none", !OPEN, !SELECTED, CHANGE], + ["r", {}, "rebeccapurple", OPEN, SELECTED, CHANGE], + ["VK_DOWN", {}, "red", OPEN, SELECTED, CHANGE], + ["VK_DOWN", {}, "rgb", OPEN, SELECTED, CHANGE], + ["VK_DOWN", {}, "rgba", OPEN, SELECTED, CHANGE], + ["VK_DOWN", {}, "rosybrown", OPEN, SELECTED, CHANGE], + ["VK_DOWN", {}, "royalblue", OPEN, SELECTED, CHANGE], + ["VK_RIGHT", {}, "royalblue", !OPEN, !SELECTED, !CHANGE], + [" ", {}, "royalblue aliceblue", OPEN, SELECTED, CHANGE], + ["!", {}, "royalblue !important", !OPEN, !SELECTED, CHANGE], + ["VK_ESCAPE", {}, null, !OPEN, !SELECTED, CHANGE] +]; + +const TEST_URI = ` + <style type="text/css"> + h1 { + border: 1px solid red; + } + </style> + <h1>Test element</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view, testActor} = yield openRuleView(); + + info("Test autocompletion after 1st page load"); + yield runAutocompletionTest(toolbox, inspector, view); + + info("Test autocompletion after page navigation"); + yield reloadPage(inspector, testActor); + yield runAutocompletionTest(toolbox, inspector, view); +}); + +function* runAutocompletionTest(toolbox, inspector, view) { + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing a new css property editable property"); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusNewRuleViewProperty(ruleEditor); + + info("Starting to test for css property completion"); + for (let i = 0; i < testData.length; i++) { + // Re-define the editor at each iteration, because the focus may have moved + // from property to value and back + editor = inplaceEditor(view.styleDocument.activeElement); + yield testCompletion(testData[i], editor, view); + } +} + +function* testCompletion([key, modifiers, completion, open, selected, change], + editor, view) { + info("Pressing key " + key); + info("Expecting " + completion); + info("Is popup opened: " + open); + info("Is item selected: " + selected); + + let onDone; + if (change) { + // If the key triggers a ruleview-changed, wait for that event, it will + // always be the last to be triggered and tells us when the preview has + // been done. + onDone = view.once("ruleview-changed"); + } else { + // Otherwise, expect an after-suggest event (except if the popup gets + // closed). + onDone = key !== "VK_RIGHT" && key !== "VK_BACK_SPACE" + ? editor.once("after-suggest") + : null; + } + + // Also listening for popup opened/closed events if needed. + let popupEvent = open ? "popup-opened" : "popup-closed"; + let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null; + + info("Synthesizing key " + key + ", modifiers: " + Object.keys(modifiers)); + EventUtils.synthesizeKey(key, modifiers, view.styleWindow); + + // Flush the throttle for the preview text. + view.throttle.flush(); + + yield onDone; + yield onPopupEvent; + + info("Checking the state"); + if (completion !== null) { + // The key might have been a TAB or shift-TAB, in which case the editor will + // be a new one + editor = inplaceEditor(view.styleDocument.activeElement); + is(editor.input.value, completion, "Correct value is autocompleted"); + } + if (!open) { + ok(!(editor.popup && editor.popup.isOpen), "Popup is closed"); + } else { + ok(editor.popup.isOpen, "Popup is open"); + is(editor.popup.selectedIndex !== -1, selected, "An item is selected"); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js new file mode 100644 index 000000000..a5072429c --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js @@ -0,0 +1,47 @@ +/* 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"; + +// Regression test for a case where completing gave the wrong answer. +// See bug 1179318. + +const TEST_URI = "<h1 style='color: red'>Header</h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Test autocompletion for background-color"); + yield runAutocompletionTest(toolbox, inspector, view); +}); + +function* runAutocompletionTest(toolbox, inspector, view) { + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing the new property editable field"); + let ruleEditor = getRuleViewRuleEditor(view, 0); + let editor = yield focusNewRuleViewProperty(ruleEditor); + + info("Sending \"background\" to the editable field"); + for (let key of "background") { + let onSuggest = editor.once("after-suggest"); + EventUtils.synthesizeKey(key, {}, view.styleWindow); + yield onSuggest; + } + + const itemIndex = 4; + + let bgcItem = editor.popup.getItemAtIndex(itemIndex); + is(bgcItem.label, "background-color", + "check the expected completion element"); + + editor.popup.selectedIndex = itemIndex; + + let node = editor.popup._list.childNodes[itemIndex]; + EventUtils.synthesizeMouseAtCenter(node, {}, editor.popup._window); + + is(editor.input.value, "background-color", "Correct value is autocompleted"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.js new file mode 100644 index 000000000..e19794e1b --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.js @@ -0,0 +1,73 @@ +/* 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 a new property editor supports the following flow: +// - type first character of property name +// - select an autocomplete suggestion !!with a mouse click!! +// - press RETURN to move to the property value +// - blur the input to commit + +const TEST_URI = "<style>.title {color: red;}</style>" + + "<h1 class=title>Header</h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let { inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing the new property editable field"); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusNewRuleViewProperty(ruleEditor); + + info("Sending \"background\" to the editable field."); + for (let key of "background") { + let onSuggest = editor.once("after-suggest"); + EventUtils.synthesizeKey(key, {}, view.styleWindow); + yield onSuggest; + } + + const itemIndex = 4; + let bgcItem = editor.popup.getItemAtIndex(itemIndex); + is(bgcItem.label, "background-color", + "Check the expected completion element is background-color."); + editor.popup.selectedIndex = itemIndex; + + info("Select the background-color suggestion with a mouse click."); + let onSuggest = editor.once("after-suggest"); + let node = editor.popup.elements.get(bgcItem); + EventUtils.synthesizeMouseAtCenter(node, {}, editor.popup._window); + + yield onSuggest; + is(editor.input.value, "background-color", "Correct value is autocompleted"); + + info("Press RETURN to move the focus to a property value editor."); + let onModifications = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + + yield onModifications; + + // Getting the new value editor after focus + editor = inplaceEditor(view.styleDocument.activeElement); + let textProp = ruleEditor.rule.textProps[1]; + + is(ruleEditor.rule.textProps.length, 2, + "Created a new text property."); + is(ruleEditor.propertyList.children.length, 2, + "Created a property editor."); + is(editor, inplaceEditor(textProp.editor.valueSpan), + "Editing the value span now."); + + info("Entering a value and blurring the field to expect a rule change"); + editor.input.value = "#F00"; + + onModifications = view.once("ruleview-changed"); + editor.input.blur(); + yield onModifications; + + is(textProp.value, "#F00", "Text prop should have been changed."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js new file mode 100644 index 000000000..ec939eafc --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js @@ -0,0 +1,131 @@ +/* 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 the behaviour of the CSS autocomplete for CSS value displayed on +// multiple lines. Expected behavior is: +// - UP/DOWN should navigate in the input and not increment/decrement numbers +// - typing a new value should still trigger the autocomplete +// - UP/DOWN when the autocomplete popup is displayed should cycle through +// suggestions + +const LONG_CSS_VALUE = + "transparent linear-gradient(0deg, blue 0%, white 5%, red 10%, blue 15%, " + + "white 20%, red 25%, blue 30%, white 35%, red 40%, blue 45%, white 50%, " + + "red 55%, blue 60%, white 65%, red 70%, blue 75%, white 80%, red 85%, " + + "blue 90%, white 95% ) repeat scroll 0% 0%"; + +const EXPECTED_CSS_VALUE = LONG_CSS_VALUE.replace("95%", "95%, red"); + +const TEST_URI = + `<style> + .title { + background: ${LONG_CSS_VALUE}; + } + </style> + <h1 class=title>Header</h1>`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let { inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing the property editable field"); + let rule = getRuleViewRuleEditor(view, 1).rule; + let prop = rule.textProps[0]; + + // Calculate offsets to click in the middle of the first box quad. + let rect = prop.editor.valueSpan.getBoundingClientRect(); + let firstQuad = prop.editor.valueSpan.getBoxQuads()[0]; + // For a multiline value, the first quad left edge is not aligned with the + // bounding rect left edge. The offsets expected by focusEditableField are + // relative to the bouding rectangle, so we need to translate the x-offset. + let x = firstQuad.bounds.left - rect.left + firstQuad.bounds.width / 2; + // The first quad top edge is aligned with the bounding top edge, no + // translation needed here. + let y = firstQuad.bounds.height / 2; + + info("Focusing the css property editable value"); + let editor = yield focusEditableField(view, prop.editor.valueSpan, x, y); + + info("Moving the caret next to a number"); + let pos = editor.input.value.indexOf("0deg") + 1; + editor.input.setSelectionRange(pos, pos); + is(editor.input.value[editor.input.selectionStart - 1], "0", + "Input caret is after a 0"); + + info("Check that UP/DOWN navigates in the input, even when next to a number"); + EventUtils.synthesizeKey("VK_DOWN", {}, view.styleWindow); + ok(editor.input.selectionStart !== pos, "Input caret moved"); + is(editor.input.value, LONG_CSS_VALUE, "Input value was not decremented."); + + info("Move the caret to the end of the gradient definition."); + pos = editor.input.value.indexOf("95%") + 3; + editor.input.setSelectionRange(pos, pos); + + info("Sending \", re\" to the editable field."); + for (let key of ", re") { + yield synthesizeKeyForAutocomplete(key, editor, view.styleWindow); + } + + info("Check the autocomplete can still be displayed."); + ok(editor.popup && editor.popup.isOpen, "Autocomplete popup is displayed."); + is(editor.popup.selectedIndex, 0, + "Autocomplete has an item selected by default"); + + let item = editor.popup.getItemAtIndex(editor.popup.selectedIndex); + is(item.label, "rebeccapurple", + "Check autocomplete displays expected value."); + + info("Check autocomplete suggestions can be cycled using UP/DOWN arrows."); + + yield synthesizeKeyForAutocomplete("VK_DOWN", editor, view.styleWindow); + ok(editor.popup.selectedIndex, 1, "Using DOWN cycles autocomplete values."); + yield synthesizeKeyForAutocomplete("VK_DOWN", editor, view.styleWindow); + ok(editor.popup.selectedIndex, 2, "Using DOWN cycles autocomplete values."); + yield synthesizeKeyForAutocomplete("VK_UP", editor, view.styleWindow); + is(editor.popup.selectedIndex, 1, "Using UP cycles autocomplete values."); + item = editor.popup.getItemAtIndex(editor.popup.selectedIndex); + is(item.label, "red", "Check autocomplete displays expected value."); + + info("Select the background-color suggestion with a mouse click."); + let onRuleviewChanged = view.once("ruleview-changed"); + let onSuggest = editor.once("after-suggest"); + + let node = editor.popup._list.childNodes[editor.popup.selectedIndex]; + EventUtils.synthesizeMouseAtCenter(node, {}, editor.popup._window); + + view.throttle.flush(); + yield onSuggest; + yield onRuleviewChanged; + + is(editor.input.value, EXPECTED_CSS_VALUE, + "Input value correctly autocompleted"); + + info("Press ESCAPE to leave the input."); + onRuleviewChanged = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onRuleviewChanged; +}); + +/** + * Send the provided key to the currently focused input of the provided window. + * Wait for the editor to emit "after-suggest" to make sure the autocompletion + * process is finished. + * + * @param {String} key + * The key to send to the input. + * @param {InplaceEditor} editor + * The inplace editor which owns the focused input. + * @param {Window} win + * Window in which the key event will be dispatched. + */ +function* synthesizeKeyForAutocomplete(key, editor, win) { + let onSuggest = editor.once("after-suggest"); + EventUtils.synthesizeKey(key, {}, win); + yield onSuggest; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-popup-hidden-after-navigation.js b/devtools/client/inspector/rules/test/browser_rules_completion-popup-hidden-after-navigation.js new file mode 100644 index 000000000..84f119606 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_completion-popup-hidden-after-navigation.js @@ -0,0 +1,41 @@ +/* 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 ruleview autocomplete popup is hidden after page navigation. + +const TEST_URI = "<h1 style='font: 24px serif'></h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = yield openRuleView(); + + info("Test autocompletion popup is hidden after page navigation"); + + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing the css property editable field"); + let propertyName = view.styleDocument + .querySelectorAll(".ruleview-propertyname")[0]; + let editor = yield focusEditableField(view, propertyName); + + info("Pressing key VK_DOWN"); + let onSuggest = once(editor.input, "keypress"); + let onPopupOpened = once(editor.popup, "popup-opened"); + + EventUtils.synthesizeKey("VK_DOWN", {}, view.styleWindow); + + info("Waiting for autocomplete popup to be displayed"); + yield onSuggest; + yield onPopupOpened; + + ok(view.popup && view.popup.isOpen, "Popup should be opened"); + + info("Reloading the page"); + yield reloadPage(inspector, testActor); + + ok(!(view.popup && view.popup.isOpen), "Popup should be closed"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js b/devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js new file mode 100644 index 000000000..5acebd562 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js @@ -0,0 +1,47 @@ +/* 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 shows expanders for properties with computed lists. + +var TEST_URI = ` + <style type="text/css"> + #testid { + margin: 4px; + top: 0px; + } + </style> + <h1 id="testid">Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testExpandersShown(inspector, view); +}); + +function* testExpandersShown(inspector, view) { + let rule = getRuleViewRuleEditor(view, 1).rule; + + info("Check that the correct rules are visible"); + is(rule.selectorText, "#testid", "Second rule is #testid."); + is(rule.textProps[0].name, "margin", "First property is margin."); + is(rule.textProps[1].name, "top", "Second property is top."); + + info("Check that the expanders are shown correctly"); + is(rule.textProps[0].editor.expander.style.visibility, "visible", + "margin expander is visible."); + is(rule.textProps[1].editor.expander.style.visibility, "hidden", + "top expander is hidden."); + ok(!rule.textProps[0].editor.expander.hasAttribute("open"), + "margin computed list is closed."); + ok(!rule.textProps[1].editor.expander.hasAttribute("open"), + "top computed list is closed."); + ok(!rule.textProps[0].editor.computed.hasChildNodes(), + "margin computed list is empty before opening."); + ok(!rule.textProps[1].editor.computed.hasChildNodes(), + "top computed list is empty."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_computed-lists_02.js b/devtools/client/inspector/rules/test/browser_rules_computed-lists_02.js new file mode 100644 index 000000000..d6dc82d5f --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_computed-lists_02.js @@ -0,0 +1,74 @@ +/* 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 computed lists can be expanded/collapsed, +// and contain the right subproperties. + +var TEST_URI = ` + <style type="text/css"> + #testid { + margin: 0px 1px 2px 3px; + top: 0px; + } + </style> + <h1 id="testid">Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testComputedList(inspector, view); +}); + +function* testComputedList(inspector, view) { + let rule = getRuleViewRuleEditor(view, 1).rule; + let propEditor = rule.textProps[0].editor; + let expander = propEditor.expander; + + ok(!expander.hasAttribute("open"), "margin computed list is closed"); + + info("Opening the computed list of margin property"); + expander.click(); + ok(expander.hasAttribute("open"), "margin computed list is open"); + + let computed = propEditor.prop.computed; + let computedDom = propEditor.computed; + let propNames = [ + "margin-top", + "margin-right", + "margin-bottom", + "margin-left" + ]; + + is(computed.length, propNames.length, "There should be 4 computed values"); + is(computedDom.children.length, propNames.length, + "There should be 4 nodes in the DOM"); + + propNames.forEach((propName, i) => { + let propValue = i + "px"; + is(computed[i].name, propName, + "Computed property #" + i + " has name " + propName); + is(computed[i].value, propValue, + "Computed property #" + i + " has value " + propValue); + is(computedDom.querySelectorAll(".ruleview-propertyname")[i].textContent, + propName, + "Computed property #" + i + " in DOM has correct name"); + is(computedDom.querySelectorAll(".ruleview-propertyvalue")[i].textContent, + propValue, + "Computed property #" + i + " in DOM has correct value"); + }); + + info("Closing the computed list of margin property"); + expander.click(); + ok(!expander.hasAttribute("open"), "margin computed list is closed"); + + info("Opening the computed list of margin property"); + expander.click(); + ok(expander.hasAttribute("open"), "margin computed list is open"); + is(computed.length, propNames.length, "Still 4 computed values"); + is(computedDom.children.length, propNames.length, "Still 4 nodes in the DOM"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_content_01.js b/devtools/client/inspector/rules/test/browser_rules_content_01.js new file mode 100644 index 000000000..8695d9b8d --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_content_01.js @@ -0,0 +1,51 @@ +/* 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 content is correct + +const TEST_URI = ` + <style type="text/css"> + @media screen and (min-width: 10px) { + #testid { + background-color: blue; + } + } + .testclass, .unmatched { + background-color: green; + } + </style> + <div id="testid" class="testclass">Styled Node</div> + <div id="testid2">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); + is(view.element.querySelectorAll("#ruleview-no-results").length, 0, + "After a highlight, no longer has a no-results element."); + + yield clearCurrentNodeSelection(inspector); + is(view.element.querySelectorAll("#ruleview-no-results").length, 1, + "After highlighting null, has a no-results element again."); + + yield selectNode("#testid", inspector); + + let linkText = getRuleViewLinkTextByIndex(view, 1); + is(linkText, "inline:3 @screen and (min-width: 10px)", + "link text at index 1 contains media query text."); + + linkText = getRuleViewLinkTextByIndex(view, 2); + is(linkText, "inline:7", + "link text at index 2 contains no media query text."); + + let selector = getRuleViewRuleEditor(view, 2).selectorText; + is(selector.querySelector(".ruleview-selector-matched").textContent, + ".testclass", ".textclass should be matched."); + is(selector.querySelector(".ruleview-selector-unmatched").textContent, + ".unmatched", ".unmatched should not be matched."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_content_02.js b/devtools/client/inspector/rules/test/browser_rules_content_02.js new file mode 100644 index 000000000..253f374b4 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_content_02.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/ */ +/* globals getTestActorWithoutToolbox */ +"use strict"; + +// Test the rule-view content when the inspector gets opened via the page +// ctx-menu "inspect element" + +const CONTENT = ` + <body style="color:red;"> + <div style="color:blue;"> + <p style="color:green;"> + <span style="color:yellow;">test element</span> + </p> + </div> + </body> +`; + +add_task(function* () { + let tab = yield addTab("data:text/html;charset=utf-8," + CONTENT); + + let testActor = yield getTestActorWithoutToolbox(tab); + let inspector = yield clickOnInspectMenuItem(testActor, "span"); + + checkRuleViewContent(inspector.ruleview.view); +}); + +function checkRuleViewContent({styleDocument}) { + info("Making sure the rule-view contains the expected content"); + + let headers = [...styleDocument.querySelectorAll(".ruleview-header")]; + is(headers.length, 3, "There are 3 headers for inherited rules"); + + is(headers[0].textContent, + STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", "p"), + "The first header is correct"); + is(headers[1].textContent, + STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", "div"), + "The second header is correct"); + is(headers[2].textContent, + STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", "body"), + "The third header is correct"); + + let rules = styleDocument.querySelectorAll(".ruleview-rule"); + is(rules.length, 4, "There are 4 rules in the view"); + + for (let rule of rules) { + let selector = rule.querySelector(".ruleview-selectorcontainer"); + is(selector.textContent, STYLE_INSPECTOR_L10N.getStr("rule.sourceElement"), + "The rule's selector is correct"); + + let propertyNames = [...rule.querySelectorAll(".ruleview-propertyname")]; + is(propertyNames.length, 1, "There's only one property name, as expected"); + + let propertyValues = [...rule.querySelectorAll(".ruleview-propertyvalue")]; + is(propertyValues.length, 1, "There's only one property value, as expected"); + } +} + diff --git a/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-01.js b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-01.js new file mode 100644 index 000000000..b81bb8013 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-01.js @@ -0,0 +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/ */ + +/** + * This file tests the code that integrates the Style Inspector's rule view + * with the MDN docs tooltip. + * + * If you display the context click on a property name in the rule view, you + * should see a menu item "Show MDN Docs". If you click that item, the MDN + * docs tooltip should be shown, containing docs from MDN for that property. + * + * This file tests that the context menu item is shown when it should be + * shown and hidden when it should be hidden. + */ + +"use strict"; + +/** + * The test document tries to confuse the context menu + * code by having a tag called "padding" and a property + * value called "margin". + */ +const TEST_URI = ` + <html> + <head> + <style> + padding {font-family: margin;} + </style> + </head> + + <body> + <padding>MDN tooltip testing</padding> + </body> + </html> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("padding", inspector); + yield testMdnContextMenuItemVisibility(view); +}); + +/** + * Tests that the MDN context menu item is shown when it should be, + * and hidden when it should be. + * - iterate through every node in the rule view + * - set that node as popupNode (the node that the context menu + * is shown for) + * - update the context menu's state + * - test that the MDN context menu item is hidden, or not, + * depending on popupNode + */ +function* testMdnContextMenuItemVisibility(view) { + info("Test that MDN context menu item is shown only when it should be."); + + let root = rootElement(view); + for (let node of iterateNodes(root)) { + info("Setting " + node + " as popupNode"); + info("Creating context menu with " + node + " as popupNode"); + let allMenuItems = openStyleContextMenuAndGetAllItems(view, node); + let menuitemShowMdnDocs = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs")); + + let isVisible = menuitemShowMdnDocs.visible; + let shouldBeVisible = isPropertyNameNode(node); + let message = shouldBeVisible ? "shown" : "hidden"; + is(isVisible, shouldBeVisible, + "The MDN context menu item is " + message + " ; content : " + + node.textContent + " ; type : " + node.nodeType); + } +} + +/** + * Check if a node is a property name. + */ +function isPropertyNameNode(node) { + return node.textContent === "font-family"; +} + +/** + * A generator that iterates recursively through all child nodes of baseNode. + */ +function* iterateNodes(baseNode) { + yield baseNode; + + for (let child of baseNode.childNodes) { + yield* iterateNodes(child); + } +} + +/** + * Returns the root element for the rule view. + */ +var rootElement = view => (view.element) ? view.element : view.styleDocument; diff --git a/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js new file mode 100644 index 000000000..e0d08d28a --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js @@ -0,0 +1,61 @@ +/* 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/ */ + +/** + * This file tests the code that integrates the Style Inspector's rule view + * with the MDN docs tooltip. + * + * If you display the context click on a property name in the rule view, you + * should see a menu item "Show MDN Docs". If you click that item, the MDN + * docs tooltip should be shown, containing docs from MDN for that property. + * + * This file tests that: + * - clicking the context menu item shows the tooltip + * - the tooltip content matches the property name for which the context menu was opened + */ + +"use strict"; + +const {setBaseCssDocsUrl} = + require("devtools/client/shared/widgets/MdnDocsWidget"); + +const PROPERTYNAME = "color"; + +const TEST_DOC = ` + <html> + <body> + <div style="color: red"> + Test "Show MDN Docs" context menu option + </div> + </body> + </html> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_DOC)); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + + setBaseCssDocsUrl(URL_ROOT); + + info("Setting the popupNode for the MDN docs tooltip"); + + let {nameSpan} = getRuleViewProperty(view, "element", PROPERTYNAME); + + let allMenuItems = openStyleContextMenuAndGetAllItems(view, nameSpan.firstChild); + let menuitemShowMdnDocs = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs")); + + let cssDocs = view.tooltips.cssDocs; + + info("Showing the MDN docs tooltip"); + let onShown = cssDocs.tooltip.once("shown"); + menuitemShowMdnDocs.click(); + yield onShown; + ok(true, "The MDN docs tooltip was shown"); + + info("Quick check that the tooltip contents are set"); + let h1 = cssDocs.tooltip.container.querySelector(".mdn-property-name"); + is(h1.textContent, PROPERTYNAME, "The MDN docs tooltip h1 is correct"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-03.js b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-03.js new file mode 100644 index 000000000..d1089fcf6 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-03.js @@ -0,0 +1,118 @@ +/* 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/ */ + +/** + * This file tests the "devtools.inspector.mdnDocsTooltip.enabled" preference, + * that we use to enable/disable the MDN tooltip in the Inspector. + * + * The desired behavior is: + * - if the preference is true, show the "Show MDN Docs" context menu item + * - if the preference is false, don't show the item + * - listen for changes to the pref, so we can show/hide the item dynamically + */ + +"use strict"; + +const { PrefObserver } = require("devtools/client/styleeditor/utils"); +const PREF_ENABLE_MDN_DOCS_TOOLTIP = + "devtools.inspector.mdnDocsTooltip.enabled"; +const PROPERTY_NAME_CLASS = "ruleview-propertyname"; + +const TEST_DOC = ` + <html> + <body> + <div style="color: red"> + Test the pref to enable/disable the "Show MDN Docs" context menu option + </div> + </body> + </html> +`; + +add_task(function* () { + info("Ensure the pref is true to begin with"); + let initial = Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP); + if (initial != true) { + setBooleanPref(PREF_ENABLE_MDN_DOCS_TOOLTIP, true); + } + + yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_DOC)); + + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + yield testMdnContextMenuItemVisibility(view, true); + + yield setBooleanPref(PREF_ENABLE_MDN_DOCS_TOOLTIP, false); + yield testMdnContextMenuItemVisibility(view, false); + + info("Close the Inspector"); + let target = TargetFactory.forTab(gBrowser.selectedTab); + yield gDevTools.closeToolbox(target); + + ({inspector, view} = yield openRuleView()); + yield selectNode("div", inspector); + yield testMdnContextMenuItemVisibility(view, false); + + yield setBooleanPref(PREF_ENABLE_MDN_DOCS_TOOLTIP, true); + yield testMdnContextMenuItemVisibility(view, true); + + info("Ensure the pref is reset to its initial value"); + let eventual = Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP); + if (eventual != initial) { + setBooleanPref(PREF_ENABLE_MDN_DOCS_TOOLTIP, initial); + } +}); + +/** + * Set a boolean pref, and wait for the pref observer to + * trigger, so that code listening for the pref change + * has had a chance to update itself. + * + * @param pref {string} Name of the pref to change + * @param state {boolean} Desired value of the pref. + * + * Note that if the pref already has the value in `state`, + * then the prefObserver will not trigger. So you should only + * call this function if you know the pref's current value is + * not `state`. + */ +function* setBooleanPref(pref, state) { + let oncePrefChanged = defer(); + let prefObserver = new PrefObserver("devtools."); + prefObserver.on(pref, oncePrefChanged.resolve); + + info("Set the pref " + pref + " to: " + state); + Services.prefs.setBoolPref(pref, state); + + info("Wait for prefObserver to call back so the UI can update"); + yield oncePrefChanged.promise; + prefObserver.off(pref, oncePrefChanged.resolve); +} + +/** + * Test whether the MDN tooltip context menu item is visible when it should be. + * + * @param view The rule view + * @param shouldBeVisible {boolean} Whether we expect the context + * menu item to be visible or not. + */ +function* testMdnContextMenuItemVisibility(view, shouldBeVisible) { + let message = shouldBeVisible ? "shown" : "hidden"; + info("Test that MDN context menu item is " + message); + + info("Set a CSS property name as popupNode"); + let root = rootElement(view); + let node = root.querySelector("." + PROPERTY_NAME_CLASS).firstChild; + let allMenuItems = openStyleContextMenuAndGetAllItems(view, node); + let menuitemShowMdnDocs = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs")); + + let isVisible = menuitemShowMdnDocs.visible; + is(isVisible, shouldBeVisible, + "The MDN context menu item is " + message); +} + +/** + * Returns the root element for the rule view. + */ +var rootElement = view => (view.element) ? view.element : view.styleDocument; diff --git a/devtools/client/inspector/rules/test/browser_rules_copy_styles.js b/devtools/client/inspector/rules/test/browser_rules_copy_styles.js new file mode 100644 index 000000000..a6f991a60 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_copy_styles.js @@ -0,0 +1,307 @@ +/* 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 the behaviour of the copy styles context menu items in the rule + * view. + */ + +const osString = Services.appinfo.OS; + +const TEST_URI = URL_ROOT + "doc_copystyles.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let { inspector, view } = yield openRuleView(); + yield selectNode("#testid", inspector); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + let data = [ + { + desc: "Test Copy Property Name", + node: ruleEditor.rule.textProps[0].editor.nameSpan, + menuItemLabel: "styleinspector.contextmenu.copyPropertyName", + expectedPattern: "color", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: true, + copyPropertyValue: false, + copySelector: false, + copyRule: true + } + }, + { + desc: "Test Copy Property Value", + node: ruleEditor.rule.textProps[2].editor.valueSpan, + menuItemLabel: "styleinspector.contextmenu.copyPropertyValue", + expectedPattern: "12px", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: false, + copyPropertyValue: true, + copySelector: false, + copyRule: true + } + }, + { + desc: "Test Copy Property Value with Priority", + node: ruleEditor.rule.textProps[3].editor.valueSpan, + menuItemLabel: "styleinspector.contextmenu.copyPropertyValue", + expectedPattern: "#00F !important", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: false, + copyPropertyValue: true, + copySelector: false, + copyRule: true + } + }, + { + desc: "Test Copy Property Declaration", + node: ruleEditor.rule.textProps[2].editor.nameSpan, + menuItemLabel: "styleinspector.contextmenu.copyPropertyDeclaration", + expectedPattern: "font-size: 12px;", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: true, + copyPropertyValue: false, + copySelector: false, + copyRule: true + } + }, + { + desc: "Test Copy Property Declaration with Priority", + node: ruleEditor.rule.textProps[3].editor.nameSpan, + menuItemLabel: "styleinspector.contextmenu.copyPropertyDeclaration", + expectedPattern: "border-color: #00F !important;", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: true, + copyPropertyValue: false, + copySelector: false, + copyRule: true + } + }, + { + desc: "Test Copy Rule", + node: ruleEditor.rule.textProps[2].editor.nameSpan, + menuItemLabel: "styleinspector.contextmenu.copyRule", + expectedPattern: "#testid {[\\r\\n]+" + + "\tcolor: #F00;[\\r\\n]+" + + "\tbackground-color: #00F;[\\r\\n]+" + + "\tfont-size: 12px;[\\r\\n]+" + + "\tborder-color: #00F !important;[\\r\\n]+" + + "\t--var: \"\\*/\";[\\r\\n]+" + + "}", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: true, + copyPropertyValue: false, + copySelector: false, + copyRule: true + } + }, + { + desc: "Test Copy Selector", + node: ruleEditor.selectorText, + menuItemLabel: "styleinspector.contextmenu.copySelector", + expectedPattern: "html, body, #testid", + visible: { + copyLocation: false, + copyPropertyDeclaration: false, + copyPropertyName: false, + copyPropertyValue: false, + copySelector: true, + copyRule: true + } + }, + { + desc: "Test Copy Location", + node: ruleEditor.source, + menuItemLabel: "styleinspector.contextmenu.copyLocation", + expectedPattern: "http://example.com/browser/devtools/client/" + + "inspector/rules/test/doc_copystyles.css", + visible: { + copyLocation: true, + copyPropertyDeclaration: false, + copyPropertyName: false, + copyPropertyValue: false, + copySelector: false, + copyRule: true + } + }, + { + setup: function* () { + yield disableProperty(view, 0); + }, + desc: "Test Copy Rule with Disabled Property", + node: ruleEditor.rule.textProps[2].editor.nameSpan, + menuItemLabel: "styleinspector.contextmenu.copyRule", + expectedPattern: "#testid {[\\r\\n]+" + + "\t\/\\* color: #F00; \\*\/[\\r\\n]+" + + "\tbackground-color: #00F;[\\r\\n]+" + + "\tfont-size: 12px;[\\r\\n]+" + + "\tborder-color: #00F !important;[\\r\\n]+" + + "\t--var: \"\\*/\";[\\r\\n]+" + + "}", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: true, + copyPropertyValue: false, + copySelector: false, + copyRule: true + } + }, + { + setup: function* () { + yield disableProperty(view, 4); + }, + desc: "Test Copy Rule with Disabled Property with Comment", + node: ruleEditor.rule.textProps[2].editor.nameSpan, + menuItemLabel: "styleinspector.contextmenu.copyRule", + expectedPattern: "#testid {[\\r\\n]+" + + "\t\/\\* color: #F00; \\*\/[\\r\\n]+" + + "\tbackground-color: #00F;[\\r\\n]+" + + "\tfont-size: 12px;[\\r\\n]+" + + "\tborder-color: #00F !important;[\\r\\n]+" + + "\t/\\* --var: \"\\*\\\\\/\"; \\*\/[\\r\\n]+" + + "}", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: true, + copyPropertyValue: false, + copySelector: false, + copyRule: true + } + }, + { + desc: "Test Copy Property Declaration with Disabled Property", + node: ruleEditor.rule.textProps[0].editor.nameSpan, + menuItemLabel: "styleinspector.contextmenu.copyPropertyDeclaration", + expectedPattern: "\/\\* color: #F00; \\*\/", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: true, + copyPropertyValue: false, + copySelector: false, + copyRule: true + } + }, + ]; + + for (let { setup, desc, node, menuItemLabel, expectedPattern, visible } of data) { + if (setup) { + yield setup(); + } + + info(desc); + yield checkCopyStyle(view, node, menuItemLabel, expectedPattern, visible); + } +}); + +function* checkCopyStyle(view, node, menuItemLabel, expectedPattern, visible) { + let allMenuItems = openStyleContextMenuAndGetAllItems(view, node); + let menuItem = allMenuItems.find(item => + item.label === STYLE_INSPECTOR_L10N.getStr(menuItemLabel)); + let menuitemCopy = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy")); + let menuitemCopyLocation = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyLocation")); + let menuitemCopyPropertyDeclaration = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyDeclaration")); + let menuitemCopyPropertyName = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyName")); + let menuitemCopyPropertyValue = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyValue")); + let menuitemCopySelector = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copySelector")); + let menuitemCopyRule = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyRule")); + + ok(menuitemCopy.disabled, + "Copy disabled is as expected: true"); + ok(menuitemCopy.visible, + "Copy visible is as expected: true"); + + is(menuitemCopyLocation.visible, + visible.copyLocation, + "Copy Location visible attribute is as expected: " + + visible.copyLocation); + + is(menuitemCopyPropertyDeclaration.visible, + visible.copyPropertyDeclaration, + "Copy Property Declaration visible attribute is as expected: " + + visible.copyPropertyDeclaration); + + is(menuitemCopyPropertyName.visible, + visible.copyPropertyName, + "Copy Property Name visible attribute is as expected: " + + visible.copyPropertyName); + + is(menuitemCopyPropertyValue.visible, + visible.copyPropertyValue, + "Copy Property Value visible attribute is as expected: " + + visible.copyPropertyValue); + + is(menuitemCopySelector.visible, + visible.copySelector, + "Copy Selector visible attribute is as expected: " + + visible.copySelector); + + is(menuitemCopyRule.visible, + visible.copyRule, + "Copy Rule visible attribute is as expected: " + + visible.copyRule); + + try { + yield waitForClipboardPromise(() => menuItem.click(), + () => checkClipboardData(expectedPattern)); + } catch (e) { + failedClipboard(expectedPattern); + } +} + +function* disableProperty(view, index) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let textProp = ruleEditor.rule.textProps[index]; + yield togglePropStatus(view, textProp); +} + +function checkClipboardData(expectedPattern) { + let actual = SpecialPowers.getClipboardData("text/unicode"); + let expectedRegExp = new RegExp(expectedPattern, "g"); + return expectedRegExp.test(actual); +} + +function failedClipboard(expectedPattern) { + // Format expected text for comparison + let terminator = osString == "WINNT" ? "\r\n" : "\n"; + expectedPattern = expectedPattern.replace(/\[\\r\\n\][+*]/g, terminator); + expectedPattern = expectedPattern.replace(/\\\(/g, "("); + expectedPattern = expectedPattern.replace(/\\\)/g, ")"); + + let actual = SpecialPowers.getClipboardData("text/unicode"); + + // Trim the right hand side of our strings. This is because expectedPattern + // accounts for windows sometimes adding a newline to our copied data. + expectedPattern = expectedPattern.trimRight(); + actual = actual.trimRight(); + + ok(false, "Clipboard text does not match expected " + + "results (escaped for accurate comparison):\n"); + info("Actual: " + escape(actual)); + info("Expected: " + escape(expectedPattern)); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_css-docs-tooltip_closes-on-escape.js b/devtools/client/inspector/rules/test/browser_rules_css-docs-tooltip_closes-on-escape.js new file mode 100644 index 000000000..f386f45b4 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_css-docs-tooltip_closes-on-escape.js @@ -0,0 +1,51 @@ +/* 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/ */ + +/** + * Test that the CssDocs tooltip of the ruleview can be closed when pressing the Escape + * key. + */ + +"use strict"; + +const {setBaseCssDocsUrl} = + require("devtools/client/shared/widgets/MdnDocsWidget"); + +const PROPERTYNAME = "color"; + +const TEST_URI = ` + <html> + <body> + <div style="color: red"> + Test "Show MDN Docs" closes on escape + </div> + </body> + </html> +`; + +/** + * Test that the tooltip is hidden when we press Escape + */ +add_task(function* () { + yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + + setBaseCssDocsUrl(URL_ROOT); + + info("Retrieve a valid anchor for the CssDocs tooltip"); + let {nameSpan} = getRuleViewProperty(view, "element", PROPERTYNAME); + + info("Showing the MDN docs tooltip"); + let onShown = view.tooltips.cssDocs.tooltip.once("shown"); + view.tooltips.cssDocs.show(nameSpan, PROPERTYNAME); + yield onShown; + ok(true, "The MDN docs tooltip was shown"); + + info("Simulate pressing the 'Escape' key"); + let onHidden = view.tooltips.cssDocs.tooltip.once("hidden"); + EventUtils.sendKey("escape"); + yield onHidden; + ok(true, "The MDN docs tooltip was hidden on pressing 'escape'"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_cssom.js b/devtools/client/inspector/rules/test/browser_rules_cssom.js new file mode 100644 index 000000000..d20e85192 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_cssom.js @@ -0,0 +1,22 @@ +/* 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 to ensure that CSSOM doesn't make the rule view blow up. +// https://bugzilla.mozilla.org/show_bug.cgi?id=1224121 + +const TEST_URI = URL_ROOT + "doc_cssom.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield selectNode("#target", inspector); + + let elementStyle = view._elementStyle; + let rule = elementStyle.rules[1]; + + is(rule.textProps.length, 1, "rule should have one property"); + is(rule.textProps[0].name, "color", "the property should be 'color'"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_cubicbezier-appears-on-swatch-click.js b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-appears-on-swatch-click.js new file mode 100644 index 000000000..18099894b --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-appears-on-swatch-click.js @@ -0,0 +1,70 @@ +/* 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 cubic-bezier pickers appear when clicking on cubic-bezier +// swatches. + +const TEST_URI = ` + <style type="text/css"> + div { + animation: move 3s linear; + transition: top 4s cubic-bezier(.1, 1.45, 1, -1.2); + } + .test { + animation-timing-function: ease-in-out; + transition-timing-function: ease-out; + } + </style> + <div class="test">Testing the cubic-bezier tooltip!</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + + let swatches = []; + swatches.push( + getRuleViewProperty(view, "div", "animation").valueSpan + .querySelector(".ruleview-bezierswatch") + ); + swatches.push( + getRuleViewProperty(view, "div", "transition").valueSpan + .querySelector(".ruleview-bezierswatch") + ); + swatches.push( + getRuleViewProperty(view, ".test", "animation-timing-function").valueSpan + .querySelector(".ruleview-bezierswatch") + ); + swatches.push( + getRuleViewProperty(view, ".test", "transition-timing-function").valueSpan + .querySelector(".ruleview-bezierswatch") + ); + + for (let swatch of swatches) { + info("Testing that the cubic-bezier appears on cubicswatch click"); + yield testAppears(view, swatch); + } +}); + +function* testAppears(view, swatch) { + ok(swatch, "The cubic-swatch exists"); + + let bezier = view.tooltips.cubicBezier; + ok(bezier, "The rule-view has the expected cubicBezier property"); + + let bezierPanel = bezier.tooltip.panel; + ok(bezierPanel, "The XUL panel for the cubic-bezier tooltip exists"); + + let onBezierWidgetReady = bezier.once("ready"); + swatch.click(); + yield onBezierWidgetReady; + + ok(true, "The cubic-bezier tooltip was shown on click of the cibuc swatch"); + ok(!inplaceEditor(swatch.parentNode), + "The inplace editor wasn't shown as a result of the cibuc swatch click"); + yield hideTooltipAndWaitForRuleViewChanged(bezier, view); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.js b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.js new file mode 100644 index 000000000..5dc43d1c9 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.js @@ -0,0 +1,66 @@ +/* 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 a curve change in the cubic-bezier tooltip is committed when ENTER +// is pressed. + +const TEST_URI = ` + <style type="text/css"> + body { + transition: top 2s linear; + } + </style> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + + info("Getting the bezier swatch element"); + let swatch = getRuleViewProperty(view, "body", "transition").valueSpan + .querySelector(".ruleview-bezierswatch"); + + yield testPressingEnterCommitsChanges(swatch, view); +}); + +function* testPressingEnterCommitsChanges(swatch, ruleView) { + let bezierTooltip = ruleView.tooltips.cubicBezier; + + info("Showing the tooltip"); + let onBezierWidgetReady = bezierTooltip.once("ready"); + swatch.click(); + yield onBezierWidgetReady; + + let widget = yield bezierTooltip.widget; + info("Simulating a change of curve in the widget"); + widget.coordinates = [0.1, 2, 0.9, -1]; + let expected = "cubic-bezier(0.1, 2, 0.9, -1)"; + + yield waitForSuccess(function* () { + let func = yield getComputedStyleProperty("body", null, + "transition-timing-function"); + return func === expected; + }, "Waiting for the change to be previewed on the element"); + + ok(getRuleViewProperty(ruleView, "body", "transition").valueSpan.textContent + .indexOf("cubic-bezier(") !== -1, + "The text of the timing-function was updated"); + + info("Sending RETURN key within the tooltip document"); + // Pressing RETURN ends up doing 2 rule-view updates, one for the preview and + // one for the commit when the tooltip closes. + let onRuleViewChanged = waitForNEvents(ruleView, "ruleview-changed", 2); + focusAndSendKey(widget.parent.ownerDocument.defaultView, "RETURN"); + yield onRuleViewChanged; + + let style = yield getComputedStyleProperty("body", null, + "transition-timing-function"); + is(style, expected, "The element's timing-function was kept after RETURN"); + + let ruleViewStyle = getRuleViewProperty(ruleView, "body", "transition") + .valueSpan.textContent.indexOf("cubic-bezier(") !== -1; + ok(ruleViewStyle, "The text of the timing-function was kept after RETURN"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.js b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.js new file mode 100644 index 000000000..826d8a5aa --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.js @@ -0,0 +1,100 @@ +/* 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 changes made to the cubic-bezier timing-function in the +// cubic-bezier tooltip are reverted when ESC is pressed. + +const TEST_URI = ` + <style type='text/css'> + body { + animation-timing-function: linear; + } + </style> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + yield testPressingEscapeRevertsChanges(view); + yield testPressingEscapeRevertsChangesAndDisables(view); +}); + +function* testPressingEscapeRevertsChanges(view) { + let {propEditor} = yield openCubicBezierAndChangeCoords(view, 1, 0, + [0.1, 2, 0.9, -1], { + selector: "body", + name: "animation-timing-function", + value: "cubic-bezier(0.1, 2, 0.9, -1)" + }); + + is(propEditor.valueSpan.textContent, "cubic-bezier(.1,2,.9,-1)", + "Got expected property value."); + + yield escapeTooltip(view); + + yield waitForComputedStyleProperty("body", null, "animation-timing-function", + "linear"); + is(propEditor.valueSpan.textContent, "linear", + "Got expected property value."); +} + +function* testPressingEscapeRevertsChangesAndDisables(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let textProp = ruleEditor.rule.textProps[0]; + let propEditor = textProp.editor; + + info("Disabling animation-timing-function property"); + yield togglePropStatus(view, textProp); + + ok(propEditor.element.classList.contains("ruleview-overridden"), + "property is overridden."); + is(propEditor.enable.style.visibility, "visible", + "property enable checkbox is visible."); + ok(!propEditor.enable.getAttribute("checked"), + "property enable checkbox is not checked."); + ok(!propEditor.prop.enabled, + "animation-timing-function property is disabled."); + let newValue = yield getRulePropertyValue("animation-timing-function"); + is(newValue, "", "animation-timing-function should have been unset."); + + yield openCubicBezierAndChangeCoords(view, 1, 0, [0.1, 2, 0.9, -1]); + + yield escapeTooltip(view); + + ok(propEditor.element.classList.contains("ruleview-overridden"), + "property is overridden."); + is(propEditor.enable.style.visibility, "visible", + "property enable checkbox is visible."); + ok(!propEditor.enable.getAttribute("checked"), + "property enable checkbox is not checked."); + ok(!propEditor.prop.enabled, + "animation-timing-function property is disabled."); + newValue = yield getRulePropertyValue("animation-timing-function"); + is(newValue, "", "animation-timing-function should have been unset."); + is(propEditor.valueSpan.textContent, "linear", + "Got expected property value."); +} + +function* getRulePropertyValue(name) { + let propValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: name + }); + return propValue; +} + +function* escapeTooltip(view) { + info("Pressing ESCAPE to close the tooltip"); + + let bezierTooltip = view.tooltips.cubicBezier; + let widget = yield bezierTooltip.widget; + let onHidden = bezierTooltip.tooltip.once("hidden"); + let onModifications = view.once("ruleview-changed"); + focusAndSendKey(widget.parent.ownerDocument.defaultView, "ESCAPE"); + yield onHidden; + yield onModifications; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_custom.js b/devtools/client/inspector/rules/test/browser_rules_custom.js new file mode 100644 index 000000000..7c941af6f --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_custom.js @@ -0,0 +1,72 @@ +/* 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"; + +const TEST_URI = URL_ROOT + "doc_custom.html"; + +// Tests the display of custom declarations in the rule-view. + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + + yield simpleCustomOverride(inspector, view); + yield importantCustomOverride(inspector, view); + yield disableCustomOverride(inspector, view); +}); + +function* simpleCustomOverride(inspector, view) { + yield selectNode("#testidSimple", inspector); + + let idRule = getRuleViewRuleEditor(view, 1).rule; + let idRuleProp = idRule.textProps[0]; + + is(idRuleProp.name, "--background-color", + "First ID prop should be --background-color"); + ok(!idRuleProp.overridden, "ID prop should not be overridden."); + + let classRule = getRuleViewRuleEditor(view, 2).rule; + let classRuleProp = classRule.textProps[0]; + + is(classRuleProp.name, "--background-color", + "First class prop should be --background-color"); + ok(classRuleProp.overridden, "Class property should be overridden."); + + // Override --background-color by changing the element style. + let elementProp = yield addProperty(view, 0, "--background-color", "purple"); + + is(classRuleProp.name, "--background-color", + "First element prop should now be --background-color"); + ok(!elementProp.overridden, + "Element style property should not be overridden"); + ok(idRuleProp.overridden, "ID property should be overridden"); + ok(classRuleProp.overridden, "Class property should be overridden"); +} + +function* importantCustomOverride(inspector, view) { + yield selectNode("#testidImportant", inspector); + + let idRule = getRuleViewRuleEditor(view, 1).rule; + let idRuleProp = idRule.textProps[0]; + ok(idRuleProp.overridden, "Not-important rule should be overridden."); + + let classRule = getRuleViewRuleEditor(view, 2).rule; + let classRuleProp = classRule.textProps[0]; + ok(!classRuleProp.overridden, "Important rule should not be overridden."); +} + +function* disableCustomOverride(inspector, view) { + yield selectNode("#testidDisable", inspector); + + let idRule = getRuleViewRuleEditor(view, 1).rule; + let idRuleProp = idRule.textProps[0]; + + yield togglePropStatus(view, idRuleProp); + + let classRule = getRuleViewRuleEditor(view, 2).rule; + let classRuleProp = classRule.textProps[0]; + ok(!classRuleProp.overridden, + "Class prop should not be overridden after id prop was disabled."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_cycle-angle.js b/devtools/client/inspector/rules/test/browser_rules_cycle-angle.js new file mode 100644 index 000000000..fa135f937 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_cycle-angle.js @@ -0,0 +1,93 @@ +/* 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 cycling angle units in the rule view. + +const TEST_URI = ` + <style type="text/css"> + body { + image-orientation: 1turn; + } + div { + image-orientation: 180deg; + } + </style> + <body><div>Test</div>cycling angle units in the rule view!</body> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let container = getRuleViewProperty( + view, "body", "image-orientation").valueSpan; + yield checkAngleCycling(container, view); + yield checkAngleCyclingPersist(inspector, view); +}); + +function* checkAngleCycling(container, view) { + let valueNode = container.querySelector(".ruleview-angle"); + let win = view.styleWindow; + + // turn + is(valueNode.textContent, "1turn", "Angle displayed as a turn value."); + + let tests = [{ + value: "360deg", + comment: "Angle displayed as a degree value." + }, { + value: `${Math.round(Math.PI * 2 * 10000) / 10000}rad`, + comment: "Angle displayed as a radian value." + }, { + value: "400grad", + comment: "Angle displayed as a gradian value." + }, { + value: "1turn", + comment: "Angle displayed as a turn value again." + }]; + + for (let test of tests) { + yield checkSwatchShiftClick(container, win, test.value, test.comment); + } +} + +function* checkAngleCyclingPersist(inspector, view) { + yield selectNode("div", inspector); + let container = getRuleViewProperty( + view, "div", "image-orientation").valueSpan; + let valueNode = container.querySelector(".ruleview-angle"); + let win = view.styleWindow; + + is(valueNode.textContent, "180deg", "Angle displayed as a degree value."); + + yield checkSwatchShiftClick(container, win, + `${Math.round(Math.PI * 10000) / 10000}rad`, + "Angle displayed as a radian value."); + + // Select the body and reselect the div to see + // if the new angle unit persisted + yield selectNode("body", inspector); + yield selectNode("div", inspector); + + // We have to query for the container and the swatch because + // they've been re-generated + container = getRuleViewProperty(view, "div", "image-orientation").valueSpan; + valueNode = container.querySelector(".ruleview-angle"); + is(valueNode.textContent, `${Math.round(Math.PI * 10000) / 10000}rad`, + "Angle still displayed as a radian value."); +} + +function* checkSwatchShiftClick(container, win, expectedValue, comment) { + let swatch = container.querySelector(".ruleview-angleswatch"); + let valueNode = container.querySelector(".ruleview-angle"); + + let onUnitChange = swatch.once("unit-change"); + EventUtils.synthesizeMouseAtCenter(swatch, { + type: "mousedown", + shiftKey: true + }, win); + yield onUnitChange; + is(valueNode.textContent, expectedValue, comment); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_cycle-color.js b/devtools/client/inspector/rules/test/browser_rules_cycle-color.js new file mode 100644 index 000000000..e31ffa133 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_cycle-color.js @@ -0,0 +1,120 @@ +/* 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 cycling color types in the rule view. + +const TEST_URI = ` + <style type="text/css"> + body { + color: #f00; + } + span { + color: blue; + border-color: #ff000080; + } + </style> + <body><span>Test</span> cycling color types in the rule view!</body> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let container = getRuleViewProperty(view, "body", "color").valueSpan; + yield checkColorCycling(container, view); + yield checkAlphaColorCycling(inspector, view); + yield checkColorCyclingPersist(inspector, view); +}); + +function* checkColorCycling(container, view) { + let valueNode = container.querySelector(".ruleview-color"); + let win = view.styleWindow; + + // Hex + is(valueNode.textContent, "#f00", "Color displayed as a hex value."); + + let tests = [{ + value: "hsl(0, 100%, 50%)", + comment: "Color displayed as an HSL value." + }, { + value: "rgb(255, 0, 0)", + comment: "Color displayed as an RGB value." + }, { + value: "red", + comment: "Color displayed as a color name." + }, { + value: "#f00", + comment: "Color displayed as an authored value." + }, { + value: "hsl(0, 100%, 50%)", + comment: "Color displayed as an HSL value again." + }]; + + for (let test of tests) { + yield checkSwatchShiftClick(container, win, test.value, test.comment); + } +} + +function* checkAlphaColorCycling(inspector, view) { + yield selectNode("span", inspector); + let container = getRuleViewProperty(view, "span", "border-color").valueSpan; + let valueNode = container.querySelector(".ruleview-color"); + let win = view.styleWindow; + + is(valueNode.textContent, "#ff000080", + "Color displayed as an alpha hex value."); + + let tests = [{ + value: "hsla(0, 100%, 50%, 0.5)", + comment: "Color displayed as an HSLa value." + }, { + value: "rgba(255, 0, 0, 0.5)", + comment: "Color displayed as an RGBa value." + }, { + value: "#ff000080", + comment: "Color displayed as an alpha hex value again." + }]; + + for (let test of tests) { + yield checkSwatchShiftClick(container, win, test.value, test.comment); + } +} + +function* checkColorCyclingPersist(inspector, view) { + yield selectNode("span", inspector); + let container = getRuleViewProperty(view, "span", "color").valueSpan; + let valueNode = container.querySelector(".ruleview-color"); + let win = view.styleWindow; + + is(valueNode.textContent, "blue", "Color displayed as a color name."); + + yield checkSwatchShiftClick(container, win, "#00f", + "Color displayed as a hex value."); + + // Select the body and reselect the span to see + // if the new color unit persisted + yield selectNode("body", inspector); + yield selectNode("span", inspector); + + // We have to query for the container and the swatch because + // they've been re-generated + container = getRuleViewProperty(view, "span", "color").valueSpan; + valueNode = container.querySelector(".ruleview-color"); + is(valueNode.textContent, "#00f", + "Color is still displayed as a hex value."); +} + +function* checkSwatchShiftClick(container, win, expectedValue, comment) { + let swatch = container.querySelector(".ruleview-colorswatch"); + let valueNode = container.querySelector(".ruleview-color"); + + let onUnitChange = swatch.once("unit-change"); + EventUtils.synthesizeMouseAtCenter(swatch, { + type: "mousedown", + shiftKey: true + }, win); + yield onUnitChange; + is(valueNode.textContent, expectedValue, comment); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js b/devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js new file mode 100644 index 000000000..18522b527 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js @@ -0,0 +1,49 @@ +/* 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 toggling the grid highlighter in the rule view and modifying the 'display: grid' +// declaration. + +const TEST_URI = ` + <style type='text/css'> + #grid { + display: grid; + } + </style> + <div id="grid"> + <div id="cell1">cell1</div> + <div id="cell2">cell2</div> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + yield selectNode("#grid", inspector); + let container = getRuleViewProperty(view, "#grid", "display").valueSpan; + let gridToggle = container.querySelector(".ruleview-grid"); + + info("Toggling ON the CSS grid highlighter from the rule-view."); + let onHighlighterShown = highlighters.once("highlighter-shown"); + gridToggle.click(); + yield onHighlighterShown; + + info("Edit the 'grid' property value to 'block'."); + let editor = yield focusEditableField(view, container); + let onHighlighterHidden = highlighters.once("highlighter-hidden"); + let onDone = view.once("ruleview-changed"); + editor.input.value = "block;"; + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onHighlighterHidden; + yield onDone; + + info("Check the grid highlighter and grid toggle button are hidden."); + gridToggle = container.querySelector(".ruleview-grid"); + ok(!gridToggle, "Grid highlighter toggle is not visible."); + ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js new file mode 100644 index 000000000..af1a6fbc0 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js @@ -0,0 +1,46 @@ +/* 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 editing a property name or value and escaping will revert the +// changes and restore the original value. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: #00F; + } + </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(); + yield selectNode("#testid", inspector); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[0].editor; + + yield focusEditableField(view, propEditor.nameSpan); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, + ["DELETE", "ESCAPE"]); + + is(propEditor.nameSpan.textContent, "background-color", + "'background-color' property name is correctly set."); + is((yield getComputedStyleProperty("#testid", null, "background-color")), + "rgb(0, 0, 255)", "#00F background color is set."); + + yield focusEditableField(view, propEditor.valueSpan); + let onValueDeleted = view.once("ruleview-changed"); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, + ["DELETE", "ESCAPE"]); + yield onValueDeleted; + + is(propEditor.valueSpan.textContent, "#00F", + "'#00F' property value is correctly set."); + is((yield getComputedStyleProperty("#testid", null, "background-color")), + "rgb(0, 0, 255)", "#00F background color is set."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-click.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-click.js new file mode 100644 index 000000000..08a5ee786 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-click.js @@ -0,0 +1,61 @@ +/* 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 property name and value editors can be triggered when +// clicking on the property-name, the property-value, the colon or semicolon. + +const TEST_URI = ` + <style type='text/css'> + #testid { + margin: 0; + } + </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(); + yield selectNode("#testid", inspector); + yield testEditPropertyAndCancel(inspector, view); +}); + +function* testEditPropertyAndCancel(inspector, view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[0].editor; + + info("Test editor is created when clicking on property name"); + yield focusEditableField(view, propEditor.nameSpan); + ok(propEditor.nameSpan.inplaceEditor, "Editor created for property name"); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]); + + info("Test editor is created when clicking on ':' next to property name"); + let nameRect = propEditor.nameSpan.getBoundingClientRect(); + yield focusEditableField(view, propEditor.nameSpan, nameRect.width + 1); + ok(propEditor.nameSpan.inplaceEditor, "Editor created for property name"); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]); + + info("Test editor is created when clicking on property value"); + yield focusEditableField(view, propEditor.valueSpan); + ok(propEditor.valueSpan.inplaceEditor, "Editor created for property value"); + // When cancelling a value edition, the text-property-editor will trigger + // a modification to make sure the property is back to its original value + // => need to wait on "ruleview-changed" to avoid unhandled promises + let onRuleviewChanged = view.once("ruleview-changed"); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]); + yield onRuleviewChanged; + + info("Test editor is created when clicking on ';' next to property value"); + let valueRect = propEditor.valueSpan.getBoundingClientRect(); + yield focusEditableField(view, propEditor.valueSpan, valueRect.width + 1); + ok(propEditor.valueSpan.inplaceEditor, "Editor created for property value"); + // When cancelling a value edition, the text-property-editor will trigger + // a modification to make sure the property is back to its original value + // => need to wait on "ruleview-changed" to avoid unhandled promises + onRuleviewChanged = view.once("ruleview-changed"); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]); + yield onRuleviewChanged; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js new file mode 100644 index 000000000..8e16601c7 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js @@ -0,0 +1,92 @@ +/* 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 original value is correctly displayed when ESCaping out of the +// inplace editor in the style inspector. + +const TEST_URI = ` + <style type='text/css'> + #testid { + color: #00F; + } + </style> + <div id='testid'>Styled Node</div> +`; + +// Test data format +// { +// value: what char sequence to type, +// commitKey: what key to type to "commit" the change, +// modifiers: commitKey modifiers, +// expected: what value is expected as a result +// } +const testData = [ + { + value: "red", + commitKey: "VK_ESCAPE", + modifiers: {}, + expected: "#00F" + }, + { + value: "red", + commitKey: "VK_RETURN", + modifiers: {}, + expected: "red" + }, + { + value: "invalid", + commitKey: "VK_RETURN", + modifiers: {}, + expected: "invalid" + }, + { + value: "blue", + commitKey: "VK_TAB", modifiers: {shiftKey: true}, + expected: "blue" + } +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + for (let data of testData) { + yield runTestData(view, data); + } +}); + +function* runTestData(view, {value, commitKey, modifiers, expected}) { + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = idRuleEditor.rule.textProps[0].editor; + + info("Focusing the inplace editor field"); + + let editor = yield focusEditableField(view, propEditor.valueSpan); + is(inplaceEditor(propEditor.valueSpan), editor, + "Focused editor should be the value span."); + + info("Entering test data " + value); + let onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.sendString(value, view.styleWindow); + view.throttle.flush(); + yield onRuleViewChanged; + + info("Entering the commit key " + commitKey + " " + modifiers); + onRuleViewChanged = view.once("ruleview-changed"); + let onBlur = once(editor.input, "blur"); + EventUtils.synthesizeKey(commitKey, modifiers); + yield onBlur; + yield onRuleViewChanged; + + if (commitKey === "VK_ESCAPE") { + is(propEditor.valueSpan.textContent, expected, + "Value is as expected: " + expected); + } else { + is(propEditor.valueSpan.textContent, expected, + "Value is as expected: " + expected); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js new file mode 100644 index 000000000..ee0a1fa74 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js @@ -0,0 +1,89 @@ +/* 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 computed values of a style (the shorthand expansion) are +// properly updated after the style is changed. + +const TEST_URI = ` + <style type="text/css"> + #testid { + padding: 10px; + } + </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(); + yield selectNode("#testid", inspector); + yield editAndCheck(view); +}); + +function* editAndCheck(view) { + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let prop = idRuleEditor.rule.textProps[0]; + let propEditor = prop.editor; + let newPaddingValue = "20px"; + + info("Focusing the inplace editor field"); + let editor = yield focusEditableField(view, propEditor.valueSpan); + is(inplaceEditor(propEditor.valueSpan), editor, + "Focused editor should be the value span."); + + let onPropertyChange = waitForComputedStyleProperty("#testid", null, + "padding-top", newPaddingValue); + let onRefreshAfterPreview = once(view, "ruleview-changed"); + + info("Entering a new value"); + EventUtils.sendString(newPaddingValue, view.styleWindow); + + info("Waiting for the throttled previewValue to apply the " + + "changes to document"); + + view.throttle.flush(); + yield onPropertyChange; + + info("Waiting for ruleview-refreshed after previewValue was applied."); + yield onRefreshAfterPreview; + + let onBlur = once(editor.input, "blur"); + + info("Entering the commit key and finishing edit"); + EventUtils.synthesizeKey("VK_RETURN", {}); + + info("Waiting for blur on the field"); + yield onBlur; + + info("Waiting for the style changes to be applied"); + yield once(view, "ruleview-changed"); + + let computed = prop.computed; + let propNames = [ + "padding-top", + "padding-right", + "padding-bottom", + "padding-left" + ]; + + is(computed.length, propNames.length, "There should be 4 computed values"); + propNames.forEach((propName, i) => { + is(computed[i].name, propName, + "Computed property #" + i + " has name " + propName); + is(computed[i].value, newPaddingValue, + "Computed value of " + propName + " is as expected"); + }); + + propEditor.expander.click(); + let computedDom = propEditor.computed; + is(computedDom.children.length, propNames.length, + "There should be 4 nodes in the DOM"); + propNames.forEach((propName, i) => { + is(computedDom.getElementsByClassName("ruleview-propertyvalue")[i] + .textContent, newPaddingValue, + "Computed value of " + propName + " in DOM is as expected"); + }); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js new file mode 100644 index 000000000..ca63cedcc --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js @@ -0,0 +1,280 @@ +/* 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 increasing/decreasing values in rule view using +// arrow keys works correctly. + +// Bug 1275446 - This test happen to hit the default timeout on linux32 +requestLongerTimeout(2); + +const TEST_URI = ` + <style> + #test { + margin-top: 0px; + padding-top: 0px; + color: #000000; + background-color: #000000; + background: none; + transition: initial; + z-index: 0; + } + </style> + <div id="test"></div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + + let {inspector, view} = yield openRuleView(); + yield selectNode("#test", inspector); + + yield testMarginIncrements(view); + yield testVariousUnitIncrements(view); + yield testHexIncrements(view); + yield testAlphaHexIncrements(view); + yield testRgbIncrements(view); + yield testShorthandIncrements(view); + yield testOddCases(view); + yield testZeroValueIncrements(view); +}); + +function* testMarginIncrements(view) { + info("Testing keyboard increments on the margin property"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let marginPropEditor = idRuleEditor.rule.textProps[0].editor; + + yield runIncrementTest(marginPropEditor, view, { + 1: {alt: true, start: "0px", end: "0.1px", selectAll: true}, + 2: {start: "0px", end: "1px", selectAll: true}, + 3: {shift: true, start: "0px", end: "10px", selectAll: true}, + 4: {down: true, alt: true, start: "0.1px", end: "0px", selectAll: true}, + 5: {down: true, start: "0px", end: "-1px", selectAll: true}, + 6: {down: true, shift: true, start: "0px", end: "-10px", selectAll: true}, + 7: {pageUp: true, shift: true, start: "0px", end: "100px", selectAll: true}, + 8: {pageDown: true, shift: true, start: "0px", end: "-100px", + selectAll: true}, + 9: {start: "0", end: "1px", selectAll: true}, + 10: {down: true, start: "0", end: "-1px", selectAll: true}, + }); +} + +function* testVariousUnitIncrements(view) { + info("Testing keyboard increments on values with various units"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let paddingPropEditor = idRuleEditor.rule.textProps[1].editor; + + yield runIncrementTest(paddingPropEditor, view, { + 1: {start: "0px", end: "1px", selectAll: true}, + 2: {start: "0pt", end: "1pt", selectAll: true}, + 3: {start: "0pc", end: "1pc", selectAll: true}, + 4: {start: "0em", end: "1em", selectAll: true}, + 5: {start: "0%", end: "1%", selectAll: true}, + 6: {start: "0in", end: "1in", selectAll: true}, + 7: {start: "0cm", end: "1cm", selectAll: true}, + 8: {start: "0mm", end: "1mm", selectAll: true}, + 9: {start: "0ex", end: "1ex", selectAll: true}, + 10: {start: "0", end: "1px", selectAll: true}, + 11: {down: true, start: "0", end: "-1px", selectAll: true}, + }); +} + +function* testHexIncrements(view) { + info("Testing keyboard increments with hex colors"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let hexColorPropEditor = idRuleEditor.rule.textProps[2].editor; + + yield runIncrementTest(hexColorPropEditor, view, { + 1: {start: "#CCCCCC", end: "#CDCDCD", selectAll: true}, + 2: {shift: true, start: "#CCCCCC", end: "#DCDCDC", selectAll: true}, + 3: {start: "#CCCCCC", end: "#CDCCCC", selection: [1, 3]}, + 4: {shift: true, start: "#CCCCCC", end: "#DCCCCC", selection: [1, 3]}, + 5: {start: "#FFFFFF", end: "#FFFFFF", selectAll: true}, + 6: {down: true, shift: true, start: "#000000", end: "#000000", + selectAll: true} + }); +} + +function* testAlphaHexIncrements(view) { + info("Testing keyboard increments with alpha hex colors"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let hexColorPropEditor = idRuleEditor.rule.textProps[2].editor; + + yield runIncrementTest(hexColorPropEditor, view, { + 1: {start: "#CCCCCCAA", end: "#CDCDCDAB", selectAll: true}, + 2: {shift: true, start: "#CCCCCCAA", end: "#DCDCDCBA", selectAll: true}, + 3: {start: "#CCCCCCAA", end: "#CDCCCCAA", selection: [1, 3]}, + 4: {shift: true, start: "#CCCCCCAA", end: "#DCCCCCAA", selection: [1, 3]}, + 5: {start: "#FFFFFFFF", end: "#FFFFFFFF", selectAll: true}, + 6: {down: true, shift: true, start: "#00000000", end: "#00000000", + selectAll: true} + }); +} + +function* testRgbIncrements(view) { + info("Testing keyboard increments with rgb colors"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let rgbColorPropEditor = idRuleEditor.rule.textProps[3].editor; + + yield runIncrementTest(rgbColorPropEditor, view, { + 1: {start: "rgb(0,0,0)", end: "rgb(0,1,0)", selection: [6, 7]}, + 2: {shift: true, start: "rgb(0,0,0)", end: "rgb(0,10,0)", + selection: [6, 7]}, + 3: {start: "rgb(0,255,0)", end: "rgb(0,255,0)", selection: [6, 9]}, + 4: {shift: true, start: "rgb(0,250,0)", end: "rgb(0,255,0)", + selection: [6, 9]}, + 5: {down: true, start: "rgb(0,0,0)", end: "rgb(0,0,0)", selection: [6, 7]}, + 6: {down: true, shift: true, start: "rgb(0,5,0)", end: "rgb(0,0,0)", + selection: [6, 7]} + }); +} + +function* testShorthandIncrements(view) { + info("Testing keyboard increments within shorthand values"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let paddingPropEditor = idRuleEditor.rule.textProps[1].editor; + + yield runIncrementTest(paddingPropEditor, view, { + 1: {start: "0px 0px 0px 0px", end: "0px 1px 0px 0px", selection: [4, 7]}, + 2: {shift: true, start: "0px 0px 0px 0px", end: "0px 10px 0px 0px", + selection: [4, 7]}, + 3: {start: "0px 0px 0px 0px", end: "1px 0px 0px 0px", selectAll: true}, + 4: {shift: true, start: "0px 0px 0px 0px", end: "10px 0px 0px 0px", + selectAll: true}, + 5: {down: true, start: "0px 0px 0px 0px", end: "0px 0px -1px 0px", + selection: [8, 11]}, + 6: {down: true, shift: true, start: "0px 0px 0px 0px", + end: "-10px 0px 0px 0px", selectAll: true}, + 7: {up: true, start: "0.1em .1em 0em 0em", end: "0.1em 1.1em 0em 0em", + selection: [6, 9]}, + 8: {up: true, alt: true, start: "0.1em .9em 0em 0em", + end: "0.1em 1em 0em 0em", selection: [6, 9]}, + 9: {up: true, shift: true, start: "0.2em .2em 0em 0em", + end: "0.2em 10.2em 0em 0em", selection: [6, 9]} + }); +} + +function* testOddCases(view) { + info("Testing some more odd cases"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let marginPropEditor = idRuleEditor.rule.textProps[0].editor; + + yield runIncrementTest(marginPropEditor, view, { + 1: {start: "98.7%", end: "99.7%", selection: [3, 3]}, + 2: {alt: true, start: "98.7%", end: "98.8%", selection: [3, 3]}, + 3: {start: "0", end: "1px"}, + 4: {down: true, start: "0", end: "-1px"}, + 5: {start: "'a=-1'", end: "'a=0'", selection: [4, 4]}, + 6: {start: "0 -1px", end: "0 0px", selection: [2, 2]}, + 7: {start: "url(-1)", end: "url(-1)", selection: [4, 4]}, + 8: {start: "url('test1.1.png')", end: "url('test1.2.png')", + selection: [11, 11]}, + 9: {start: "url('test1.png')", end: "url('test2.png')", selection: [9, 9]}, + 10: {shift: true, start: "url('test1.1.png')", end: "url('test11.1.png')", + selection: [9, 9]}, + 11: {down: true, start: "url('test-1.png')", end: "url('test-2.png')", + selection: [9, 11]}, + 12: {start: "url('test1.1.png')", end: "url('test1.2.png')", + selection: [11, 12]}, + 13: {down: true, alt: true, start: "url('test-0.png')", + end: "url('test--0.1.png')", selection: [10, 11]}, + 14: {alt: true, start: "url('test--0.1.png')", end: "url('test-0.png')", + selection: [10, 14]} + }); +} + +function* testZeroValueIncrements(view) { + info("Testing a valid unit is added when incrementing from 0"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let backgroundPropEditor = idRuleEditor.rule.textProps[4].editor; + yield runIncrementTest(backgroundPropEditor, view, { + 1: { start: "url(test-0.png) no-repeat 0 0", + end: "url(test-0.png) no-repeat 1px 0", selection: [26, 26] }, + 2: { start: "url(test-0.png) no-repeat 0 0", + end: "url(test-0.png) no-repeat 0 1px", selection: [28, 28] }, + 3: { start: "url(test-0.png) no-repeat center/0", + end: "url(test-0.png) no-repeat center/1px", selection: [34, 34] }, + 4: { start: "url(test-0.png) no-repeat 0 0", + end: "url(test-1.png) no-repeat 0 0", selection: [10, 10] }, + 5: { start: "linear-gradient(0, red 0, blue 0)", + end: "linear-gradient(1deg, red 0, blue 0)", selection: [17, 17] }, + 6: { start: "linear-gradient(1deg, red 0, blue 0)", + end: "linear-gradient(1deg, red 1px, blue 0)", selection: [27, 27] }, + 7: { start: "linear-gradient(1deg, red 0, blue 0)", + end: "linear-gradient(1deg, red 0, blue 1px)", selection: [35, 35] }, + }); + + let transitionPropEditor = idRuleEditor.rule.textProps[5].editor; + yield runIncrementTest(transitionPropEditor, view, { + 1: { start: "all 0 ease-out", end: "all 1s ease-out", selection: [5, 5] }, + 2: { start: "margin 4s, color 0", + end: "margin 4s, color 1s", selection: [18, 18] }, + }); + + let zIndexPropEditor = idRuleEditor.rule.textProps[6].editor; + yield runIncrementTest(zIndexPropEditor, view, { + 1: {start: "0", end: "1", selection: [1, 1]}, + }); +} + +function* runIncrementTest(propertyEditor, view, tests) { + let editor = yield focusEditableField(view, propertyEditor.valueSpan); + + for (let test in tests) { + yield testIncrement(editor, tests[test], view, propertyEditor); + } + + // Blur the field to put back the UI in its initial state (and avoid pending + // requests when the test ends). + let onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + view.throttle.flush(); + yield onRuleViewChanged; +} + +function* testIncrement(editor, options, view) { + editor.input.value = options.start; + let input = editor.input; + + if (options.selectAll) { + input.select(); + } else if (options.selection) { + input.setSelectionRange(options.selection[0], options.selection[1]); + } + + is(input.value, options.start, "Value initialized at " + options.start); + + let onRuleViewChanged = view.once("ruleview-changed"); + let onKeyUp = once(input, "keyup"); + + let key; + key = options.down ? "VK_DOWN" : "VK_UP"; + if (options.pageDown) { + key = "VK_PAGE_DOWN"; + } else if (options.pageUp) { + key = "VK_PAGE_UP"; + } + + EventUtils.synthesizeKey(key, {altKey: options.alt, shiftKey: options.shift}, + view.styleWindow); + + yield onKeyUp; + + // Only expect a change if the value actually changed! + if (options.start !== options.end) { + view.throttle.flush(); + yield onRuleViewChanged; + } + + is(input.value, options.end, "Value changed to " + options.end); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-order.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-order.js new file mode 100644 index 000000000..b4a86c194 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-order.js @@ -0,0 +1,89 @@ +/* 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"; + +// Checking properties orders and overrides in the rule-view. + +const TEST_URI = "<style>#testid {}</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(); + yield selectNode("#testid", inspector); + + let elementStyle = view._elementStyle; + let elementRule = elementStyle.rules[1]; + + info("Checking rules insertion order and checking the applied style"); + let firstProp = yield addProperty(view, 1, "background-color", "green"); + let secondProp = yield addProperty(view, 1, "background-color", "blue"); + + is(elementRule.textProps[0], firstProp, + "Rules should be in addition order."); + is(elementRule.textProps[1], secondProp, + "Rules should be in addition order."); + + // rgb(0, 0, 255) = blue + is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)", + "Second property should have been used."); + + info("Removing the second property and checking the applied style again"); + yield removeProperty(view, secondProp); + // rgb(0, 128, 0) = green + is((yield getValue("#testid", "background-color")), "rgb(0, 128, 0)", + "After deleting second property, first should be used."); + + info("Creating a new second property and checking that the insertion order " + + "is still the same"); + + secondProp = yield addProperty(view, 1, "background-color", "blue"); + + is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)", + "New property should be used."); + is(elementRule.textProps[0], firstProp, + "Rules shouldn't have switched places."); + is(elementRule.textProps[1], secondProp, + "Rules shouldn't have switched places."); + + info("Disabling the second property and checking the applied style"); + yield togglePropStatus(view, secondProp); + + is((yield getValue("#testid", "background-color")), "rgb(0, 128, 0)", + "After disabling second property, first value should be used"); + + info("Disabling the first property too and checking the applied style"); + yield togglePropStatus(view, firstProp); + + is((yield getValue("#testid", "background-color")), "transparent", + "After disabling both properties, value should be empty."); + + info("Re-enabling the second propertyt and checking the applied style"); + yield togglePropStatus(view, secondProp); + + is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)", + "Value should be set correctly after re-enabling"); + + info("Re-enabling the first property and checking the insertion order " + + "is still respected"); + yield togglePropStatus(view, firstProp); + + is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)", + "Re-enabling an earlier property shouldn't make it override " + + "a later property."); + is(elementRule.textProps[0], firstProp, + "Rules shouldn't have switched places."); + is(elementRule.textProps[1], secondProp, + "Rules shouldn't have switched places."); + info("Modifying the first property and checking the applied style"); + yield setProperty(view, firstProp, "purple"); + + is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)", + "Modifying an earlier property shouldn't override a later property."); +}); + +function* getValue(selector, propName) { + let value = yield getComputedStyleProperty(selector, null, propName); + return value; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.js new file mode 100644 index 000000000..0aed2f5c8 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.js @@ -0,0 +1,67 @@ +/* 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 removing a property by clearing the property name and pressing the +// return key, and checks if the focus is moved to the appropriate editable +// field. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: #00F; + color: #00F; + } + </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(); + yield selectNode("#testid", inspector); + + info("Getting the first property in the #testid rule"); + let rule = getRuleViewRuleEditor(view, 1).rule; + let prop = rule.textProps[0]; + + info("Deleting the name of that property to remove the property"); + yield removeProperty(view, prop, false); + + let newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "background-color" + }); + is(newValue, "", "background-color should have been unset."); + + info("Getting the new first property in the rule"); + prop = rule.textProps[0]; + + let editor = inplaceEditor(view.styleDocument.activeElement); + is(inplaceEditor(prop.editor.nameSpan), editor, + "Focus should have moved to the next property name"); + + info("Deleting the name of that property to remove the property"); + view.styleDocument.activeElement.blur(); + yield removeProperty(view, prop, false); + + newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "color" + }); + is(newValue, "", "color should have been unset."); + + editor = inplaceEditor(view.styleDocument.activeElement); + is(inplaceEditor(rule.editor.newPropSpan), editor, + "Focus should have moved to the new property span"); + is(rule.textProps.length, 0, + "All properties should have been removed."); + is(rule.editor.propertyList.children.length, 1, + "Should have the new property span."); + + view.styleDocument.activeElement.blur(); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.js new file mode 100644 index 000000000..5690e7c2d --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.js @@ -0,0 +1,67 @@ +/* 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 removing a property by clearing the property value and pressing the +// return key, and checks if the focus is moved to the appropriate editable +// field. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: #00F; + color: #00F; + } + </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(); + yield selectNode("#testid", inspector); + + info("Getting the first property in the rule"); + let rule = getRuleViewRuleEditor(view, 1).rule; + let prop = rule.textProps[0]; + + info("Clearing the property value"); + yield setProperty(view, prop, null, false); + + let newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "background-color" + }); + is(newValue, "", "background-color should have been unset."); + + info("Getting the new first property in the rule"); + prop = rule.textProps[0]; + + let editor = inplaceEditor(view.styleDocument.activeElement); + is(inplaceEditor(prop.editor.nameSpan), editor, + "Focus should have moved to the next property name"); + view.styleDocument.activeElement.blur(); + + info("Clearing the property value"); + yield setProperty(view, prop, null, false); + + newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "color" + }); + is(newValue, "", "color should have been unset."); + + editor = inplaceEditor(view.styleDocument.activeElement); + is(inplaceEditor(rule.editor.newPropSpan), editor, + "Focus should have moved to the new property span"); + is(rule.textProps.length, 0, + "All properties should have been removed."); + is(rule.editor.propertyList.children.length, 1, + "Should have the new property span."); + + view.styleDocument.activeElement.blur(); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.js new file mode 100644 index 000000000..21a1063c2 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.js @@ -0,0 +1,83 @@ +/* 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 removing a property by clearing the property name and pressing shift +// and tab keys, and checks if the focus is moved to the appropriate editable +// field. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: #00F; + color: #00F; + } + </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(); + yield selectNode("#testid", inspector); + + info("Getting the second property in the rule"); + let rule = getRuleViewRuleEditor(view, 1).rule; + let prop = rule.textProps[1]; + + info("Clearing the property value and pressing shift-tab"); + let editor = yield focusEditableField(view, prop.editor.valueSpan); + let onValueDone = view.once("ruleview-changed"); + editor.input.value = ""; + EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, view.styleWindow); + yield onValueDone; + + let newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "color" + }); + is(newValue, "", "color should have been unset."); + is(prop.editor.valueSpan.textContent, "", + "'' property value is correctly set."); + + info("Pressing shift-tab again to focus the previous property value"); + let onValueFocused = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, view.styleWindow); + yield onValueFocused; + + info("Getting the first property in the rule"); + prop = rule.textProps[0]; + + editor = inplaceEditor(view.styleDocument.activeElement); + is(inplaceEditor(prop.editor.valueSpan), editor, + "Focus should have moved to the previous property value"); + + info("Pressing shift-tab again to focus the property name"); + let onNameFocused = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, view.styleWindow); + yield onNameFocused; + + info("Removing the name and pressing shift-tab to focus the selector"); + let onNameDeleted = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow); + EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, view.styleWindow); + yield onNameDeleted; + + newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "background-color" + }); + is(newValue, "", "background-color should have been unset."); + + editor = inplaceEditor(view.styleDocument.activeElement); + is(inplaceEditor(rule.editor.selectorText), editor, + "Focus should have moved to the selector text."); + is(rule.textProps.length, 0, + "All properties should have been removed."); + ok(!rule.editor.propertyList.hasChildNodes(), + "Should not have any properties."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_01.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_01.js new file mode 100644 index 000000000..6f4c49e20 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_01.js @@ -0,0 +1,93 @@ +/* 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"; + +// Testing adding new properties via the inplace-editors in the rule +// view. +// FIXME: some of the inplace-editor focus/blur/commit/revert stuff +// should be factored out in head.js + +const TEST_URI = ` + <style type="text/css"> + #testid { + color: red; + background-color: blue; + } + .testclass, .unmatched { + background-color: green; + } + </style> + <div id="testid" class="testclass">Styled Node</div> + <div id="testid2">Styled Node</div> +`; + +var BACKGROUND_IMAGE_URL = 'url("' + URL_ROOT + 'doc_test_image.png")'; + +var TEST_DATA = [ + { name: "border-color", value: "red", isValid: true }, + { name: "background-image", value: BACKGROUND_IMAGE_URL, isValid: true }, + { name: "border", value: "solid 1px foo", isValid: false }, +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + let rule = getRuleViewRuleEditor(view, 1).rule; + for (let {name, value, isValid} of TEST_DATA) { + yield testEditProperty(view, rule, name, value, isValid); + } +}); + +function* testEditProperty(view, rule, name, value, isValid) { + info("Test editing existing property name/value fields"); + + let doc = rule.editor.doc; + let prop = rule.textProps[0]; + + info("Focusing an existing property name in the rule-view"); + let editor = yield focusEditableField(view, prop.editor.nameSpan, 32, 1); + + is(inplaceEditor(prop.editor.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(rule.editor.element, "focus", true); + let onNameDone = view.once("ruleview-changed"); + EventUtils.sendString(name + ":", doc.defaultView); + yield onValueFocus; + yield onNameDone; + + // Getting the value editor after focus + editor = inplaceEditor(doc.activeElement); + input = editor.input; + is(inplaceEditor(prop.editor.valueSpan), editor, "Focus moved to the value."); + + info("Entering a new value, including ; to commit and blur the value"); + let onValueDone = view.once("ruleview-changed"); + let onBlur = once(input, "blur"); + EventUtils.sendString(value + ";", doc.defaultView); + yield onBlur; + yield onValueDone; + + is(prop.editor.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, + name + }); + + if (isValid) { + is(propValue, value, name + " should have been set."); + } else { + isnot(propValue, value, name + " shouldn't have been set."); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js new file mode 100644 index 000000000..7e6315236 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js @@ -0,0 +1,133 @@ +/* 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 several types of rule-view property edition + +const TEST_URI = ` + <style type="text/css"> + #testid { + background-color: blue; + } + .testclass, .unmatched { + background-color: green; + } + </style> + <div id="testid" class="testclass">Styled Node</div> + <div id="testid2">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 testEditProperty(inspector, view); + yield testDisableProperty(inspector, view); + yield testPropertyStillMarkedDirty(inspector, view); +}); + +function* testEditProperty(inspector, ruleView) { + let idRule = getRuleViewRuleEditor(ruleView, 1).rule; + let prop = idRule.textProps[0]; + + let editor = yield focusEditableField(ruleView, prop.editor.nameSpan); + let input = editor.input; + is(inplaceEditor(prop.editor.nameSpan), editor, + "Next focused editor should be the name editor."); + + ok(input.selectionStart === 0 && input.selectionEnd === input.value.length, + "Editor contents are selected."); + + // Try clicking on the editor's input again, shouldn't cause trouble + // (see bug 761665). + EventUtils.synthesizeMouse(input, 1, 1, {}, ruleView.styleWindow); + input.select(); + + info("Entering property name \"border-color\" followed by a colon to " + + "focus the value"); + let onNameDone = ruleView.once("ruleview-changed"); + let onFocus = once(idRule.editor.element, "focus", true); + EventUtils.sendString("border-color:", ruleView.styleWindow); + yield onFocus; + yield onNameDone; + + info("Verifying that the focused field is the valueSpan"); + editor = inplaceEditor(ruleView.styleDocument.activeElement); + input = editor.input; + is(inplaceEditor(prop.editor.valueSpan), editor, + "Focus should have moved to the value."); + ok(input.selectionStart === 0 && input.selectionEnd === input.value.length, + "Editor contents are selected."); + + info("Entering a value following by a semi-colon to commit it"); + let onBlur = once(editor.input, "blur"); + // Use sendChar() to pass each character as a string so that we can test + // prop.editor.warning.hidden after each character. + for (let ch of "red;") { + let onPreviewDone = ruleView.once("ruleview-changed"); + EventUtils.sendChar(ch, ruleView.styleWindow); + ruleView.throttle.flush(); + yield onPreviewDone; + is(prop.editor.warning.hidden, true, + "warning triangle is hidden or shown as appropriate"); + } + yield onBlur; + + let newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "border-color" + }); + is(newValue, "red", "border-color should have been set."); + + ruleView.styleDocument.activeElement.blur(); + yield addProperty(ruleView, 1, "color", "red", ";"); + + let props = ruleView.element.querySelectorAll(".ruleview-property"); + for (let i = 0; i < props.length; i++) { + is(props[i].hasAttribute("dirty"), i <= 1, + "props[" + i + "] marked dirty as appropriate"); + } +} + +function* testDisableProperty(inspector, ruleView) { + let idRule = getRuleViewRuleEditor(ruleView, 1).rule; + let prop = idRule.textProps[0]; + + info("Disabling a property"); + yield togglePropStatus(ruleView, prop); + + let newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "border-color" + }); + is(newValue, "", "Border-color should have been unset."); + + info("Enabling the property again"); + yield togglePropStatus(ruleView, prop); + + newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "border-color" + }); + is(newValue, "red", "Border-color should have been reset."); +} + +function* testPropertyStillMarkedDirty(inspector, ruleView) { + // Select an unstyled node. + yield selectNode("#testid2", inspector); + + // Select the original node again. + yield selectNode("#testid", inspector); + + let props = ruleView.element.querySelectorAll(".ruleview-property"); + for (let i = 0; i < props.length; i++) { + is(props[i].hasAttribute("dirty"), i <= 1, + "props[" + i + "] marked dirty as appropriate"); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_03.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_03.js new file mode 100644 index 000000000..a5771b41e --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_03.js @@ -0,0 +1,50 @@ +/* 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 emptying out an existing value removes the property and +// doesn't cause any other issues. See also Bug 1150780. + +const TEST_URI = ` + <style type="text/css"> + #testid { + color: red; + background-color: blue; + font-size: 12px; + } + .testclass, .unmatched { + background-color: green; + } + </style> + <div id="testid" class="testclass">Styled Node</div> + <div id="testid2">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); + + 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 onRuleViewChanged = view.once("ruleview-changed"); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, + ["DELETE", "RETURN"]); + yield onRuleViewChanged; + + info("Pressing enter a couple times to cycle through editors"); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["RETURN"]); + onRuleViewChanged = view.once("ruleview-changed"); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["RETURN"]); + yield onRuleViewChanged; + + 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"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_04.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_04.js new file mode 100644 index 000000000..7460db4cd --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_04.js @@ -0,0 +1,85 @@ +/* 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 a disabled property remains disabled when the escaping out of +// the property editor. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + } + </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(); + yield selectNode("#testid", inspector); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let prop = rule.textProps[0]; + + info("Disabling a property"); + yield togglePropStatus(view, prop); + + let newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "background-color" + }); + is(newValue, "", "background-color should have been unset."); + + yield testEditDisableProperty(view, rule, prop, "name", "VK_ESCAPE"); + yield testEditDisableProperty(view, rule, prop, "value", "VK_ESCAPE"); + yield testEditDisableProperty(view, rule, prop, "value", "VK_TAB"); + yield testEditDisableProperty(view, rule, prop, "value", "VK_RETURN"); +}); + +function* testEditDisableProperty(view, rule, prop, fieldType, commitKey) { + let field = fieldType === "name" ? prop.editor.nameSpan + : prop.editor.valueSpan; + + let editor = yield focusEditableField(view, field); + + ok(!prop.editor.element.classList.contains("ruleview-overridden"), + "property is not overridden."); + is(prop.editor.enable.style.visibility, "hidden", + "property enable checkbox is hidden."); + + let newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "background-color" + }); + is(newValue, "", "background-color should remain unset."); + + let onChangeDone; + if (fieldType === "value") { + onChangeDone = view.once("ruleview-changed"); + } + + let onBlur = once(editor.input, "blur"); + EventUtils.synthesizeKey(commitKey, {}, view.styleWindow); + yield onBlur; + yield onChangeDone; + + ok(!prop.enabled, "property is disabled."); + ok(prop.editor.element.classList.contains("ruleview-overridden"), + "property is overridden."); + is(prop.editor.enable.style.visibility, "visible", + "property enable checkbox is visible."); + ok(!prop.editor.enable.getAttribute("checked"), + "property enable checkbox is not checked."); + + newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "background-color" + }); + is(newValue, "", "background-color should remain unset."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_05.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_05.js new file mode 100644 index 000000000..3d37c81d5 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_05.js @@ -0,0 +1,77 @@ +/* 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 a disabled property is re-enabled if the property name or value is +// modified + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + } + </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(); + yield selectNode("#testid", inspector); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let prop = rule.textProps[0]; + + info("Disabling background-color property"); + yield togglePropStatus(view, prop); + + let newValue = yield getRulePropertyValue("background-color"); + is(newValue, "", "background-color should have been unset."); + + info("Entering a new property name, including : to commit and " + + "focus the value"); + + yield focusEditableField(view, prop.editor.nameSpan); + let onNameDone = view.once("ruleview-changed"); + EventUtils.sendString("border-color:", view.styleWindow); + yield onNameDone; + + info("Escape editing the property value"); + let onValueDone = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onValueDone; + + newValue = yield getRulePropertyValue("border-color"); + is(newValue, "blue", "border-color should have been set."); + + ok(prop.enabled, "border-color property is enabled."); + ok(!prop.editor.element.classList.contains("ruleview-overridden"), + "border-color is not overridden"); + + info("Disabling border-color property"); + yield togglePropStatus(view, prop); + + newValue = yield getRulePropertyValue("border-color"); + is(newValue, "", "border-color should have been unset."); + + info("Enter a new property value for the border-color property"); + yield setProperty(view, prop, "red"); + + newValue = yield getRulePropertyValue("border-color"); + is(newValue, "red", "new border-color should have been set."); + + ok(prop.enabled, "border-color property is enabled."); + ok(!prop.editor.element.classList.contains("ruleview-overridden"), + "border-color is not overridden"); +}); + +function* getRulePropertyValue(name) { + let propValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: name + }); + return propValue; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_06.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_06.js new file mode 100644 index 000000000..95211f1d0 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_06.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"; + +// Tests that editing a property's priority is behaving correctly, and disabling +// and editing the property will re-enable the property. + +const TEST_URI = ` + <style type='text/css'> + body { + background-color: green !important; + } + body { + background-color: red; + } + </style> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("body", inspector); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let prop = rule.textProps[0]; + + is((yield getComputedStyleProperty("body", null, "background-color")), + "rgb(0, 128, 0)", "green background color is set."); + + yield setProperty(view, prop, "red !important"); + + is(prop.editor.valueSpan.textContent, "red !important", + "'red !important' property value is correctly set."); + is((yield getComputedStyleProperty("body", null, "background-color")), + "rgb(255, 0, 0)", "red background color is set."); + + info("Disabling red background color property"); + yield togglePropStatus(view, prop); + + is((yield getComputedStyleProperty("body", null, "background-color")), + "rgb(0, 128, 0)", "green background color is set."); + + yield setProperty(view, prop, "red"); + + is(prop.editor.valueSpan.textContent, "red", + "'red' property value is correctly set."); + ok(prop.enabled, "red background-color property is enabled."); + is((yield getComputedStyleProperty("body", null, "background-color")), + "rgb(0, 128, 0)", "green background color is set."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_07.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_07.js new file mode 100644 index 000000000..40314819f --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_07.js @@ -0,0 +1,50 @@ +/* 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 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: #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(); + yield selectNode("#testid", inspector); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let prop = rule.textProps[0]; + + info("Disabling red background color property"); + yield togglePropStatus(view, prop); + ok(!prop.enabled, "red background-color property is disabled."); + + let editor = yield focusEditableField(view, prop.editor.valueSpan); + let onDone = view.once("ruleview-changed"); + editor.input.value = "red; color: red;"; + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onDone; + + is(prop.editor.valueSpan.textContent, "red", + "'red' property value is correctly set."); + ok(prop.enabled, "red background-color property is enabled."); + is((yield getComputedStyleProperty("#testid", null, "background-color")), + "rgb(255, 0, 0)", "red background color is set."); + + let propEditor = rule.textProps[1].editor; + is(propEditor.nameSpan.textContent, "color", + "new 'color' property name is correctly set."); + is(propEditor.valueSpan.textContent, "red", + "new 'red' property value is correctly set."); + is((yield getComputedStyleProperty("#testid", null, "color")), + "rgb(255, 0, 0)", "red color is set."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_08.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_08.js new file mode 100644 index 000000000..1becd40d9 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_08.js @@ -0,0 +1,57 @@ +/* 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 renaming a property works. + +const TEST_URI = ` + <style type="text/css"> + #testid { + color: #FFF; + } + </style> + <div style='color: red' id='testid'>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); + + info("Get the color property editor"); + let ruleEditor = getRuleViewRuleEditor(view, 0); + let propEditor = ruleEditor.rule.textProps[0].editor; + is(ruleEditor.rule.textProps[0].name, "color"); + + info("Focus the property name field"); + yield focusEditableField(ruleEditor.ruleView, propEditor.nameSpan, 32, 1); + + info("Rename the property to background-color"); + // Expect 3 events: the value editor being focused, the ruleview-changed event + // which signals that the new value has been previewed (fires once when the + // value gets focused), and the markupmutation event since we're modifying an + // inline style. + let onValueFocus = once(ruleEditor.element, "focus", true); + let onRuleViewChanged = ruleEditor.ruleView.once("ruleview-changed"); + let onMutation = inspector.once("markupmutation"); + EventUtils.sendString("background-color:", ruleEditor.doc.defaultView); + yield onValueFocus; + yield onRuleViewChanged; + yield onMutation; + + is(ruleEditor.rule.textProps[0].name, "background-color"); + yield waitForComputedStyleProperty("#testid", null, "background-color", + "rgb(255, 0, 0)"); + + is((yield getComputedStyleProperty("#testid", null, "color")), + "rgb(255, 255, 255)", "color is white"); + + // The value field is still focused. Blur it now and wait for the + // ruleview-changed event to avoid pending requests. + onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield onRuleViewChanged; +}); + diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_09.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_09.js new file mode 100644 index 000000000..51f714021 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_09.js @@ -0,0 +1,69 @@ +/* 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 a newProperty editor is only created if no other editor was +// previously displayed. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + } + </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(); + yield selectNode("#testid", inspector); + yield testClickOnEmptyAreaToCloseEditor(inspector, view); +}); + +function synthesizeMouseOnEmptyArea(ruleEditor, view) { + // any text property editor will do + let propEditor = ruleEditor.rule.textProps[0].editor; + let valueContainer = propEditor.valueContainer; + let valueRect = valueContainer.getBoundingClientRect(); + // click right next to the ";" at the end of valueContainer + EventUtils.synthesizeMouse(valueContainer, valueRect.width + 1, 1, {}, + view.styleWindow); +} + +function* testClickOnEmptyAreaToCloseEditor(inspector, view) { + // Start at the beginning: start to add a rule to the element's style + // declaration, add some text, then press escape. + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[0].editor; + + info("Create a property value editor"); + let editor = yield focusEditableField(view, propEditor.valueSpan); + ok(editor.input, "The inplace-editor field is ready"); + + info("Close the property value editor by clicking on an empty area " + + "in the rule editor"); + let onRuleViewChanged = view.once("ruleview-changed"); + let onBlur = once(editor.input, "blur"); + synthesizeMouseOnEmptyArea(ruleEditor, view); + yield onBlur; + yield onRuleViewChanged; + ok(!view.isEditing, "No inplace editor should be displayed in the ruleview"); + + info("Create new newProperty editor by clicking again on the empty area"); + let onFocus = once(ruleEditor.element, "focus", true); + synthesizeMouseOnEmptyArea(ruleEditor, view); + yield onFocus; + editor = inplaceEditor(ruleEditor.element.ownerDocument.activeElement); + is(inplaceEditor(ruleEditor.newPropSpan), editor, + "New property editor was created"); + + info("Close the newProperty editor by clicking again on the empty area"); + onBlur = once(editor.input, "blur"); + synthesizeMouseOnEmptyArea(ruleEditor, view); + yield onBlur; + + ok(!view.isEditing, "No inplace editor should be displayed in the ruleview"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js new file mode 100644 index 000000000..1846df60d --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js @@ -0,0 +1,88 @@ +/* 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"; + +// Testing ruleview inplace-editor is not blurred when clicking on the ruleview +// container scrollbar. + +const TEST_URI = ` + <style type="text/css"> + div.testclass { + color: black; + } + .a { + color: #aaa; + } + .b { + color: #bbb; + } + .c { + color: #ccc; + } + .d { + color: #ddd; + } + .e { + color: #eee; + } + .f { + color: #fff; + } + </style> + <div class="testclass a b c d e f">Styled Node</div> +`; + +add_task(function* () { + info("Toolbox height should be small enough to force scrollbars to appear"); + yield new Promise(done => { + let options = {"set": [ + ["devtools.toolbox.footer.height", 200], + ]}; + SpecialPowers.pushPrefEnv(options, done); + }); + + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode(".testclass", inspector); + + info("Check we have an overflow on the ruleview container."); + let container = view.element; + let hasScrollbar = container.offsetHeight < container.scrollHeight; + ok(hasScrollbar, "The rule view container should have a vertical scrollbar."); + + info("Focusing an existing selector name in the rule-view."); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + is(inplaceEditor(ruleEditor.selectorText), editor, + "The selector editor is focused."); + + info("Click on the scrollbar element."); + yield clickOnRuleviewScrollbar(view); + + is(editor.input, view.styleDocument.activeElement, + "The editor input should still be focused."); + + info("Check a new value can still be committed in the editable field"); + let newValue = ".testclass.a.b.c.d.e.f"; + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Enter new value and commit."); + editor.input.value = newValue; + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + ok(getRuleViewRule(view, newValue), "Rule with '" + newValue + " 'exists."); +}); + +function* clickOnRuleviewScrollbar(view) { + let container = view.element.parentNode; + let onScroll = once(container, "scroll"); + let rect = container.getBoundingClientRect(); + // click 5 pixels before the bottom-right corner should hit the scrollbar + EventUtils.synthesizeMouse(container, rect.width - 5, rect.height - 5, + {}, view.styleWindow); + yield onScroll; + + ok(true, "The rule view container scrolled after clicking on the scrollbar."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector-click.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click.js new file mode 100644 index 000000000..7a3b6d467 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click.js @@ -0,0 +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"; + +// Testing selector inplace-editor remains available and focused after clicking +// in its input. + +const TEST_URI = ` + <style type="text/css"> + .testclass { + text-align: center; + } + </style> + <div 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(".testclass", inspector); + yield testClickOnSelectorEditorInput(view); +}); + +function* testClickOnSelectorEditorInput(view) { + info("Test clicking inside the selector editor input"); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + let editorInput = editor.input; + is(inplaceEditor(ruleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Click inside the editor input"); + let onClick = once(editorInput, "click"); + EventUtils.synthesizeMouse(editor.input, 2, 1, {}, view.styleWindow); + yield onClick; + is(editor.input, view.styleDocument.activeElement, + "The editor input should still be focused"); + ok(!ruleEditor.newPropSpan, "No newProperty editor was created"); + + info("Doubleclick inside the editor input"); + let onDoubleClick = once(editorInput, "dblclick"); + EventUtils.synthesizeMouse(editor.input, 2, 1, { clickCount: 2 }, + view.styleWindow); + yield onDoubleClick; + is(editor.input, view.styleDocument.activeElement, + "The editor input should still be focused"); + ok(!ruleEditor.newPropSpan, "No newProperty editor was created"); + + info("Click outside the editor input"); + let onBlur = once(editorInput, "blur"); + let rect = editorInput.getBoundingClientRect(); + EventUtils.synthesizeMouse(editorInput, rect.width + 5, rect.height / 2, {}, + view.styleWindow); + yield onBlur; + + isnot(editorInput, view.styleDocument.activeElement, + "The editor input should no longer be focused"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js new file mode 100644 index 000000000..f7058371f --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js @@ -0,0 +1,117 @@ +/* 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 selector value is correctly displayed when committing the inplace editor +// with ENTER, ESC, SHIFT+TAB and TAB + +const TEST_URI = ` + <style type='text/css'> + #testid1 { + text-align: center; + } + #testid2 { + text-align: center; + } + #testid3 { + } + </style> + <div id='testid1'>Styled Node</div> + <div id='testid2'>Styled Node</div> + <div id='testid3'>Styled Node</div> +`; + +const TEST_DATA = [ + { + node: "#testid1", + value: ".testclass", + commitKey: "VK_ESCAPE", + modifiers: {}, + expected: "#testid1", + + }, + { + node: "#testid1", + value: ".testclass1", + commitKey: "VK_RETURN", + modifiers: {}, + expected: ".testclass1" + }, + { + node: "#testid2", + value: ".testclass2", + commitKey: "VK_TAB", + modifiers: {}, + expected: ".testclass2" + }, + { + node: "#testid3", + value: ".testclass3", + commitKey: "VK_TAB", + modifiers: {shiftKey: true}, + expected: ".testclass3" + } +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let { inspector, view } = yield openRuleView(); + + for (let data of TEST_DATA) { + yield runTestData(inspector, view, data); + } +}); + +function* runTestData(inspector, view, data) { + let {node, value, commitKey, modifiers, expected} = data; + + info("Updating " + node + " to " + value + " and committing with " + + commitKey + ". Expecting: " + expected); + + info("Selecting the test element"); + yield selectNode(node, inspector); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, idRuleEditor.selectorText); + is(inplaceEditor(idRuleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Enter the new selector value: " + value); + editor.input.value = value; + + info("Entering the commit key " + commitKey + " " + modifiers); + EventUtils.synthesizeKey(commitKey, modifiers); + + let activeElement = view.styleDocument.activeElement; + + if (commitKey === "VK_ESCAPE") { + is(idRuleEditor.rule.selectorText, expected, + "Value is as expected: " + expected); + is(idRuleEditor.isEditing, false, "Selector is not being edited."); + is(idRuleEditor.selectorText, activeElement, + "Focus is on selector span."); + return; + } + + yield once(view, "ruleview-changed"); + + ok(getRuleViewRule(view, expected), + "Rule with " + expected + " selector exists."); + + if (modifiers.shiftKey) { + idRuleEditor = getRuleViewRuleEditor(view, 0); + } + + let rule = idRuleEditor.rule; + if (rule.textProps.length > 0) { + is(inplaceEditor(rule.textProps[0].editor.nameSpan).input, activeElement, + "Focus is on the first property name span."); + } else { + is(inplaceEditor(idRuleEditor.newPropSpan).input, activeElement, + "Focus is on the new property span."); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_01.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_01.js new file mode 100644 index 000000000..af228094b --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_01.js @@ -0,0 +1,62 @@ +/* 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"; + +// Testing selector inplace-editor behaviors in the rule-view + +const TEST_URI = ` + <style type="text/css"> + .testclass { + text-align: center; + } + </style> + <div id="testid" class="testclass">Styled Node</div> + <span>This is a span</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode("#testid", inspector); + yield testEditSelector(view, "span"); + + info("Selecting the modified element with the new rule"); + yield selectNode("span", inspector); + yield checkModifiedElement(view, "span"); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector fields"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, idRuleEditor.selectorText); + + is(inplaceEditor(idRuleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name and committing"); + editor.input.value = name; + + info("Waiting for rule view to update"); + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); + ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"), + "Rule with " + name + " does not match the current element."); +} + +function* checkModifiedElement(view, name) { + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js new file mode 100644 index 000000000..503f91efa --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js @@ -0,0 +1,88 @@ +/* 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"; + +// Testing selector inplace-editor behaviors in the rule-view with pseudo +// classes. + +const TEST_URI = ` + <style type="text/css"> + .testclass { + text-align: center; + } + #testid3::first-letter { + text-decoration: "italic" + } + </style> + <div id="testid">Styled Node</div> + <span class="testclass">This is a span</span> + <div class="testclass2">A</div> + <div id="testid3">B</div> +`; + +const PSEUDO_PREF = "devtools.inspector.show_pseudo_elements"; + +add_task(function* () { + // Expand the pseudo-elements section by default. + Services.prefs.setBoolPref(PSEUDO_PREF, true); + + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode(".testclass", inspector); + yield testEditSelector(view, "div:nth-child(1)"); + + info("Selecting the modified element"); + yield selectNode("#testid", inspector); + yield checkModifiedElement(view, "div:nth-child(1)"); + + info("Selecting the test element"); + yield selectNode("#testid3", inspector); + yield testEditSelector(view, ".testclass2::first-letter"); + + info("Selecting the modified element"); + yield selectNode(".testclass2", inspector); + yield checkModifiedElement(view, ".testclass2::first-letter"); + + // Reset the pseudo-elements section pref to its default value. + Services.prefs.clearUserPref(PSEUDO_PREF); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector fields"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1) || + getRuleViewRuleEditor(view, 1, 0); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, idRuleEditor.selectorText); + + is(inplaceEditor(idRuleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name: " + name); + editor.input.value = name; + + info("Waiting for rule view to update"); + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + is(view._elementStyle.rules.length, 2, "Should have 2 rule."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); + + let newRuleEditor = getRuleViewRuleEditor(view, 1) || + getRuleViewRuleEditor(view, 1, 0); + ok(newRuleEditor.element.getAttribute("unmatched"), + "Rule with " + name + " does not match the current element."); +} + +function* checkModifiedElement(view, name) { + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_03.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_03.js new file mode 100644 index 000000000..c6834f6ee --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_03.js @@ -0,0 +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"; + +// Testing selector inplace-editor behaviors in the rule-view with invalid +// selectors + +const TEST_URI = ` + <style type="text/css"> + .testclass { + text-align: center; + } + </style> + <div 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(".testclass", inspector); + yield testEditSelector(view, "asd@:::!"); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector fields"); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + is(inplaceEditor(ruleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name and committing"); + editor.input.value = name; + let onRuleViewChanged = once(view, "ruleview-invalid-selector"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + is(getRuleViewRule(view, name), undefined, + "Rule with " + name + " selector should not exist."); + ok(getRuleViewRule(view, ".testclass"), + "Rule with .testclass selector exists."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js new file mode 100644 index 000000000..09b6ad841 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js @@ -0,0 +1,69 @@ +/* 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 selector highlighter is removed when modifying a selector and +// the selector highlighter works for the newly added unmatched rule. + +const TEST_URI = ` + <style type="text/css"> + p { + background: red; + } + </style> + <p>Test the selector highlighter</p> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("p", inspector); + + ok(!view.selectorHighlighter, + "No selectorhighlighter exist in the rule-view"); + + yield testSelectorHighlight(view, "p"); + yield testEditSelector(view, "body"); + yield testSelectorHighlight(view, "body"); +}); + +function* testSelectorHighlight(view, name) { + info("Test creating selector highlighter"); + + info("Clicking on a selector icon"); + let icon = getRuleViewSelectorHighlighterIcon(view, name); + + let onToggled = view.once("ruleview-selectorhighlighter-toggled"); + EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow); + let isVisible = yield onToggled; + + ok(view.selectorHighlighter, "The selectorhighlighter instance was created"); + ok(isVisible, "The toggle event says the highlighter is visible"); +} + +function* testEditSelector(view, name) { + info("Test editing existing selector fields"); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + is(inplaceEditor(ruleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Waiting for rule view to update"); + let onToggled = view.once("ruleview-selectorhighlighter-toggled"); + + info("Entering a new selector name and committing"); + editor.input.value = name; + EventUtils.synthesizeKey("VK_RETURN", {}); + + let isVisible = yield onToggled; + + ok(!view.highlighters.selectorHighlighterShown, + "The selectorHighlighterShown instance was removed"); + ok(!isVisible, "The toggle event says the highlighter is not visible"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_05.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_05.js new file mode 100644 index 000000000..cd996b4b0 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_05.js @@ -0,0 +1,78 @@ +/* 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 adding a new property of an unmatched rule works properly. + +const TEST_URI = ` + <style type="text/css"> + #testid { + } + .testclass { + background-color: white; + } + </style> + <div id="testid">Styled Node</div> + <span class="testclass">This is a span</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode("#testid", inspector); + yield testEditSelector(view, "span"); + yield testAddProperty(view); + + info("Selecting the modified element with the new rule"); + yield selectNode("span", inspector); + yield checkModifiedElement(view, "span"); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector fields"); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + is(inplaceEditor(ruleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name and committing"); + editor.input.value = name; + + info("Waiting for rule view to update"); + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); + ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"), + "Rule with " + name + " does not match the current element."); + + // Escape the new property editor after editing the selector + let onBlur = once(view.styleDocument.activeElement, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onBlur; +} + +function* checkModifiedElement(view, name) { + is(view._elementStyle.rules.length, 3, "Should have 3 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); +} + +function* testAddProperty(view) { + info("Test creating a new property"); + let textProp = yield addProperty(view, 1, "text-align", "center"); + + is(textProp.value, "center", "Text prop should have been changed."); + ok(!textProp.overridden, "Property should not be overridden"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_06.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_06.js new file mode 100644 index 000000000..7d782a309 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_06.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"; + +// Testing selector inplace-editor behaviors in the rule-view with unmatched +// selectors + +const TEST_URI = ` + <style type="text/css"> + .testclass { + text-align: center; + } + div { + } + </style> + <div 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(".testclass", inspector); + yield testEditClassSelector(view); + yield testEditDivSelector(view); +}); + +function* testEditClassSelector(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + editor.input.value = "body"; + let onRuleViewChanged = once(view, "ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + // Get the new rule editor that replaced the original + ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[0].editor; + + info("Check that the correct rules are visible"); + is(view._elementStyle.rules.length, 3, "Should have 3 rules."); + ok(ruleEditor.element.getAttribute("unmatched"), "Rule editor is unmatched."); + is(getRuleViewRule(view, ".testclass"), undefined, + "Rule with .testclass selector should not exist."); + ok(getRuleViewRule(view, "body"), + "Rule with body selector exists."); + is(inplaceEditor(propEditor.nameSpan), + inplaceEditor(view.styleDocument.activeElement), + "Focus should have moved to the property name."); +} + +function* testEditDivSelector(view) { + let ruleEditor = getRuleViewRuleEditor(view, 2); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + editor.input.value = "asdf"; + let onRuleViewChanged = once(view, "ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + // Get the new rule editor that replaced the original + ruleEditor = getRuleViewRuleEditor(view, 2); + + info("Check that the correct rules are visible"); + is(view._elementStyle.rules.length, 3, "Should have 3 rules."); + ok(ruleEditor.element.getAttribute("unmatched"), "Rule editor is unmatched."); + is(getRuleViewRule(view, "div"), undefined, + "Rule with div selector should not exist."); + ok(getRuleViewRule(view, "asdf"), + "Rule with asdf selector exists."); + is(inplaceEditor(ruleEditor.newPropSpan), + inplaceEditor(view.styleDocument.activeElement), + "Focus should have moved to the property name."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js new file mode 100644 index 000000000..81c7aad72 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the rule view overridden search filter does not appear for an +// unmatched rule. + +const TEST_URI = ` + <style type="text/css"> + div { + height: 0px; + } + #testid { + height: 1px; + } + .testclass { + height: 10px; + } + </style> + <div id="testid">Styled Node</div> + <span class="testclass">This is a span</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + yield selectNode("#testid", inspector); + yield testEditSelector(view, "span"); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector fields"); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + is(inplaceEditor(ruleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name and committing"); + editor.input.value = name; + + info("Entering the commit key"); + let onRuleViewChanged = once(view, "ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + // Get the new rule editor that replaced the original + ruleEditor = getRuleViewRuleEditor(view, 1); + let rule = ruleEditor.rule; + let textPropEditor = rule.textProps[0].editor; + + is(view._elementStyle.rules.length, 3, "Should have 3 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); + ok(ruleEditor.element.getAttribute("unmatched"), + "Rule with " + name + " does not match the current element."); + ok(textPropEditor.filterProperty.hidden, "Overridden search is hidden."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js new file mode 100644 index 000000000..33382e0de --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js @@ -0,0 +1,71 @@ +/* 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 reverting a selector edit does the right thing. +// Bug 1241046. + +const TEST_URI = ` + <style type="text/css"> + span { + color: chartreuse; + } + </style> + <span> + <div id="testid" class="testclass">Styled Node</div> + </span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode("#testid", inspector); + + let idRuleEditor = getRuleViewRuleEditor(view, 2); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, idRuleEditor.selectorText); + + is(inplaceEditor(idRuleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name and committing"); + editor.input.value = "pre"; + + info("Waiting for rule view to update"); + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + info("Re-focusing the selector name in the rule-view"); + idRuleEditor = getRuleViewRuleEditor(view, 2); + editor = yield focusEditableField(view, idRuleEditor.selectorText); + + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, "pre"), "Rule with pre selector exists."); + is(getRuleViewRuleEditor(view, 2).element.getAttribute("unmatched"), + "true", + "Rule with pre does not match the current element."); + + // Now change it back. + info("Re-entering original selector name and committing"); + editor.input.value = "span"; + + info("Waiting for rule view to update"); + onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, "span"), "Rule with span selector exists."); + is(getRuleViewRuleEditor(view, 2).element.getAttribute("unmatched"), + "false", "Rule with span matches the current element."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_09.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_09.js new file mode 100644 index 000000000..a18ddc5ef --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_09.js @@ -0,0 +1,110 @@ +/* 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 editing a selector to an unmatched rule does set up the correct +// property on the rule, and that settings property in said rule does not +// lead to overriding properties from matched rules. +// Test that having a rule with both matched and unmatched selectors does work +// correctly. + +const TEST_URI = ` + <style type="text/css"> + #testid { + color: black; + } + .testclass { + background-color: white; + } + </style> + <div id="testid">Styled Node</div> + <span class="testclass">This is a span</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + yield selectNode("#testid", inspector); + yield testEditSelector(view, "span"); + yield testAddImportantProperty(view); + yield testAddMatchedRule(view, "span, div"); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector fields"); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + is(inplaceEditor(ruleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name and committing"); + editor.input.value = name; + + info("Waiting for rule view to update"); + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); + ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"), + "Rule with " + name + " does not match the current element."); + + // Escape the new property editor after editing the selector + let onBlur = once(view.styleDocument.activeElement, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onBlur; +} + +function* testAddImportantProperty(view) { + info("Test creating a new property with !important"); + let textProp = yield addProperty(view, 1, "color", "red !important"); + + is(textProp.value, "red", "Text prop should have been changed."); + is(textProp.priority, "important", + "Text prop has an \"important\" priority."); + ok(!textProp.overridden, "Property should not be overridden"); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + let prop = ruleEditor.rule.textProps[0]; + ok(!prop.overridden, + "Existing property on matched rule should not be overridden"); +} + +function* testAddMatchedRule(view, name) { + info("Test adding a matching selector"); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + is(inplaceEditor(ruleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name and committing"); + editor.input.value = name; + + info("Waiting for rule view to update"); + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + is(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"), "false", + "Rule with " + name + " does match the current element."); + + // Escape the new property editor after editing the selector + let onBlur = once(view.styleDocument.activeElement, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onBlur; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js new file mode 100644 index 000000000..d878dd516 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js @@ -0,0 +1,64 @@ +/* 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"; + +// Regression test for bug 1293616: make sure that editing a selector +// keeps the rule in the proper position. + +const TEST_URI = ` + <style type="text/css"> + #testid span, #testid p { + background: aqua; + } + span { + background: fuchsia; + } + </style> + <div id="testid"> + <span class="pickme"> + Styled Node + </span> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode(".pickme", inspector); + yield testEditSelector(view); +}); + +function* testEditSelector(view) { + 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; + + // Escape the new property editor after editing the selector + let onBlur = once(view.styleDocument.activeElement, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onBlur; + + // Get the new rule editor that replaced the original + ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Check that the correct rules are visible"); + is(view._elementStyle.rules.length, 3, "Should have 3 rules."); + is(ruleEditor.element.getAttribute("unmatched"), "false", "Rule editor is matched."); + + let props = ruleEditor.rule.textProps; + is(props.length, 1, "Rule has correct number of properties"); + is(props[0].name, "background", "Found background property"); + ok(!props[0].overridden, "Background property is not overridden"); + + ruleEditor = getRuleViewRuleEditor(view, 2); + props = ruleEditor.rule.textProps; + is(props.length, 1, "Rule has correct number of properties"); + is(props[0].name, "background", "Found background property"); + ok(props[0].overridden, "Background property is overridden"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js new file mode 100644 index 000000000..9a1bdc8fa --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js @@ -0,0 +1,69 @@ +/* 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"; + +// Regression test for bug 1293616, where editing a selector should +// change the relative priority of the rule. + +const TEST_URI = ` + <style type="text/css"> + #testid { + background: aqua; + } + .pickme { + background: seagreen; + } + span { + background: fuchsia; + } + </style> + <div> + <span id="testid" class="pickme"> + Styled Node + </span> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode(".pickme", inspector); + yield testEditSelector(view); +}); + +function* testEditSelector(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + editor.input.value = ".pickme"; + let onRuleViewChanged = once(view, "ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + // Escape the new property editor after editing the selector + let onBlur = once(view.styleDocument.activeElement, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onBlur; + + // Get the new rule editor that replaced the original + ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Check that the correct rules are visible"); + is(view._elementStyle.rules.length, 4, "Should have 4 rules."); + is(ruleEditor.element.getAttribute("unmatched"), "false", "Rule editor is matched."); + + let props = ruleEditor.rule.textProps; + is(props.length, 1, "Rule has correct number of properties"); + is(props[0].name, "background", "Found background property"); + is(props[0].value, "aqua", "Background property is aqua"); + ok(props[0].overridden, "Background property is overridden"); + + ruleEditor = getRuleViewRuleEditor(view, 2); + props = ruleEditor.rule.textProps; + is(props.length, 1, "Rule has correct number of properties"); + is(props[0].name, "background", "Found background property"); + is(props[0].value, "seagreen", "Background property is seagreen"); + ok(!props[0].overridden, "Background property is not overridden"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js new file mode 100644 index 000000000..dbf59cba9 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js @@ -0,0 +1,107 @@ +/* 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 clicking on swatch-preceeded value while editing the property name +// will result in editing the property value. Also tests that the value span is updated +// only if the property name has changed. See also Bug 1248274. + +const TEST_URI = ` + <style type="text/css"> + #testid { + color: red; + } + </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(); + + yield selectNode("#testid", inspector); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[0].editor; + + yield testColorValueSpanClickWithoutNameChange(propEditor, view); + yield testColorValueSpanClickAfterNameChange(propEditor, view); +}); + +function* testColorValueSpanClickWithoutNameChange(propEditor, view) { + info("Test click on color span while focusing property name editor"); + let colorSpan = propEditor.valueSpan.querySelector(".ruleview-color"); + + info("Focus the color name span"); + yield focusEditableField(view, propEditor.nameSpan); + let editor = inplaceEditor(propEditor.doc.activeElement); + + // We add a click event to make sure the color span won't be cleared + // on nameSpan blur (which would lead to the click event not being triggered) + let onColorSpanClick = once(colorSpan, "click"); + + // The property-value-updated is emitted when the valueSpan markup is being + // re-populated, which should not be the case when not modifying the property name + let onPropertyValueUpdated = function () { + ok(false, "The \"property-value-updated\" should not be emitted"); + }; + view.on("property-value-updated", onPropertyValueUpdated); + + info("blur propEditor.nameSpan by clicking on the color span"); + EventUtils.synthesizeMouse(colorSpan, 1, 1, {}, propEditor.doc.defaultView); + + info("wait for the click event on the color span"); + yield onColorSpanClick; + ok(true, "Expected click event was emitted"); + + editor = inplaceEditor(propEditor.doc.activeElement); + is(inplaceEditor(propEditor.valueSpan), editor, + "The property value editor got focused"); + + // We remove this listener in order to not cause unwanted conflict in the next test + view.off("property-value-updated", onPropertyValueUpdated); + + info("blur valueSpan editor to trigger ruleview-changed event and prevent " + + "having pending request"); + let onRuleViewChanged = view.once("ruleview-changed"); + editor.input.blur(); + yield onRuleViewChanged; +} + +function* testColorValueSpanClickAfterNameChange(propEditor, view) { + info("Test click on color span after property name change"); + let colorSpan = propEditor.valueSpan.querySelector(".ruleview-color"); + + info("Focus the color name span"); + yield focusEditableField(view, propEditor.nameSpan); + let editor = inplaceEditor(propEditor.doc.activeElement); + + info("Modify the property to border-color to trigger the " + + "property-value-updated event"); + editor.input.value = "border-color"; + + let onRuleViewChanged = view.once("ruleview-changed"); + let onPropertyValueUpdate = view.once("property-value-updated"); + + info("blur propEditor.nameSpan by clicking on the color span"); + EventUtils.synthesizeMouse(colorSpan, 1, 1, {}, propEditor.doc.defaultView); + + info("wait for ruleview-changed event to be triggered to prevent pending requests"); + yield onRuleViewChanged; + + info("wait for the property value to be updated"); + yield onPropertyValueUpdate; + ok(true, "Expected \"property-value-updated\" event was emitted"); + + editor = inplaceEditor(propEditor.doc.activeElement); + is(inplaceEditor(propEditor.valueSpan), editor, + "The property value editor got focused"); + + info("blur valueSpan editor to trigger ruleview-changed event and prevent " + + "having pending request"); + onRuleViewChanged = view.once("ruleview-changed"); + editor.input.blur(); + yield onRuleViewChanged; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js new file mode 100644 index 000000000..372ed7477 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js @@ -0,0 +1,65 @@ +/* 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 hitting shift + click on color swatch while editing the property +// name will only change the color unit and not lead to edit the property value. +// See also Bug 1248274. + +const TEST_URI = ` + <style type="text/css"> + #testid { + color: red; + background: linear-gradient( + 90deg, + rgb(183,222,237), + rgb(33,180,226), + rgb(31,170,217), + rgba(200,170,140,0.5)); + } + </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(); + + info("Test shift + click on color swatch while editing property name"); + + yield selectNode("#testid", inspector); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[1].editor; + let swatchSpan = propEditor.valueSpan.querySelectorAll(".ruleview-colorswatch")[2]; + + info("Focus the background name span"); + yield focusEditableField(view, propEditor.nameSpan); + let editor = inplaceEditor(propEditor.doc.activeElement); + + info("Modify the property to background-image to trigger the " + + "property-value-updated event"); + editor.input.value = "background-image"; + + let onPropertyValueUpdate = view.once("property-value-updated"); + let onSwatchUnitChange = swatchSpan.once("unit-change"); + let onRuleViewChanged = view.once("ruleview-changed"); + + info("blur propEditor.nameSpan by clicking on the color swatch"); + EventUtils.synthesizeMouseAtCenter(swatchSpan, {shiftKey: true}, + propEditor.doc.defaultView); + + info("wait for ruleview-changed event to be triggered to prevent pending requests"); + yield onRuleViewChanged; + + info("wait for the color unit to change"); + yield onSwatchUnitChange; + ok(true, "the color unit was changed"); + + info("wait for the property value to be updated"); + yield onPropertyValueUpdate; + + ok(!inplaceEditor(propEditor.valueSpan), "The inplace editor wasn't shown " + + "as a result of the color swatch shift + click"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js new file mode 100644 index 000000000..041a45a3e --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js @@ -0,0 +1,69 @@ +/* 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 clicking on color swatch while editing the property name +// will show the color tooltip with the correct value. See also Bug 1248274. + +const TEST_URI = ` + <style type="text/css"> + #testid { + color: red; + background: linear-gradient( + 90deg, + rgb(183,222,237), + rgb(33,180,226), + rgb(31,170,217), + rgba(200,170,140,0.5)); + } + </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(); + + info("Test click on color swatch while editing property name"); + + yield selectNode("#testid", inspector); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[1].editor; + let swatchSpan = propEditor.valueSpan.querySelectorAll( + ".ruleview-colorswatch")[3]; + let colorPicker = view.tooltips.colorPicker; + + info("Focus the background name span"); + yield focusEditableField(view, propEditor.nameSpan); + let editor = inplaceEditor(propEditor.doc.activeElement); + + info("Modify the background property to background-image to trigger the " + + "property-value-updated event"); + editor.input.value = "background-image"; + + let onRuleViewChanged = view.once("ruleview-changed"); + let onPropertyValueUpdate = view.once("property-value-updated"); + let onReady = colorPicker.once("ready"); + + info("blur propEditor.nameSpan by clicking on the color swatch"); + EventUtils.synthesizeMouseAtCenter(swatchSpan, {}, + propEditor.doc.defaultView); + + info("wait for ruleview-changed event to be triggered to prevent pending requests"); + yield onRuleViewChanged; + + info("wait for the property value to be updated"); + yield onPropertyValueUpdate; + + info("wait for the color picker to be shown"); + yield onReady; + + ok(true, "The color picker was shown on click of the color swatch"); + ok(!inplaceEditor(propEditor.valueSpan), + "The inplace editor wasn't shown as a result of the color swatch click"); + + let spectrum = colorPicker.spectrum; + is(spectrum.rgb, "200,170,140,0.5", "The correct color picker was shown"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.js b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.js new file mode 100644 index 000000000..fa4d8e6e2 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.js @@ -0,0 +1,62 @@ +/* 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 clicking on a property's value URL while editing the property name +// will open the link in a new tab. See also Bug 1248274. + +const TEST_URI = ` + <style type="text/css"> + #testid { + background: url("chrome://global/skin/icons/warning-64.png"), linear-gradient(white, #F06 400px); + } + </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(); + + info("Test click on background-image url while editing property name"); + + yield selectNode("#testid", inspector); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[0].editor; + let anchor = propEditor.valueSpan.querySelector(".ruleview-propertyvalue .theme-link"); + + info("Focus the background name span"); + yield focusEditableField(view, propEditor.nameSpan); + let editor = inplaceEditor(propEditor.doc.activeElement); + + info("Modify the property to background to trigger the " + + "property-value-updated event"); + editor.input.value = "background-image"; + + let onRuleViewChanged = view.once("ruleview-changed"); + let onPropertyValueUpdate = view.once("property-value-updated"); + let onTabOpened = waitForTab(); + + info("blur propEditor.nameSpan by clicking on the link"); + // The url can be wrapped across multiple lines, and so we click the lower left corner + // of the anchor to make sure to target the link. + let rect = anchor.getBoundingClientRect(); + EventUtils.synthesizeMouse(anchor, 2, rect.height - 2, {}, propEditor.doc.defaultView); + + info("wait for ruleview-changed event to be triggered to prevent pending requests"); + yield onRuleViewChanged; + + info("wait for the property value to be updated"); + yield onPropertyValueUpdate; + + info("wait for the image to be open in a new tab"); + let tab = yield onTabOpened; + ok(true, "A new tab opened"); + + is(tab.linkedBrowser.currentURI.spec, anchor.href, + "The URL for the new tab is correct"); + + gBrowser.removeTab(tab); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js new file mode 100644 index 000000000..c9c7cd3d2 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js @@ -0,0 +1,94 @@ +/* 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 correct editable fields are focused when tabbing and entering +// through the rule view. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + color: red; + margin: 0; + padding: 0; + } + div { + border-color: red + } + </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(); + yield selectNode("#testid", inspector); + yield testEditableFieldFocus(inspector, view, "VK_RETURN"); + yield testEditableFieldFocus(inspector, view, "VK_TAB"); +}); + +function* testEditableFieldFocus(inspector, view, commitKey) { + info("Click on the selector of the inline style ('element')"); + let ruleEditor = getRuleViewRuleEditor(view, 0); + let onFocus = once(ruleEditor.element, "focus", true); + ruleEditor.selectorText.click(); + yield onFocus; + assertEditor(view, ruleEditor.newPropSpan, + "Focus should be in the element property span"); + + info("Focus the next field with " + commitKey); + ruleEditor = getRuleViewRuleEditor(view, 1); + yield focusNextEditableField(view, ruleEditor, commitKey); + assertEditor(view, ruleEditor.selectorText, + "Focus should have moved to the next rule selector"); + + for (let i = 0; i < ruleEditor.rule.textProps.length; i++) { + let textProp = ruleEditor.rule.textProps[i]; + let propEditor = textProp.editor; + + info("Focus the next field with " + commitKey); + // Expect a ruleview-changed event if we are moving from a property value + // to the next property name (which occurs after the first iteration, as for + // i=0, the previous field is the selector). + let onRuleViewChanged = i > 0 ? view.once("ruleview-changed") : null; + yield focusNextEditableField(view, ruleEditor, commitKey); + yield onRuleViewChanged; + assertEditor(view, propEditor.nameSpan, + "Focus should have moved to the property name"); + + info("Focus the next field with " + commitKey); + yield focusNextEditableField(view, ruleEditor, commitKey); + assertEditor(view, propEditor.valueSpan, + "Focus should have moved to the property value"); + } + + // Expect a ruleview-changed event again as we're bluring a property value. + let onRuleViewChanged = view.once("ruleview-changed"); + yield focusNextEditableField(view, ruleEditor, commitKey); + yield onRuleViewChanged; + assertEditor(view, ruleEditor.newPropSpan, + "Focus should have moved to the new property span"); + + ruleEditor = getRuleViewRuleEditor(view, 2); + + yield focusNextEditableField(view, ruleEditor, commitKey); + assertEditor(view, ruleEditor.selectorText, + "Focus should have moved to the next rule selector"); + + info("Blur the selector field"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); +} + +function* focusNextEditableField(view, ruleEditor, commitKey) { + let onFocus = once(ruleEditor.element, "focus", true); + EventUtils.synthesizeKey(commitKey, {}, view.styleWindow); + yield onFocus; +} + +function assertEditor(view, element, message) { + let editor = inplaceEditor(view.styleDocument.activeElement); + is(inplaceEditor(element), editor, message); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.js b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.js new file mode 100644 index 000000000..13ad221f0 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.js @@ -0,0 +1,84 @@ +/* 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 correct editable fields are focused when shift tabbing +// through the rule view. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + color: red; + margin: 0; + padding: 0; + } + div { + border-color: red + } + </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(); + yield selectNode("#testid", inspector); + yield testEditableFieldFocus(inspector, view, "VK_TAB", { shiftKey: true }); +}); + +function* testEditableFieldFocus(inspector, view, commitKey, options = {}) { + let ruleEditor = getRuleViewRuleEditor(view, 2); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + is(inplaceEditor(ruleEditor.selectorText), editor, + "Focus should be in the 'div' rule selector"); + + ruleEditor = getRuleViewRuleEditor(view, 1); + + yield focusNextField(view, ruleEditor, commitKey, options); + assertEditor(view, ruleEditor.newPropSpan, + "Focus should have moved to the new property span"); + + for (let textProp of ruleEditor.rule.textProps.slice(0).reverse()) { + let propEditor = textProp.editor; + + yield focusNextField(view, ruleEditor, commitKey, options); + yield assertEditor(view, propEditor.valueSpan, + "Focus should have moved to the property value"); + + yield focusNextFieldAndExpectChange(view, ruleEditor, commitKey, options); + yield assertEditor(view, propEditor.nameSpan, + "Focus should have moved to the property name"); + } + + ruleEditor = getRuleViewRuleEditor(view, 1); + + yield focusNextField(view, ruleEditor, commitKey, options); + yield assertEditor(view, ruleEditor.selectorText, + "Focus should have moved to the '#testid' rule selector"); + + ruleEditor = getRuleViewRuleEditor(view, 0); + + yield focusNextField(view, ruleEditor, commitKey, options); + assertEditor(view, ruleEditor.newPropSpan, + "Focus should have moved to the new property span"); +} + +function* focusNextFieldAndExpectChange(view, ruleEditor, commitKey, options) { + let onRuleViewChanged = view.once("ruleview-changed"); + yield focusNextField(view, ruleEditor, commitKey, options); + yield onRuleViewChanged; +} + +function* focusNextField(view, ruleEditor, commitKey, options) { + let onFocus = once(ruleEditor.element, "focus", true); + EventUtils.synthesizeKey(commitKey, options, view.styleWindow); + yield onFocus; +} + +function* assertEditor(view, element, message) { + let editor = inplaceEditor(view.styleDocument.activeElement); + is(inplaceEditor(element), editor, message); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_eyedropper.js b/devtools/client/inspector/rules/test/browser_rules_eyedropper.js new file mode 100644 index 000000000..0762066e3 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_eyedropper.js @@ -0,0 +1,123 @@ +/* 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 opening the eyedropper from the color picker. Pressing escape to close it, and +// clicking the page to select a color. + +const TEST_URI = ` + <style type="text/css"> + body { + background-color: white; + padding: 0px + } + + #div1 { + background-color: #ff5; + width: 20px; + height: 20px; + } + + #div2 { + margin-left: 20px; + width: 20px; + height: 20px; + background-color: #f09; + } + </style> + <body><div id="div1"></div><div id="div2"></div></body> +`; + +// #f09 +const ORIGINAL_COLOR = "rgb(255, 0, 153)"; +// #ff5 +const EXPECTED_COLOR = "rgb(255, 255, 85)"; + +add_task(function* () { + info("Add the test tab, open the rule-view and select the test node"); + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {testActor, inspector, view} = yield openRuleView(); + yield selectNode("#div2", inspector); + + info("Get the background-color property from the rule-view"); + let property = getRuleViewProperty(view, "#div2", "background-color"); + let swatch = property.valueSpan.querySelector(".ruleview-colorswatch"); + ok(swatch, "Color swatch is displayed for the bg-color property"); + + info("Open the eyedropper from the colorpicker tooltip"); + yield openEyedropper(view, swatch); + + let tooltip = view.tooltips.colorPicker.tooltip; + ok(!tooltip.isVisible(), "color picker tooltip is closed after opening eyedropper"); + + info("Test that pressing escape dismisses the eyedropper"); + yield testESC(swatch, inspector, testActor); + + info("Open the eyedropper again"); + yield openEyedropper(view, swatch); + + info("Test that a color can be selected with the eyedropper"); + yield testSelect(view, swatch, inspector, testActor); + + let onHidden = tooltip.once("hidden"); + tooltip.hide(); + yield onHidden; + ok(!tooltip.isVisible(), "color picker tooltip is closed"); + + yield waitForTick(); +}); + +function* testESC(swatch, inspector, testActor) { + info("Press escape"); + let onCanceled = new Promise(resolve => { + inspector.inspector.once("color-pick-canceled", resolve); + }); + yield testActor.synthesizeKey({key: "VK_ESCAPE", options: {}}); + yield onCanceled; + + let color = swatch.style.backgroundColor; + is(color, ORIGINAL_COLOR, "swatch didn't change after pressing ESC"); +} + +function* testSelect(view, swatch, inspector, testActor) { + info("Click at x:10px y:10px"); + let onPicked = new Promise(resolve => { + inspector.inspector.once("color-picked", resolve); + }); + // The change to the content is done async after rule view change + let onRuleViewChanged = view.once("ruleview-changed"); + + yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10, + options: {type: "mousemove"}}); + yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10, + options: {type: "mousedown"}}); + yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10, + options: {type: "mouseup"}}); + + yield onPicked; + yield onRuleViewChanged; + + let color = swatch.style.backgroundColor; + is(color, EXPECTED_COLOR, "swatch changed colors"); + + is((yield getComputedStyleProperty("div", null, "background-color")), + EXPECTED_COLOR, + "div's color set to body color after dropper"); +} + +function* openEyedropper(view, swatch) { + let tooltip = view.tooltips.colorPicker.tooltip; + + info("Click on the swatch"); + let onColorPickerReady = view.tooltips.colorPicker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + let dropperButton = tooltip.doc.querySelector("#eyedropper-button"); + + info("Click on the eyedropper icon"); + let onOpened = tooltip.once("eyedropper-opened"); + dropperButton.click(); + yield onOpened; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js b/devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js new file mode 100644 index 000000000..21eeebb36 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests the that Filter Editor Tooltip opens by clicking on filter swatches + +const TEST_URL = URL_ROOT + "doc_filter.html"; + +add_task(function* () { + yield addTab(TEST_URL); + + let {view} = yield openRuleView(); + + info("Getting the filter swatch element"); + let swatch = getRuleViewProperty(view, "body", "filter").valueSpan + .querySelector(".ruleview-filterswatch"); + + let filterTooltip = view.tooltips.filterEditor; + // Clicking on a cssfilter swatch sets the current filter value in the tooltip + // which, in turn, makes the FilterWidget emit an "updated" event that causes + // the rule-view to refresh. So we must wait for the ruleview-changed event. + let onRuleViewChanged = view.once("ruleview-changed"); + swatch.click(); + yield onRuleViewChanged; + + ok(true, "The shown event was emitted after clicking on swatch"); + ok(!inplaceEditor(swatch.parentNode), + "The inplace editor wasn't shown as a result of the filter swatch click"); + + yield hideTooltipAndWaitForRuleViewChanged(filterTooltip, view); + + yield waitForTick(); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js b/devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js new file mode 100644 index 000000000..127a20843 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests the Filter Editor Tooltip committing changes on ENTER + +const TEST_URL = URL_ROOT + "doc_filter.html"; + +add_task(function* () { + yield addTab(TEST_URL); + let {view} = yield openRuleView(); + + info("Get the filter swatch element"); + let swatch = getRuleViewProperty(view, "body", "filter").valueSpan + .querySelector(".ruleview-filterswatch"); + + info("Click on the filter swatch element"); + // Clicking on a cssfilter swatch sets the current filter value in the tooltip + // which, in turn, makes the FilterWidget emit an "updated" event that causes + // the rule-view to refresh. So we must wait for the ruleview-changed event. + let onRuleViewChanged = view.once("ruleview-changed"); + swatch.click(); + yield onRuleViewChanged; + + info("Get the cssfilter widget instance"); + let filterTooltip = view.tooltips.filterEditor; + let widget = filterTooltip.widget; + + info("Set a new value in the cssfilter widget"); + onRuleViewChanged = view.once("ruleview-changed"); + widget.setCssValue("blur(2px)"); + yield waitForComputedStyleProperty("body", null, "filter", "blur(2px)"); + yield onRuleViewChanged; + ok(true, "Changes previewed on the element"); + + info("Press RETURN to commit changes"); + // Pressing return in the cssfilter tooltip triggeres 2 ruleview-changed + onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2); + EventUtils.sendKey("RETURN", widget.styleWindow); + yield onRuleViewChanged; + + is((yield getComputedStyleProperty("body", null, "filter")), "blur(2px)", + "The elemenet's filter was kept after RETURN"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js b/devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js new file mode 100644 index 000000000..0302f40a9 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that changes made to the Filter Editor Tooltip are reverted when +// ESC is pressed + +const TEST_URL = URL_ROOT + "doc_filter.html"; + +add_task(function* () { + yield addTab(TEST_URL); + let {view} = yield openRuleView(); + yield testPressingEscapeRevertsChanges(view); + yield testPressingEscapeRevertsChangesAndDisables(view); +}); + +function* testPressingEscapeRevertsChanges(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[0].editor; + let swatch = propEditor.valueSpan.querySelector(".ruleview-filterswatch"); + + yield clickOnFilterSwatch(swatch, view); + yield setValueInFilterWidget("blur(2px)", view); + + yield waitForComputedStyleProperty("body", null, "filter", "blur(2px)"); + is(propEditor.valueSpan.textContent, "blur(2px)", + "Got expected property value."); + + yield pressEscapeToCloseTooltip(view); + + yield waitForComputedStyleProperty("body", null, "filter", + "blur(2px) contrast(2)"); + is(propEditor.valueSpan.textContent, "blur(2px) contrast(2)", + "Got expected property value."); +} + +function* testPressingEscapeRevertsChangesAndDisables(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[0].editor; + + info("Disabling filter property"); + let onRuleViewChanged = view.once("ruleview-changed"); + propEditor.enable.click(); + yield onRuleViewChanged; + + ok(propEditor.element.classList.contains("ruleview-overridden"), + "property is overridden."); + is(propEditor.enable.style.visibility, "visible", + "property enable checkbox is visible."); + ok(!propEditor.enable.getAttribute("checked"), + "property enable checkbox is not checked."); + ok(!propEditor.prop.enabled, + "filter property is disabled."); + let newValue = yield getRulePropertyValue("filter"); + is(newValue, "", "filter should have been unset."); + + let swatch = propEditor.valueSpan.querySelector(".ruleview-filterswatch"); + yield clickOnFilterSwatch(swatch, view); + + ok(!propEditor.element.classList.contains("ruleview-overridden"), + "property overridden is not displayed."); + is(propEditor.enable.style.visibility, "hidden", + "property enable checkbox is hidden."); + + yield setValueInFilterWidget("blur(2px)", view); + yield pressEscapeToCloseTooltip(view); + + ok(propEditor.element.classList.contains("ruleview-overridden"), + "property is overridden."); + is(propEditor.enable.style.visibility, "visible", + "property enable checkbox is visible."); + ok(!propEditor.enable.getAttribute("checked"), + "property enable checkbox is not checked."); + ok(!propEditor.prop.enabled, "filter property is disabled."); + newValue = yield getRulePropertyValue("filter"); + is(newValue, "", "filter should have been unset."); + is(propEditor.valueSpan.textContent, "blur(2px) contrast(2)", + "Got expected property value."); +} + +function* getRulePropertyValue(name) { + let propValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: name + }); + return propValue; +} + +function* clickOnFilterSwatch(swatch, view) { + info("Clicking on a css filter swatch to open the tooltip"); + + // Clicking on a cssfilter swatch sets the current filter value in the tooltip + // which, in turn, makes the FilterWidget emit an "updated" event that causes + // the rule-view to refresh. So we must wait for the ruleview-changed event. + let onRuleViewChanged = view.once("ruleview-changed"); + swatch.click(); + yield onRuleViewChanged; +} + +function* setValueInFilterWidget(value, view) { + info("Setting the CSS filter value in the tooltip"); + + let filterTooltip = view.tooltips.filterEditor; + let onRuleViewChanged = view.once("ruleview-changed"); + filterTooltip.widget.setCssValue(value); + yield onRuleViewChanged; +} + +function* pressEscapeToCloseTooltip(view) { + info("Pressing ESCAPE to close the tooltip"); + + let filterTooltip = view.tooltips.filterEditor; + let onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.sendKey("ESCAPE", filterTooltip.widget.styleWindow); + yield onRuleViewChanged; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js new file mode 100644 index 000000000..617eb00da --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js @@ -0,0 +1,41 @@ +/* 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 grid highlighter is hidden on page navigation. + +const TEST_URI = ` + <style type='text/css'> + #grid { + display: grid; + } + </style> + <div id="grid"> + <div id="cell1">cell1</div> + <div id="cell2">cell2</div> + </div> +`; + +const TEST_URI_2 = "data:text/html,<html><body>test</body></html>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + yield selectNode("#grid", inspector); + let container = getRuleViewProperty(view, "#grid", "display").valueSpan; + let gridToggle = container.querySelector(".ruleview-grid"); + + info("Toggling ON the CSS grid highlighter from the rule-view."); + let onHighlighterShown = highlighters.once("highlighter-shown"); + gridToggle.click(); + yield onHighlighterShown; + + ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown."); + + yield navigateTo(inspector, TEST_URI_2); + ok(!highlighters.gridHighlighterShown, "CSS grid highlighter is hidden."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.js b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.js new file mode 100644 index 000000000..a6780a94a --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.js @@ -0,0 +1,53 @@ +/* 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 a grid highlighter showing grid gaps can be displayed after reloading the +// page (Bug 1342051). + +const TEST_URI = ` + <style type='text/css'> + #grid { + display: grid; + grid-gap: 10px; + } + </style> + <div id="grid"> + <div id="cell1">cell1</div> + <div id="cell2">cell2</div> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + + info("Check that the grid highlighter can be displayed"); + yield checkGridHighlighter(); + + info("Close the toolbox before reloading the tab"); + let target = TargetFactory.forTab(gBrowser.selectedTab); + yield gDevTools.closeToolbox(target); + + yield refreshTab(gBrowser.selectedTab); + + info("Check that the grid highlighter can be displayed after reloading the page"); + yield checkGridHighlighter(); +}); + +function* checkGridHighlighter() { + let {inspector, view} = yield openRuleView(); + let {highlighters} = view; + + yield selectNode("#grid", inspector); + let container = getRuleViewProperty(view, "#grid", "display").valueSpan; + let gridToggle = container.querySelector(".ruleview-grid"); + + info("Toggling ON the CSS grid highlighter from the rule-view."); + let onHighlighterShown = highlighters.once("highlighter-shown"); + gridToggle.click(); + yield onHighlighterShown; + + ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js new file mode 100644 index 000000000..04534522b --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js @@ -0,0 +1,64 @@ +/* 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 toggling the grid highlighter in the rule view and the display of the +// grid highlighter. + +const TEST_URI = ` + <style type='text/css'> + #grid { + display: grid; + } + </style> + <div id="grid"> + <div id="cell1">cell1</div> + <div id="cell2">cell2</div> + </div> +`; + +const HIGHLIGHTER_TYPE = "CssGridHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + yield selectNode("#grid", inspector); + let container = getRuleViewProperty(view, "#grid", "display").valueSpan; + let gridToggle = container.querySelector(".ruleview-grid"); + + info("Checking the initial state of the CSS grid toggle in the rule-view."); + ok(gridToggle, "Grid highlighter toggle is visible."); + ok(!gridToggle.classList.contains("active"), + "Grid highlighter toggle button is not active."); + ok(!highlighters.highlighters[HIGHLIGHTER_TYPE], + "No CSS grid highlighter exists in the rule-view."); + ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown."); + + info("Toggling ON the CSS grid highlighter from the rule-view."); + let onHighlighterShown = highlighters.once("highlighter-shown"); + gridToggle.click(); + yield onHighlighterShown; + + info("Checking the CSS grid highlighter is created and toggle button is active in " + + "the rule-view."); + ok(gridToggle.classList.contains("active"), + "Grid highlighter toggle is active."); + ok(highlighters.highlighters[HIGHLIGHTER_TYPE], + "CSS grid highlighter created in the rule-view."); + ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown."); + + info("Toggling OFF the CSS grid highlighter from the rule-view."); + let onHighlighterHidden = highlighters.once("highlighter-hidden"); + gridToggle.click(); + yield onHighlighterHidden; + + info("Checking the CSS grid highlighter is not shown and toggle button is not active " + + "in the rule-view."); + ok(!gridToggle.classList.contains("active"), + "Grid highlighter toggle button is not active."); + ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js new file mode 100644 index 000000000..5c339e892 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js @@ -0,0 +1,73 @@ +/* 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 toggling the grid highlighter in the rule view from an overridden 'display: grid' +// declaration. + +const TEST_URI = ` + <style type='text/css'> + #grid { + display: grid; + } + div, ul { + display: grid; + } + </style> + <ul id="grid"> + <li id="cell1">cell1</li> + <li id="cell2">cell2</li> + </ul> +`; + +const HIGHLIGHTER_TYPE = "CssGridHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + yield selectNode("#grid", inspector); + let container = getRuleViewProperty(view, "#grid", "display").valueSpan; + let gridToggle = container.querySelector(".ruleview-grid"); + let overriddenContainer = getRuleViewProperty(view, "div, ul", "display").valueSpan; + let overriddenGridToggle = overriddenContainer.querySelector(".ruleview-grid"); + + info("Checking the initial state of the CSS grid toggle in the rule-view."); + ok(gridToggle && overriddenGridToggle, "Grid highlighter toggles are visible."); + ok(!gridToggle.classList.contains("active") && + !overriddenGridToggle.classList.contains("active"), + "Grid highlighter toggle buttons are not active."); + ok(!highlighters.highlighters[HIGHLIGHTER_TYPE], + "No CSS grid highlighter exists in the rule-view."); + ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown."); + + info("Toggling ON the CSS grid highlighter from the overridden rule in the rule-view."); + let onHighlighterShown = highlighters.once("highlighter-shown"); + overriddenGridToggle.click(); + yield onHighlighterShown; + + info("Checking the CSS grid highlighter is created and toggle buttons are active in " + + "the rule-view."); + ok(gridToggle.classList.contains("active") && + overriddenGridToggle.classList.contains("active"), + "Grid highlighter toggle is active."); + ok(highlighters.highlighters[HIGHLIGHTER_TYPE], + "CSS grid highlighter created in the rule-view."); + ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown."); + + info("Toggling off the CSS grid highlighter from the normal grid declaration in the " + + "rule-view."); + let onHighlighterHidden = highlighters.once("highlighter-hidden"); + gridToggle.click(); + yield onHighlighterHidden; + + info("Checking the CSS grid highlighter is not shown and toggle buttons are not " + + "active in the rule-view."); + ok(!gridToggle.classList.contains("active") && + !overriddenGridToggle.classList.contains("active"), + "Grid highlighter toggle buttons are not active."); + ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js new file mode 100644 index 000000000..a908d6a97 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js @@ -0,0 +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"; + +// Test toggling the grid highlighter in the rule view with multiple grids in the page. + +const TEST_URI = ` + <style type='text/css'> + .grid { + display: grid; + } + </style> + <div id="grid1" class="grid"> + <div class="cell1">cell1</div> + <div class="cell2">cell2</div> + </div> + <div id="grid2" class="grid"> + <div class="cell1">cell1</div> + <div class="cell2">cell2</div> + </div> +`; + +const HIGHLIGHTER_TYPE = "CssGridHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + info("Selecting the first grid container."); + yield selectNode("#grid1", inspector); + let container = getRuleViewProperty(view, ".grid", "display").valueSpan; + let gridToggle = container.querySelector(".ruleview-grid"); + + info("Checking the state of the CSS grid toggle for the first grid container in the " + + "rule-view."); + ok(gridToggle, "Grid highlighter toggle is visible."); + ok(!gridToggle.classList.contains("active"), + "Grid highlighter toggle button is not active."); + ok(!highlighters.highlighters[HIGHLIGHTER_TYPE], + "No CSS grid highlighter exists in the rule-view."); + ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown."); + + info("Toggling ON the CSS grid highlighter for the first grid container from the " + + "rule-view."); + let onHighlighterShown = highlighters.once("highlighter-shown"); + gridToggle.click(); + yield onHighlighterShown; + + info("Checking the CSS grid highlighter is created and toggle button is active in " + + "the rule-view."); + ok(gridToggle.classList.contains("active"), + "Grid highlighter toggle is active."); + ok(highlighters.highlighters[HIGHLIGHTER_TYPE], + "CSS grid highlighter created in the rule-view."); + ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown."); + + info("Selecting the second grid container."); + yield selectNode("#grid2", inspector); + let firstGridHighterShown = highlighters.gridHighlighterShown; + container = getRuleViewProperty(view, ".grid", "display").valueSpan; + gridToggle = container.querySelector(".ruleview-grid"); + + info("Checking the state of the CSS grid toggle for the second grid container in the " + + "rule-view."); + ok(gridToggle, "Grid highlighter toggle is visible."); + ok(!gridToggle.classList.contains("active"), + "Grid highlighter toggle button is not active."); + ok(highlighters.gridHighlighterShown, "CSS grid highlighter is still shown."); + + info("Toggling ON the CSS grid highlighter for the second grid container from the " + + "rule-view."); + onHighlighterShown = highlighters.once("highlighter-shown"); + gridToggle.click(); + yield onHighlighterShown; + + info("Checking the CSS grid highlighter is created for the second grid container and " + + "toggle button is active in the rule-view."); + ok(gridToggle.classList.contains("active"), + "Grid highlighter toggle is active."); + ok(highlighters.gridHighlighterShown != firstGridHighterShown, + "Grid highlighter for the second grid container is shown."); + + info("Selecting the first grid container."); + yield selectNode("#grid1", inspector); + container = getRuleViewProperty(view, ".grid", "display").valueSpan; + gridToggle = container.querySelector(".ruleview-grid"); + + info("Checking the state of the CSS grid toggle for the first grid container in the " + + "rule-view."); + ok(gridToggle, "Grid highlighter toggle is visible."); + ok(!gridToggle.classList.contains("active"), + "Grid highlighter toggle button is not active."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_guessIndentation.js b/devtools/client/inspector/rules/test/browser_rules_guessIndentation.js new file mode 100644 index 000000000..ba2a1d7fb --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_guessIndentation.js @@ -0,0 +1,47 @@ +/* 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. + +// 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 {toolbox, inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Add a new property in the rule-view"); + yield addProperty(view, 2, "color", "chartreuse"); + + info("Switch to the style-editor"); + let { UI } = yield toolbox.selectTool("styleeditor"); + + let styleEditor = yield UI.editors[0].getSourceEditor(); + let text = styleEditor.sourceEditor.getText(); + is(text, expectedText, "style inspector changes are synced"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js new file mode 100644 index 000000000..d1f6d7f45 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js @@ -0,0 +1,47 @@ +/* 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 inherited properties appear for a nested element in the +// rule view. + +const TEST_URI = ` + <style type="text/css"> + #test2 { + background-color: green; + color: purple; + } + </style> + <div id="test2"><div id="test1">Styled Node</div></div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#test1", inspector); + yield simpleInherit(inspector, view); +}); + +function* simpleInherit(inspector, view) { + let elementStyle = view._elementStyle; + is(elementStyle.rules.length, 2, "Should have 2 rules."); + + 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, 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."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js new file mode 100644 index 000000000..db9662eee --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js @@ -0,0 +1,34 @@ +/* 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 no inherited properties appear when the property does not apply +// to the nested element. + +const TEST_URI = ` + <style type="text/css"> + #test2 { + background-color: green; + } + </style> + <div id="test2"><div id="test1">Styled Node</div></div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#test1", inspector); + yield emptyInherit(inspector, view); +}); + +function* emptyInherit(inspector, view) { + // No inheritable styles, this rule shouldn't show up. + let elementStyle = view._elementStyle; + is(elementStyle.rules.length, 1, "Should have 1 rule."); + + let elementRule = elementStyle.rules[0]; + ok(!elementRule.inherited, + "Element style attribute should not consider itself inherited."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.js b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.js new file mode 100644 index 000000000..d6075f6f4 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.js @@ -0,0 +1,40 @@ +/* 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 inline inherited properties appear in the nested element. + +var {ELEMENT_STYLE} = require("devtools/shared/specs/styles"); + +const TEST_URI = ` + <div id="test2" style="color: red"> + <div id="test1">Styled Node</div> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#test1", inspector); + yield elementStyleInherit(inspector, view); +}); + +function* elementStyleInherit(inspector, view) { + let elementStyle = view._elementStyle; + is(elementStyle.rules.length, 2, "Should have 2 rules."); + + let elementRule = elementStyle.rules[0]; + ok(!elementRule.inherited, + "Element style attribute should not consider itself inherited."); + + let inheritRule = elementStyle.rules[1]; + is(inheritRule.domRule.type, ELEMENT_STYLE, + "Inherited rule should be an element style, not a rule."); + 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(inheritProp.name, "color", "color should have been inherited."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_inline-source-map.js b/devtools/client/inspector/rules/test/browser_rules_inline-source-map.js new file mode 100644 index 000000000..05109d8c6 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_inline-source-map.js @@ -0,0 +1,26 @@ +/* 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 when a source map comment appears in an inline stylesheet, the +// rule-view still appears correctly. +// Bug 1255787. + +const TESTCASE_URI = URL_ROOT + "doc_inline_sourcemap.html"; +const PREF = "devtools.styleeditor.source-maps-enabled"; + +add_task(function* () { + Services.prefs.setBoolPref(PREF, true); + + yield addTab(TESTCASE_URI); + let {inspector, view} = yield openRuleView(); + + yield selectNode("div", inspector); + + let ruleEl = getRuleViewRule(view, "div"); + ok(ruleEl, "The 'div' rule exists in the rule-view"); + + Services.prefs.clearUserPref(PREF); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_invalid-source-map.js b/devtools/client/inspector/rules/test/browser_rules_invalid-source-map.js new file mode 100644 index 000000000..825f48a96 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_invalid-source-map.js @@ -0,0 +1,44 @@ +/* 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 when a source map is missing/invalid, the rule view still loads +// correctly. + +const TESTCASE_URI = URL_ROOT + "doc_invalid_sourcemap.html"; +const PREF = "devtools.styleeditor.source-maps-enabled"; +const CSS_LOC = "doc_invalid_sourcemap.css:1"; + +add_task(function* () { + Services.prefs.setBoolPref(PREF, true); + + yield addTab(TESTCASE_URI); + let {inspector, view} = yield openRuleView(); + + yield selectNode("div", inspector); + + let ruleEl = getRuleViewRule(view, "div"); + ok(ruleEl, "The 'div' rule exists in the rule-view"); + + let prop = getRuleViewProperty(view, "div", "color"); + ok(prop, "The 'color' property exists in this rule"); + + let value = getRuleViewPropertyValue(view, "div", "color"); + is(value, "gold", "The 'color' property has the right value"); + + yield verifyLinkText(view, CSS_LOC); + + Services.prefs.clearUserPref(PREF); +}); + +function verifyLinkText(view, text) { + info("Verifying that the rule-view stylesheet link is " + text); + let label = getRuleViewLinkByIndex(view, 1) + .querySelector(".ruleview-rule-source-label"); + return waitForSuccess( + () => label.textContent == text, + "Link text changed to display correct location: " + text + ); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_invalid.js b/devtools/client/inspector/rules/test/browser_rules_invalid.js new file mode 100644 index 000000000..e664f68ac --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_invalid.js @@ -0,0 +1,33 @@ +/* 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 an invalid property still lets us display the rule view +// Bug 1235603. + +const TEST_URI = ` + <style> + div { + background: #fff; + font-family: sans-serif; + url(display-table.min.htc); + } + </style> + <body> + <div id="testid" class="testclass">Styled Node</div> + </body> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + // Have to actually get the rule in order to ensure that the + // elements were created. + ok(getRuleViewRule(view, "div"), "Rule with div selector exists"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_keybindings.js b/devtools/client/inspector/rules/test/browser_rules_keybindings.js new file mode 100644 index 000000000..84fdeff85 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_keybindings.js @@ -0,0 +1,49 @@ +/* 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 focus doesn't leave the style editor when adding a property +// (bug 719916) + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8,<h1>Some header text</h1>"); + let {inspector, view} = yield openRuleView(); + yield selectNode("h1", inspector); + + info("Getting the ruleclose brace element"); + let brace = view.styleDocument.querySelector(".ruleview-ruleclose"); + + info("Focus the new property editable field to create a color property"); + let ruleEditor = getRuleViewRuleEditor(view, 0); + let editor = yield focusNewRuleViewProperty(ruleEditor); + editor.input.value = "color"; + + info("Typing ENTER to focus the next field: property value"); + let onFocus = once(brace.parentNode, "focus", true); + let onRuleViewChanged = view.once("ruleview-changed"); + + EventUtils.sendKey("return"); + + yield onFocus; + yield onRuleViewChanged; + ok(true, "The value field was focused"); + + info("Entering a property value"); + editor = getCurrentInplaceEditor(view); + editor.input.value = "green"; + + info("Typing ENTER again should focus a new property name"); + onFocus = once(brace.parentNode, "focus", true); + onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.sendKey("return"); + yield onFocus; + yield onRuleViewChanged; + ok(true, "The new property name field was focused"); + getCurrentInplaceEditor(view).input.blur(); +}); + +function getCurrentInplaceEditor(view) { + return inplaceEditor(view.styleDocument.activeElement); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js b/devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js new file mode 100644 index 000000000..ebbde08ac --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js @@ -0,0 +1,25 @@ +/* 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 = URL_ROOT + "doc_keyframeLineNumbers.html"; + +add_task(function* () { + yield addTab(TESTCASE_URI); + let { inspector, view } = yield openRuleView(); + yield selectNode("#outer", inspector); + + info("Insert a new property, which will affect the line numbers"); + yield addProperty(view, 1, "font-size", "72px"); + + yield selectNode("#inner", inspector); + + let value = getRuleViewLinkTextByIndex(view, 3); + // Note that this is relative to the <style>. + is(value.slice(-3), ":27", "rule line number is 27"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js new file mode 100644 index 000000000..8d4b436c5 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js @@ -0,0 +1,106 @@ +/* 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 = URL_ROOT + "doc_keyframeanimation.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {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"); + + yield assertKeyframeRules("#pacman", inspector, view, { + elementRulesNb: 2, + keyframeRulesNb: 2, + keyframesRules: ["pacman", "pacman"], + keyframeRules: ["100%", "100%"] + }); + + assertGutters(view, { + guttersNbs: 2, + gutterHeading: ["Keyframes pacman", "Keyframes pacman"] + }); +} + +function* testBoxy(inspector, view) { + info("Test content and gutter in the keyframes rule of #boxy"); + + yield assertKeyframeRules("#boxy", inspector, view, { + elementRulesNb: 3, + keyframeRulesNb: 3, + keyframesRules: ["boxy", "boxy", "boxy"], + keyframeRules: ["10%", "20%", "100%"] + }); + + assertGutters(view, { + guttersNbs: 1, + gutterHeading: ["Keyframes boxy"] + }); +} + +function* testMoxy(inspector, view) { + info("Test content and gutter in the keyframes rule of #moxy"); + + yield assertKeyframeRules("#moxy", inspector, view, { + elementRulesNb: 3, + keyframeRulesNb: 4, + keyframesRules: ["boxy", "boxy", "boxy", "moxy"], + keyframeRules: ["10%", "20%", "100%", "100%"] + }); + + assertGutters(view, { + guttersNbs: 2, + gutterHeading: ["Keyframes boxy", "Keyframes moxy"] + }); +} + +function* assertKeyframeRules(selector, inspector, view, expected) { + yield selectNode(selector, inspector); + let elementStyle = view._elementStyle; + + 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++; + } +} + +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/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.js b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.js new file mode 100644 index 000000000..b7652ecaa --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.js @@ -0,0 +1,92 @@ +/* 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 = URL_ROOT + "doc_keyframeanimation.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {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 = 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" + ); + + // Dynamic changes test disabled because of Bug 1050940 + // If this part of the test is ever enabled again, it should be changed to + // use addProperty (in head.js) and stop using _applyingModifications + + // 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", true); + + // yield ruleEditor._applyingModifications; + // yield once(element, "animationend"); + + // is + // ( + // convertTextPropsToString(rules.keyframeRules[1].textProps), + // "left: 750px; opacity: 0", + // "Keyframe pacman (100%) property is correct" + // ); + + // is(defaultView.getComputedStyle(element).getPropertyValue("opacity"), "0", + // "Added opacity property should have been used."); +} + +function* testBoxy(inspector, view) { + info("Test content in the keyframes rule of #boxy"); + + let rules = 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) { + yield selectNode(selector, inspector); + let elementStyle = view._elementStyle; + + let rules = { + elementRules: elementStyle.rules.filter(rule => !rule.keyframes), + keyframeRules: elementStyle.rules.filter(rule => rule.keyframes) + }; + + return rules; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_lineNumbers.js b/devtools/client/inspector/rules/test/browser_rules_lineNumbers.js new file mode 100644 index 000000000..3b09209f5 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_lineNumbers.js @@ -0,0 +1,29 @@ +/* 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 = URL_ROOT + "doc_ruleLineNumbers.html"; + +add_task(function* () { + yield addTab(TESTCASE_URI); + let { inspector, view } = yield openRuleView(); + yield selectNode("#testid", inspector); + + 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"); + + let onLocationChanged = once(bodyRuleEditor.rule.domRule, "location-changed"); + yield addProperty(view, 1, "font-size", "23px"); + 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"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_livepreview.js b/devtools/client/inspector/rules/test/browser_rules_livepreview.js new file mode 100644 index 000000000..1f1302a70 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_livepreview.js @@ -0,0 +1,72 @@ +/* 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 changes are previewed when editing a property value. + +const TEST_URI = ` + <style type="text/css"> + #testid { + display:block; + } + </style> + <div id="testid">Styled Node</div><span>inline element</span> +`; + +// Format +// { +// value : what to type in the field +// expected : expected computed style on the targeted element +// } +const TEST_DATA = [ + {value: "inline", expected: "inline"}, + {value: "inline-block", expected: "inline-block"}, + + // Invalid property values should not apply, and should fall back to default + {value: "red", expected: "block"}, + {value: "something", expected: "block"}, + + {escape: true, value: "inline", expected: "block"} +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + for (let data of TEST_DATA) { + yield testLivePreviewData(data, view, "#testid"); + } +}); + +function* testLivePreviewData(data, ruleView, selector) { + let rule = getRuleViewRuleEditor(ruleView, 1).rule; + let propEditor = rule.textProps[0].editor; + + info("Focusing the property value inplace-editor"); + let editor = yield focusEditableField(ruleView, propEditor.valueSpan); + is(inplaceEditor(propEditor.valueSpan), editor, + "The focused editor is the value"); + + info("Entering value in the editor: " + data.value); + let onPreviewDone = ruleView.once("ruleview-changed"); + EventUtils.sendString(data.value, ruleView.styleWindow); + ruleView.throttle.flush(); + yield onPreviewDone; + + let onValueDone = ruleView.once("ruleview-changed"); + if (data.escape) { + EventUtils.synthesizeKey("VK_ESCAPE", {}); + } else { + EventUtils.synthesizeKey("VK_RETURN", {}); + } + yield onValueDone; + + // While the editor is still focused in, the display should have + // changed already + is((yield getComputedStyleProperty(selector, null, "display")), + data.expected, + "Element should be previewed as " + data.expected); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.js new file mode 100644 index 000000000..ab10fadfe --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.js @@ -0,0 +1,56 @@ +/* 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 based on the +// specificity of the rule. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + } + .testclass { + background-color: green; + } + </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); + + let idRule = getRuleViewRuleEditor(view, 1).rule; + let idProp = idRule.textProps[0]; + is(idProp.name, "background-color", + "First ID property should be background-color"); + is(idProp.value, "blue", "First ID property value should be blue"); + ok(!idProp.overridden, "ID prop should not be overridden."); + ok(!idProp.editor.element.classList.contains("ruleview-overridden"), + "ID property editor should not have ruleview-overridden class"); + + let classRule = getRuleViewRuleEditor(view, 2).rule; + let classProp = classRule.textProps[0]; + is(classProp.name, "background-color", + "First class prop should be background-color"); + is(classProp.value, "green", "First class property value should be green"); + ok(classProp.overridden, "Class property should be overridden."); + ok(classProp.editor.element.classList.contains("ruleview-overridden"), + "Class property editor should have ruleview-overridden class"); + + // Override background-color by changing the element style. + let elementProp = yield addProperty(view, 0, "background-color", "purple"); + + ok(!elementProp.overridden, + "Element style property should not be overridden"); + ok(idProp.overridden, "ID property should be overridden"); + ok(idProp.editor.element.classList.contains("ruleview-overridden"), + "ID property editor should have ruleview-overridden class"); + ok(classProp.overridden, "Class property should be overridden"); + ok(classProp.editor.element.classList.contains("ruleview-overridden"), + "Class property editor should have ruleview-overridden class"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.js new file mode 100644 index 000000000..c71fc7211 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.js @@ -0,0 +1,45 @@ +/* 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 for short hand +// properties and the computed list properties + +const TEST_URI = ` + <style type='text/css'> + #testid { + margin-left: 1px; + } + .testclass { + margin: 2px; + } + </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 classRule = elementStyle.rules[2]; + let classProp = classRule.textProps[0]; + ok(!classProp.overridden, + "Class prop shouldn't be overridden, some props are still being used."); + + for (let computed of classProp.computed) { + if (computed.name.indexOf("margin-left") == 0) { + ok(computed.overridden, "margin-left props should be overridden."); + } else { + ok(!computed.overridden, + "Non-margin-left props should not be overridden."); + } + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.js new file mode 100644 index 000000000..b99bab8b4 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.js @@ -0,0 +1,41 @@ +/* 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 based on the +// priority for the rule + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + } + .testclass { + background-color: green !important; + } + </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); + + let idRule = getRuleViewRuleEditor(view, 1).rule; + let idProp = idRule.textProps[0]; + ok(idProp.overridden, "Not-important rule should be overridden."); + + let classRule = getRuleViewRuleEditor(view, 2).rule; + let classProp = classRule.textProps[0]; + ok(!classProp.overridden, "Important rule should not be overridden."); + + ok(idProp.overridden, "ID property should be overridden."); + + // FIXME: re-enable these 2 assertions when bug 1247737 is fixed. + // let elementProp = yield addProperty(view, 0, "background-color", "purple"); + // ok(!elementProp.overridden, "New important prop should not be overriden."); + // ok(classProp.overridden, "Class property should be overridden."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.js new file mode 100644 index 000000000..fbce1ebf4 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.js @@ -0,0 +1,36 @@ +/* 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 if a property gets +// disabled + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + } + .testclass { + background-color: green; + } + </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); + + let idRule = getRuleViewRuleEditor(view, 1).rule; + let idProp = idRule.textProps[0]; + + yield togglePropStatus(view, idProp); + + let classRule = getRuleViewRuleEditor(view, 2).rule; + let classProp = classRule.textProps[0]; + ok(!classProp.overridden, + "Class prop should not be overridden after id prop was disabled."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js new file mode 100644 index 000000000..11ecd72ff --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js @@ -0,0 +1,33 @@ +/* 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 based on the +// order of the property. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: green; + } + </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); + + let rule = getRuleViewRuleEditor(view, 1).rule; + + yield addProperty(view, 1, "background-color", "red"); + + let firstProp = rule.textProps[0]; + let secondProp = rule.textProps[1]; + + ok(firstProp.overridden, "First property should be overridden."); + ok(!secondProp.overridden, "Second property should not be overridden."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_06.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_06.js new file mode 100644 index 000000000..c2e71fe49 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_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."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.js new file mode 100644 index 000000000..9480ddd47 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.js @@ -0,0 +1,72 @@ +/* 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 based on the +// specificity of the rule. + +const TEST_URI = ` + <style type='text/css'> + #testid { + margin-left: 23px; + } + + div { + margin-right: 23px; + margin-left: 1px !important; + } + + body { + margin-right: 1px !important; + font-size: 79px; + } + + span { + font-size: 12px; + } + </style> + <body> + <span> + <div id='testid' class='testclass'>Styled Node</div> + </span> + </body> +`; + +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 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: "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]; + + info("Checking rule " + i + ", property " + propIndex); + + is(prop.name, expected.name, "check property name"); + is(prop.value, expected.value, "check property value"); + is(prop.overridden, expected.overridden, "check property overridden"); + } + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_mathml-element.js b/devtools/client/inspector/rules/test/browser_rules_mathml-element.js new file mode 100644 index 000000000..f8a1e8572 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_mathml-element.js @@ -0,0 +1,53 @@ +/* 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 displays correctly on MathML elements. + +const TEST_URI = ` + <div> + <math xmlns=\http://www.w3.org/1998/Math/MathML\> + <mfrac> + <msubsup> + <mi>a</mi> + <mi>i</mi> + <mi>j</mi> + </msubsup> + <msub> + <mi>x</mi> + <mn>0</mn> + </msub> + </mfrac> + </math> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + info("Select the DIV node and verify the rule-view shows rules"); + yield selectNode("div", inspector); + ok(view.element.querySelectorAll(".ruleview-rule").length, + "The rule-view shows rules for the div element"); + + info("Select various MathML nodes and verify the rule-view is empty"); + yield selectNode("math", inspector); + ok(!view.element.querySelectorAll(".ruleview-rule").length, + "The rule-view is empty for the math element"); + + yield selectNode("msubsup", inspector); + ok(!view.element.querySelectorAll(".ruleview-rule").length, + "The rule-view is empty for the msubsup element"); + + yield selectNode("mn", inspector); + ok(!view.element.querySelectorAll(".ruleview-rule").length, + "The rule-view is empty for the mn element"); + + info("Select again the DIV node and verify the rule-view shows rules"); + yield selectNode("div", inspector); + ok(view.element.querySelectorAll(".ruleview-rule").length, + "The rule-view shows rules for the div element"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_media-queries.js b/devtools/client/inspector/rules/test/browser_rules_media-queries.js new file mode 100644 index 000000000..57ab19163 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_media-queries.js @@ -0,0 +1,26 @@ +/* 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 correctly display appropriate media query titles in the +// rule view. + +const TEST_URI = URL_ROOT + "doc_media_queries.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + + let elementStyle = view._elementStyle; + + let inline = STYLE_INSPECTOR_L10N.getStr("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 + + ":9 @media screen and (min-width: 1px)", "check rule 1 title"); + is(elementStyle.rules[2].title, inline + ":2", "check rule 2 title"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.js new file mode 100644 index 000000000..c820dd73f --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.js @@ -0,0 +1,68 @@ +/* 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 +// 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); + + let ruleEditor = getRuleViewRuleEditor(view, 0); + // Note that we wait for a markup mutation here because this new rule will end + // up creating a style attribute on the node shown in the markup-view. + // (we also wait for the rule-view to refresh). + let onMutation = inspector.once("markupmutation"); + let onRuleViewChanged = view.once("ruleview-changed"); + yield createNewRuleViewProperty(ruleEditor, + "color:red;color:orange;color:yellow;color:green;color:blue;color:indigo;" + + "color:violet;"); + yield onMutation; + yield onRuleViewChanged; + + is(ruleEditor.rule.textProps.length, 7, + "Should have created new text properties."); + is(ruleEditor.propertyList.children.length, 8, + "Should have created new property editors."); + + is(ruleEditor.rule.textProps[0].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[0].value, "red", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[1].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[1].value, "orange", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[2].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[2].value, "yellow", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[3].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[3].value, "green", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[4].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[4].value, "blue", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[5].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[5].value, "indigo", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[6].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[6].value, "violet", + "Should have correct property value"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js new file mode 100644 index 000000000..f7d98b768 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js @@ -0,0 +1,47 @@ +/* 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 +// 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); + + let ruleEditor = getRuleViewRuleEditor(view, 0); + // Note that we wait for a markup mutation here because this new rule will end + // up creating a style attribute on the node shown in the markup-view. + // (we also wait for the rule-view to refresh). + let onMutation = inspector.once("markupmutation"); + let onRuleViewChanged = view.once("ruleview-changed"); + yield createNewRuleViewProperty(ruleEditor, + "color:red;width:100px;height: 100px;"); + yield onMutation; + yield onRuleViewChanged; + + is(ruleEditor.rule.textProps.length, 3, + "Should have created new text properties."); + is(ruleEditor.propertyList.children.length, 4, + "Should have created new property editors."); + + is(ruleEditor.rule.textProps[0].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[0].value, "red", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[1].name, "width", + "Should have correct property name"); + is(ruleEditor.rule.textProps[1].value, "100px", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[2].name, "height", + "Should have correct property name"); + is(ruleEditor.rule.textProps[2].value, "100px", + "Should have correct property value"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.js new file mode 100644 index 000000000..deaf16029 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.js @@ -0,0 +1,62 @@ +/* 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 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* testCreateNewMultiUnfinished(inspector, view) { + let ruleEditor = getRuleViewRuleEditor(view, 0); + let onMutation = inspector.once("markupmutation"); + let onRuleViewChanged = view.once("ruleview-changed"); + 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, + "Should have created property editors."); + + EventUtils.sendString("red", view.styleWindow); + onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onRuleViewChanged; + + is(ruleEditor.rule.textProps.length, 4, + "Should have the same number of text properties."); + is(ruleEditor.propertyList.children.length, 5, + "Should have added the changed value editor."); + + is(ruleEditor.rule.textProps[0].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[0].value, "blue", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[1].name, "background", + "Should have correct property name"); + is(ruleEditor.rule.textProps[1].value, "orange", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[2].name, "text-align", + "Should have correct property name"); + is(ruleEditor.rule.textProps[2].value, "center", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[3].name, "border-color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[3].value, "red", + "Should have correct property value"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js new file mode 100644 index 000000000..dd1360b96 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js @@ -0,0 +1,71 @@ +/* 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 +// 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(); + + // Turn off throttling, which can cause intermittents. Throttling is used by + // the TextPropertyEditor. + view.throttle = () => {}; + + yield selectNode("div", inspector); + + let ruleEditor = getRuleViewRuleEditor(view, 0); + // Note that we wait for a markup mutation here because this new rule will end + // up creating a style attribute on the node shown in the markup-view. + // (we also wait for the rule-view to refresh). + let onMutation = inspector.once("markupmutation"); + let onRuleViewChanged = view.once("ruleview-changed"); + yield createNewRuleViewProperty(ruleEditor, "width: 100px; heig"); + yield onMutation; + yield onRuleViewChanged; + + is(ruleEditor.rule.textProps.length, 2, + "Should have created a new text property."); + is(ruleEditor.propertyList.children.length, 2, + "Should have created a property editor."); + + // Value is focused, lets add multiple rules here and make sure they get added + onMutation = inspector.once("markupmutation"); + onRuleViewChanged = view.once("ruleview-changed"); + let valueEditor = ruleEditor.propertyList.children[1] + .querySelector(".styleinspector-propertyeditor"); + valueEditor.value = "10px;background:orangered;color: black;"; + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onMutation; + yield onRuleViewChanged; + + is(ruleEditor.rule.textProps.length, 4, + "Should have added the changed value."); + is(ruleEditor.propertyList.children.length, 5, + "Should have added the changed value editor."); + + is(ruleEditor.rule.textProps[0].name, "width", + "Should have correct property name"); + is(ruleEditor.rule.textProps[0].value, "100px", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[1].name, "heig", + "Should have correct property name"); + is(ruleEditor.rule.textProps[1].value, "10px", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[2].name, "background", + "Should have correct property name"); + is(ruleEditor.rule.textProps[2].value, "orangered", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[3].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[3].value, "black", + "Should have correct property value"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple_properties_01.js b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_01.js new file mode 100644 index 000000000..2801df652 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_01.js @@ -0,0 +1,53 @@ +/* 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 +// 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); + + let ruleEditor = getRuleViewRuleEditor(view, 0); + // Note that we wait for a markup mutation here because this new rule will end + // up creating a style attribute on the node shown in the markup-view. + // (we also wait for the rule-view to refresh). + let onMutation = inspector.once("markupmutation"); + let onRuleViewChanged = view.once("ruleview-changed"); + yield createNewRuleViewProperty(ruleEditor, + "color:blue;background : orange ; text-align:center; " + + "border-color: green;"); + yield onMutation; + yield onRuleViewChanged; + + is(ruleEditor.rule.textProps.length, 4, + "Should have created a new text property."); + is(ruleEditor.propertyList.children.length, 5, + "Should have created a new property editor."); + + is(ruleEditor.rule.textProps[0].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[0].value, "blue", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[1].name, "background", + "Should have correct property name"); + is(ruleEditor.rule.textProps[1].value, "orange", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[2].name, "text-align", + "Should have correct property name"); + is(ruleEditor.rule.textProps[2].value, "center", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[3].name, "border-color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[3].value, "green", + "Should have correct property value"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.js b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.js new file mode 100644 index 000000000..ce6f1909f --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.js @@ -0,0 +1,54 @@ +/* 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 +// 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); + + let ruleEditor = getRuleViewRuleEditor(view, 0); + let onDone = view.once("ruleview-changed"); + yield createNewRuleViewProperty(ruleEditor, "width:"); + yield onDone; + + is(ruleEditor.rule.textProps.length, 1, + "Should have created a new text property."); + is(ruleEditor.propertyList.children.length, 1, + "Should have created a property editor."); + + // Value is focused, lets add multiple rules here and make sure they get added + onDone = view.once("ruleview-changed"); + let onMutation = inspector.once("markupmutation"); + let input = view.styleDocument.activeElement; + input.value = "height: 10px;color:blue"; + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onMutation; + yield onDone; + + is(ruleEditor.rule.textProps.length, 2, + "Should have added the changed value."); + is(ruleEditor.propertyList.children.length, 3, + "Should have added the changed value editor."); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + is(ruleEditor.propertyList.children.length, 2, + "Should have removed the value editor."); + + is(ruleEditor.rule.textProps[0].name, "width", + "Should have correct property name"); + is(ruleEditor.rule.textProps[0].value, "height: 10px", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[1].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[1].value, "blue", + "Should have correct property value"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_original-source-link.js b/devtools/client/inspector/rules/test/browser_rules_original-source-link.js new file mode 100644 index 000000000..09dad9a86 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_original-source-link.js @@ -0,0 +1,85 @@ +/* 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 stylesheet links in the rule view are correct when source maps +// are involved. + +const TESTCASE_URI = URL_ROOT + "doc_sourcemaps.html"; +const PREF = "devtools.styleeditor.source-maps-enabled"; +const SCSS_LOC = "doc_sourcemaps.scss:4"; +const CSS_LOC = "doc_sourcemaps.css:1"; + +add_task(function* () { + info("Setting the " + PREF + " pref to true"); + Services.prefs.setBoolPref(PREF, true); + + yield addTab(TESTCASE_URI); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("div", inspector); + + yield verifyLinkText(SCSS_LOC, view); + + info("Setting the " + PREF + " pref to false"); + Services.prefs.setBoolPref(PREF, false); + yield verifyLinkText(CSS_LOC, view); + + info("Setting the " + PREF + " pref to true again"); + Services.prefs.setBoolPref(PREF, true); + + yield testClickingLink(toolbox, view); + yield checkDisplayedStylesheet(toolbox); + + info("Clearing the " + PREF + " pref"); + Services.prefs.clearUserPref(PREF); +}); + +function* testClickingLink(toolbox, view) { + info("Listening for switch to the style editor"); + let onStyleEditorReady = toolbox.once("styleeditor-ready"); + + info("Finding the stylesheet link and clicking it"); + let link = getRuleViewLinkByIndex(view, 1); + link.scrollIntoView(); + link.click(); + yield onStyleEditorReady; +} + +function checkDisplayedStylesheet(toolbox) { + let def = defer(); + + let panel = toolbox.getCurrentPanel(); + panel.UI.on("editor-selected", (event, editor) => { + // The style editor selects the first sheet at first load before + // selecting the desired sheet. + if (editor.styleSheet.href.endsWith("scss")) { + info("Original source editor selected"); + editor.getSourceEditor().then(editorSelected) + .then(def.resolve, def.reject); + } + }); + + return def.promise; +} + +function editorSelected(editor) { + let href = editor.styleSheet.href; + ok(href.endsWith("doc_sourcemaps.scss"), + "selected stylesheet is correct one"); + + let {line} = editor.sourceEditor.getCursor(); + is(line, 3, "cursor is at correct line number in original source"); +} + +function verifyLinkText(text, view) { + info("Verifying that the rule-view stylesheet link is " + text); + let label = getRuleViewLinkByIndex(view, 1) + .querySelector(".ruleview-rule-source-label"); + return waitForSuccess(function* () { + return label.textContent == text; + }, "Link text changed to display correct location: " + text); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js new file mode 100644 index 000000000..e98b5437c --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js @@ -0,0 +1,260 @@ +/* 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 pseudoelements are displayed correctly in the rule view + +const TEST_URI = URL_ROOT + "doc_pseudoelement.html"; +const PSEUDO_PREF = "devtools.inspector.show_pseudo_elements"; + +add_task(function* () { + yield pushPref(PSEUDO_PREF, true); + + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + + yield testTopLeft(inspector, view); + yield testTopRight(inspector, view); + yield testBottomRight(inspector, view); + yield testBottomLeft(inspector, view); + yield testParagraph(inspector, view); + yield testBody(inspector, view); +}); + +function* testTopLeft(inspector, view) { + let id = "#topleft"; + let rules = yield assertPseudoElementRulesNumbers(id, + inspector, view, { + elementRulesNb: 4, + firstLineRulesNb: 2, + firstLetterRulesNb: 1, + selectionRulesNb: 0, + afterRulesNb: 1, + beforeRulesNb: 2 + } + ); + + let gutters = assertGutters(view); + + info("Make sure that clicking on the twisty hides pseudo elements"); + let expander = gutters[0].querySelector(".ruleview-expander"); + ok(!view.element.children[1].hidden, "Pseudo Elements are expanded"); + + expander.click(); + ok(view.element.children[1].hidden, + "Pseudo Elements are collapsed by twisty"); + + expander.click(); + ok(!view.element.children[1].hidden, "Pseudo Elements are expanded again"); + + info("Make sure that dblclicking on the header container also toggles " + + "the pseudo elements"); + EventUtils.synthesizeMouseAtCenter(gutters[0], {clickCount: 2}, + view.styleWindow); + ok(view.element.children[1].hidden, + "Pseudo Elements are collapsed by dblclicking"); + + let elementRuleView = getRuleViewRuleEditor(view, 3); + + let elementFirstLineRule = rules.firstLineRules[0]; + let elementFirstLineRuleView = + [...view.element.children[1].children].filter(e => { + return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule; + })[0]._ruleEditor; + + is(convertTextPropsToString(elementFirstLineRule.textProps), + "color: orange", + "TopLeft firstLine properties are correct"); + + let onAdded = view.once("ruleview-changed"); + let firstProp = elementFirstLineRuleView.addProperty("background-color", + "rgb(0, 255, 0)", "", true); + yield onAdded; + + onAdded = view.once("ruleview-changed"); + let secondProp = elementFirstLineRuleView.addProperty("font-style", + "italic", "", true); + yield onAdded; + + is(firstProp, + elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 2], + "First added property is on back of array"); + is(secondProp, + elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 1], + "Second added property is on back of array"); + + is((yield getComputedStyleProperty(id, ":first-line", "background-color")), + "rgb(0, 255, 0)", "Added property should have been used."); + is((yield getComputedStyleProperty(id, ":first-line", "font-style")), + "italic", "Added property should have been used."); + is((yield getComputedStyleProperty(id, null, "text-decoration")), + "none", "Added property should not apply to element"); + + yield togglePropStatus(view, firstProp); + + is((yield getComputedStyleProperty(id, ":first-line", "background-color")), + "rgb(255, 0, 0)", "Disabled property should now have been used."); + is((yield getComputedStyleProperty(id, null, "background-color")), + "rgb(221, 221, 221)", "Added property should not apply to element"); + + yield togglePropStatus(view, firstProp); + + is((yield getComputedStyleProperty(id, ":first-line", "background-color")), + "rgb(0, 255, 0)", "Added property should have been used."); + is((yield getComputedStyleProperty(id, null, "text-decoration")), + "none", "Added property should not apply to element"); + + onAdded = view.once("ruleview-changed"); + firstProp = elementRuleView.addProperty("background-color", + "rgb(0, 0, 255)", "", true); + yield onAdded; + + is((yield getComputedStyleProperty(id, null, "background-color")), + "rgb(0, 0, 255)", "Added property should have been used."); + is((yield getComputedStyleProperty(id, ":first-line", "background-color")), + "rgb(0, 255, 0)", "Added prop does not apply to pseudo"); +} + +function* testTopRight(inspector, view) { + yield assertPseudoElementRulesNumbers("#topright", inspector, view, { + elementRulesNb: 4, + firstLineRulesNb: 1, + firstLetterRulesNb: 1, + selectionRulesNb: 0, + beforeRulesNb: 2, + afterRulesNb: 1 + }); + + let gutters = assertGutters(view); + + let expander = gutters[0].querySelector(".ruleview-expander"); + ok(!view.element.firstChild.classList.contains("show-expandable-container"), + "Pseudo Elements remain collapsed after switching element"); + + expander.scrollIntoView(); + expander.click(); + ok(!view.element.children[1].hidden, + "Pseudo Elements are shown again after clicking twisty"); +} + +function* testBottomRight(inspector, view) { + yield assertPseudoElementRulesNumbers("#bottomright", inspector, view, { + elementRulesNb: 4, + firstLineRulesNb: 1, + firstLetterRulesNb: 1, + selectionRulesNb: 0, + beforeRulesNb: 3, + afterRulesNb: 1 + }); +} + +function* testBottomLeft(inspector, view) { + yield assertPseudoElementRulesNumbers("#bottomleft", inspector, view, { + elementRulesNb: 4, + firstLineRulesNb: 1, + firstLetterRulesNb: 1, + selectionRulesNb: 0, + beforeRulesNb: 2, + afterRulesNb: 1 + }); +} + +function* testParagraph(inspector, view) { + let rules = + yield assertPseudoElementRulesNumbers("#bottomleft p", inspector, view, { + elementRulesNb: 3, + firstLineRulesNb: 1, + firstLetterRulesNb: 1, + selectionRulesNb: 1, + beforeRulesNb: 0, + afterRulesNb: 0 + }); + + assertGutters(view); + + let elementFirstLineRule = rules.firstLineRules[0]; + is(convertTextPropsToString(elementFirstLineRule.textProps), + "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", + "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"); +} + +function convertTextPropsToString(textProps) { + return textProps.map(t => t.name + ": " + t.value).join("; "); +} + +function* testNode(selector, inspector, view) { + yield selectNode(selector, inspector); + let elementStyle = view._elementStyle; + return elementStyle; +} + +function* assertPseudoElementRulesNumbers(selector, inspector, view, ruleNbs) { + let elementStyle = yield testNode(selector, inspector, view); + + let rules = { + elementRules: elementStyle.rules.filter(rule => !rule.pseudoElement), + firstLineRules: elementStyle.rules.filter(rule => + rule.pseudoElement === ":first-line"), + firstLetterRules: elementStyle.rules.filter(rule => + rule.pseudoElement === ":first-letter"), + selectionRules: elementStyle.rules.filter(rule => + rule.pseudoElement === ":-moz-selection"), + beforeRules: elementStyle.rules.filter(rule => + rule.pseudoElement === ":before"), + afterRules: elementStyle.rules.filter(rule => + rule.pseudoElement === ":after"), + }; + + is(rules.elementRules.length, ruleNbs.elementRulesNb, + selector + " has the correct number of non pseudo element rules"); + is(rules.firstLineRules.length, ruleNbs.firstLineRulesNb, + selector + " has the correct number of :first-line rules"); + is(rules.firstLetterRules.length, ruleNbs.firstLetterRulesNb, + selector + " has the correct number of :first-letter rules"); + is(rules.selectionRules.length, ruleNbs.selectionRulesNb, + selector + " has the correct number of :selection rules"); + is(rules.beforeRules.length, ruleNbs.beforeRulesNb, + selector + " has the correct number of :before rules"); + is(rules.afterRules.length, ruleNbs.afterRulesNb, + selector + " has the correct number of :after rules"); + + return rules; +} + +function getGutters(view) { + return view.element.querySelectorAll(".theme-gutter"); +} + +function assertGutters(view) { + let gutters = getGutters(view); + + is(gutters.length, 3, + "There are 3 gutter headings"); + is(gutters[0].textContent, "Pseudo-elements", + "Gutter heading is correct"); + is(gutters[1].textContent, "This Element", + "Gutter heading is correct"); + is(gutters[2].textContent, "Inherited from body", + "Gutter heading is correct"); + + return gutters; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js new file mode 100644 index 000000000..f69c328db --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js @@ -0,0 +1,29 @@ +/* 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 pseudoelements are displayed correctly in the markup view. + +const TEST_URI = URL_ROOT + "doc_pseudoelement.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector} = yield openRuleView(); + + let node = yield getNodeFront("#topleft", inspector); + let children = yield inspector.markup.walker.children(node); + + is(children.nodes.length, 3, "Element has correct number of children"); + + let beforeElement = children.nodes[0]; + is(beforeElement.tagName, "_moz_generated_content_before", + "tag name is correct"); + yield selectNode(beforeElement, inspector); + + let afterElement = children.nodes[children.nodes.length - 1]; + is(afterElement.tagName, "_moz_generated_content_after", + "tag name is correct"); + yield selectNode(afterElement, inspector); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js b/devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js new file mode 100644 index 000000000..d795ba5f3 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js @@ -0,0 +1,131 @@ +/* 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 pseudo lock options work properly. + +const TEST_URI = ` + <style type='text/css'> + div { + color: red; + } + div:hover { + color: blue; + } + div:active { + color: yellow; + } + div:focus { + color: green; + } + </style> + <div>test div</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 assertPseudoPanelClosed(view); + + info("Toggle the pseudo class panel open"); + view.pseudoClassToggle.click(); + yield assertPseudoPanelOpened(view); + + info("Toggle each pseudo lock and check that the pseudo lock is added"); + yield togglePseudoClass(inspector, view.hoverCheckbox); + yield assertPseudoAdded(inspector, view, ":hover", 3, 1); + yield togglePseudoClass(inspector, view.hoverCheckbox); + yield assertPseudoRemoved(inspector, view, 2); + + yield togglePseudoClass(inspector, view.activeCheckbox); + yield assertPseudoAdded(inspector, view, ":active", 3, 1); + yield togglePseudoClass(inspector, view.activeCheckbox); + yield assertPseudoRemoved(inspector, view, 2); + + yield togglePseudoClass(inspector, view.focusCheckbox); + yield assertPseudoAdded(inspector, view, ":focus", 3, 1); + yield togglePseudoClass(inspector, view.focusCheckbox); + yield assertPseudoRemoved(inspector, view, 2); + + info("Toggle all pseudo lock and check that the pseudo lock is added"); + yield togglePseudoClass(inspector, view.hoverCheckbox); + yield togglePseudoClass(inspector, view.activeCheckbox); + yield togglePseudoClass(inspector, view.focusCheckbox); + yield assertPseudoAdded(inspector, view, ":focus", 5, 1); + yield assertPseudoAdded(inspector, view, ":active", 5, 2); + yield assertPseudoAdded(inspector, view, ":hover", 5, 3); + yield togglePseudoClass(inspector, view.hoverCheckbox); + yield togglePseudoClass(inspector, view.activeCheckbox); + yield togglePseudoClass(inspector, view.focusCheckbox); + yield assertPseudoRemoved(inspector, view, 2); + + info("Select a null element"); + yield view.selectElement(null); + ok(!view.hoverCheckbox.checked && view.hoverCheckbox.disabled, + ":hover checkbox is unchecked and disabled"); + ok(!view.activeCheckbox.checked && view.activeCheckbox.disabled, + ":active checkbox is unchecked and disabled"); + ok(!view.focusCheckbox.checked && view.focusCheckbox.disabled, + ":focus checkbox is unchecked and disabled"); + + info("Toggle the pseudo class panel close"); + view.pseudoClassToggle.click(); + yield assertPseudoPanelClosed(view); +}); + +function* togglePseudoClass(inspector, pseudoClassOption) { + info("Toggle the pseudoclass, wait for it to be applied"); + let onRefresh = inspector.once("rule-view-refreshed"); + pseudoClassOption.click(); + yield onRefresh; +} + +function* assertPseudoAdded(inspector, view, pseudoClass, numRules, + childIndex) { + info("Check that the ruleview contains the pseudo-class rule"); + is(view.element.children.length, numRules, + "Should have " + numRules + " rules."); + is(getRuleViewRuleEditor(view, childIndex).rule.selectorText, + "div" + pseudoClass, "rule view is showing " + pseudoClass + " rule"); +} + +function* assertPseudoRemoved(inspector, view, numRules) { + info("Check that the ruleview no longer contains the pseudo-class rule"); + is(view.element.children.length, numRules, + "Should have " + numRules + " rules."); + is(getRuleViewRuleEditor(view, 1).rule.selectorText, "div", + "Second rule is div"); +} + +function* assertPseudoPanelOpened(view) { + info("Check the opened state of the pseudo class panel"); + + ok(!view.pseudoClassPanel.hidden, "Pseudo Class Panel Opened"); + ok(!view.hoverCheckbox.disabled, ":hover checkbox is not disabled"); + ok(!view.activeCheckbox.disabled, ":active checkbox is not disabled"); + ok(!view.focusCheckbox.disabled, ":focus checkbox is not disabled"); + + is(view.hoverCheckbox.getAttribute("tabindex"), "0", + ":hover checkbox has a tabindex of 0"); + is(view.activeCheckbox.getAttribute("tabindex"), "0", + ":active checkbox has a tabindex of 0"); + is(view.focusCheckbox.getAttribute("tabindex"), "0", + ":focus checkbox has a tabindex of 0"); +} + +function* assertPseudoPanelClosed(view) { + info("Check the closed state of the pseudo clas panel"); + + ok(view.pseudoClassPanel.hidden, "Pseudo Class Panel Hidden"); + + is(view.hoverCheckbox.getAttribute("tabindex"), "-1", + ":hover checkbox has a tabindex of -1"); + is(view.activeCheckbox.getAttribute("tabindex"), "-1", + ":active checkbox has a tabindex of -1"); + is(view.focusCheckbox.getAttribute("tabindex"), "-1", + ":focus checkbox has a tabindex of -1"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js b/devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js new file mode 100644 index 000000000..25ea3d972 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js @@ -0,0 +1,39 @@ +/* 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 does not go blank while selecting a new node. + +const TESTCASE_URI = "data:text/html;charset=utf-8," + + "<div id=\"testdiv\" style=\"font-size:10px;\">" + + "Test div!</div>"; + +add_task(function* () { + yield addTab(TESTCASE_URI); + + info("Opening the rule view and selecting the test node"); + let {inspector, view} = yield openRuleView(); + let testdiv = yield getNodeFront("#testdiv", inspector); + yield selectNode(testdiv, inspector); + + let htmlBefore = view.element.innerHTML; + ok(htmlBefore.indexOf("font-size") > -1, + "The rule view should contain a font-size property."); + + // Do the selectNode call manually, because otherwise it's hard to guarantee + // that we can make the below checks at a reasonable time. + info("refreshing the node"); + let p = view.selectElement(testdiv, true); + is(view.element.innerHTML, htmlBefore, + "The rule view is unchanged during selection."); + ok(view.element.classList.contains("non-interactive"), + "The rule view is marked non-interactive."); + yield p; + + info("node refreshed"); + ok(!view.element.classList.contains("non-interactive"), + "The rule view is marked interactive again."); +}); + diff --git a/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.js b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.js new file mode 100644 index 000000000..381a6bda2 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.js @@ -0,0 +1,61 @@ +/* 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 changing the current element's attributes refreshes the rule-view + +const TEST_URI = ` + <style type="text/css"> + #testid { + background-color: blue; + } + .testclass { + background-color: green; + } + </style> + <div id="testid" class="testclass" style="margin-top: 1px; padding-top: 5px;"> + Styled Node + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Checking that the rule-view has the element, #testid and " + + ".testclass selectors"); + checkRuleViewContent(view, ["element", "#testid", ".testclass"]); + + info("Changing the node's ID attribute and waiting for the " + + "rule-view refresh"); + let ruleViewRefreshed = inspector.once("rule-view-refreshed"); + yield testActor.setAttribute("#testid", "id", "differentid"); + yield ruleViewRefreshed; + + info("Checking that the rule-view doesn't have the #testid selector anymore"); + checkRuleViewContent(view, ["element", ".testclass"]); + + info("Reverting the ID attribute change"); + ruleViewRefreshed = inspector.once("rule-view-refreshed"); + yield testActor.setAttribute("#differentid", "id", "testid"); + yield ruleViewRefreshed; + + info("Checking that the rule-view has all the selectors again"); + checkRuleViewContent(view, ["element", "#testid", ".testclass"]); +}); + +function checkRuleViewContent(view, expectedSelectors) { + let selectors = view.styleDocument + .querySelectorAll(".ruleview-selectorcontainer"); + + is(selectors.length, expectedSelectors.length, + expectedSelectors.length + " selectors are displayed"); + + for (let i = 0; i < expectedSelectors.length; i++) { + is(selectors[i].textContent.indexOf(expectedSelectors[i]), 0, + "Selector " + (i + 1) + " is " + expectedSelectors[i]); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_02.js b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_02.js new file mode 100644 index 000000000..6ee385faa --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_02.js @@ -0,0 +1,153 @@ +/* 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 changing the current element's style attribute refreshes the +// rule-view + +const TEST_URI = ` + <div id="testid" class="testclass" style="margin-top: 1px; padding-top: 5px;"> + Styled Node + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = yield openRuleView(); + yield selectNode("#testid", inspector); + + yield testPropertyChanges(inspector, view); + yield testPropertyChange0(inspector, view, "#testid", testActor); + yield testPropertyChange1(inspector, view, "#testid", testActor); + yield testPropertyChange2(inspector, view, "#testid", testActor); + yield testPropertyChange3(inspector, view, "#testid", testActor); + yield testPropertyChange4(inspector, view, "#testid", testActor); + yield testPropertyChange5(inspector, view, "#testid", testActor); + yield testPropertyChange6(inspector, view, "#testid", testActor); +}); + +function* testPropertyChanges(inspector, ruleView) { + info("Adding a second margin-top value in the element selector"); + let ruleEditor = ruleView._elementStyle.rules[0].editor; + let onRefreshed = inspector.once("rule-view-refreshed"); + ruleEditor.addProperty("margin-top", "5px", "", true); + yield onRefreshed; + + let rule = ruleView._elementStyle.rules[0]; + validateTextProp(rule.textProps[0], false, "margin-top", "1px", + "Original margin property active"); +} + +function* testPropertyChange0(inspector, ruleView, selector, testActor) { + yield changeElementStyle(selector, "margin-top: 1px; padding-top: 5px", + inspector, testActor); + + let rule = ruleView._elementStyle.rules[0]; + is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, + "Correct number of properties"); + validateTextProp(rule.textProps[0], true, "margin-top", "1px", + "First margin property re-enabled"); + validateTextProp(rule.textProps[2], false, "margin-top", "5px", + "Second margin property disabled"); +} + +function* testPropertyChange1(inspector, ruleView, selector, testActor) { + info("Now set it back to 5px, the 5px value should be re-enabled."); + yield changeElementStyle(selector, "margin-top: 5px; padding-top: 5px;", + inspector, testActor); + + let rule = ruleView._elementStyle.rules[0]; + is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, + "Correct number of properties"); + validateTextProp(rule.textProps[0], false, "margin-top", "1px", + "First margin property re-enabled"); + validateTextProp(rule.textProps[2], true, "margin-top", "5px", + "Second margin property disabled"); +} + +function* testPropertyChange2(inspector, ruleView, selector, testActor) { + info("Set the margin property to a value that doesn't exist in the editor."); + info("Should reuse the currently-enabled element (the second one.)"); + yield changeElementStyle(selector, "margin-top: 15px; padding-top: 5px;", + inspector, testActor); + + let rule = ruleView._elementStyle.rules[0]; + is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, + "Correct number of properties"); + validateTextProp(rule.textProps[0], false, "margin-top", "1px", + "First margin property re-enabled"); + validateTextProp(rule.textProps[2], true, "margin-top", "15px", + "Second margin property disabled"); +} + +function* testPropertyChange3(inspector, ruleView, selector, testActor) { + info("Remove the padding-top attribute. Should disable the padding " + + "property but not remove it."); + yield changeElementStyle(selector, "margin-top: 5px;", inspector, testActor); + + let rule = ruleView._elementStyle.rules[0]; + is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, + "Correct number of properties"); + validateTextProp(rule.textProps[1], false, "padding-top", "5px", + "Padding property disabled"); +} + +function* testPropertyChange4(inspector, ruleView, selector, testActor) { + info("Put the padding-top attribute back in, should re-enable the " + + "padding property."); + yield changeElementStyle(selector, "margin-top: 5px; padding-top: 25px", + inspector, testActor); + + let rule = ruleView._elementStyle.rules[0]; + is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, + "Correct number of properties"); + validateTextProp(rule.textProps[1], true, "padding-top", "25px", + "Padding property enabled"); +} + +function* testPropertyChange5(inspector, ruleView, selector, testActor) { + info("Add an entirely new property"); + yield changeElementStyle(selector, + "margin-top: 5px; padding-top: 25px; padding-left: 20px;", + inspector, testActor); + + let rule = ruleView._elementStyle.rules[0]; + is(rule.editor.element.querySelectorAll(".ruleview-property").length, 4, + "Added a property"); + validateTextProp(rule.textProps[3], true, "padding-left", "20px", + "Padding property enabled"); +} + +function* testPropertyChange6(inspector, ruleView, selector, testActor) { + info("Add an entirely new property again"); + yield changeElementStyle(selector, "background: red " + + "url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%", + inspector, testActor); + + 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"); +} + +function* changeElementStyle(selector, style, inspector, testActor) { + let onRefreshed = inspector.once("rule-view-refreshed"); + yield testActor.setAttribute(selector, "style", style); + yield onRefreshed; +} + +function validateTextProp(prop, enabled, name, value, desc) { + is(prop.enabled, enabled, desc + ": enabled."); + is(prop.name, name, desc + ": name."); + is(prop.value, value, desc + ": value."); + + is(prop.editor.enable.hasAttribute("checked"), enabled, + desc + ": enabled checkbox."); + is(prop.editor.nameSpan.textContent, name, desc + ": name span."); + is(prop.editor.valueSpan.textContent, + value, desc + ": value span."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js b/devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js new file mode 100644 index 000000000..81ff9d4d5 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js @@ -0,0 +1,38 @@ +/* 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 refreshes when the current node has its style +// changed + +const TEST_URI = "<div id='testdiv' style='font-size: 10px;''>Test div!</div>"; + +add_task(function* () { + Services.prefs.setCharPref("devtools.defaultColorUnit", "name"); + + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = yield openRuleView(); + yield selectNode("#testdiv", inspector); + + let fontSize = getRuleViewPropertyValue(view, "element", "font-size"); + is(fontSize, "10px", "The rule view shows the right font-size"); + + info("Changing the node's style and waiting for the update"); + let onUpdated = inspector.once("rule-view-refreshed"); + yield testActor.setAttribute("#testdiv", "style", + "font-size: 3em; color: lightgoldenrodyellow; " + + "text-align: right; text-transform: uppercase"); + yield onUpdated; + + let textAlign = getRuleViewPropertyValue(view, "element", "text-align"); + is(textAlign, "right", "The rule view shows the new text align."); + let color = getRuleViewPropertyValue(view, "element", "color"); + is(color, "lightgoldenrodyellow", "The rule view shows the new color."); + fontSize = getRuleViewPropertyValue(view, "element", "font-size"); + is(fontSize, "3em", "The rule view shows the new font size."); + let textTransform = getRuleViewPropertyValue(view, "element", + "text-transform"); + is(textTransform, "uppercase", "The rule view shows the new text transform."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js new file mode 100644 index 000000000..f4c47bba0 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js @@ -0,0 +1,156 @@ +/* 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 search filter and clear button works properly in +// the computed list. + +const TEST_URI = ` + <style type="text/css"> + #testid { + margin: 4px 0px; + } + .testclass { + background-color: red; + } + </style> + <h1 id="testid" class="testclass">Styled Node</h1> +`; + +const TEST_DATA = [ + { + desc: "Tests that the search filter works properly in the computed list " + + "for property names", + search: "margin", + isExpanderOpen: false, + isFilterOpen: false, + isMarginHighlighted: true, + isMarginTopHighlighted: true, + isMarginRightHighlighted: true, + isMarginBottomHighlighted: true, + isMarginLeftHighlighted: true + }, + { + desc: "Tests that the search filter works properly in the computed list " + + "for property values", + search: "0px", + isExpanderOpen: false, + isFilterOpen: false, + isMarginHighlighted: true, + isMarginTopHighlighted: false, + isMarginRightHighlighted: true, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: true + }, + { + desc: "Tests that the search filter works properly in the computed list " + + "for property line input", + search: "margin-top:4px", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: true, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: false + }, + { + desc: "Tests that the search filter works properly in the computed list " + + "for parsed name", + search: "margin-top:", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: true, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: false + }, + { + desc: "Tests that the search filter works properly in the computed list " + + "for parsed property value", + search: ":4px", + isExpanderOpen: false, + isFilterOpen: false, + isMarginHighlighted: true, + isMarginTopHighlighted: true, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: true, + isMarginLeftHighlighted: false + } +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + for (let data of TEST_DATA) { + info(data.desc); + yield setSearchFilter(view, data.search); + yield checkRules(view, data); + yield clearSearchAndCheckRules(view); + } +} + +function* checkRules(view, data) { + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let textPropEditor = rule.textProps[0].editor; + let computed = textPropEditor.computed; + + is(rule.selectorText, "#testid", "Second rule is #testid."); + is(!!textPropEditor.expander.getAttribute("open"), data.isExpanderOpen, + "Got correct expander state."); + is(computed.hasAttribute("filter-open"), data.isFilterOpen, + "Got correct expanded state for margin computed list."); + is(textPropEditor.container.classList.contains("ruleview-highlight"), + data.isMarginHighlighted, + "Got correct highlight for margin text property."); + + is(computed.children[0].classList.contains("ruleview-highlight"), + data.isMarginTopHighlighted, + "Got correct highlight for margin-top computed property."); + is(computed.children[1].classList.contains("ruleview-highlight"), + data.isMarginRightHighlighted, + "Got correct highlight for margin-right computed property."); + is(computed.children[2].classList.contains("ruleview-highlight"), + data.isMarginBottomHighlighted, + "Got correct highlight for margin-bottom computed property."); + is(computed.children[3].classList.contains("ruleview-highlight"), + data.isMarginLeftHighlighted, + "Got correct highlight for margin-left computed property."); +} + +function* clearSearchAndCheckRules(view) { + let win = view.styleWindow; + let searchField = view.searchField; + let searchClearButton = view.searchClearButton; + + let rule = getRuleViewRuleEditor(view, 1).rule; + let textPropEditor = rule.textProps[0].editor; + let computed = textPropEditor.computed; + + info("Clearing the search filter"); + EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win); + yield view.inspector.once("ruleview-filtered"); + + info("Check the search filter is cleared and no rules are highlighted"); + is(view.element.children.length, 3, "Should have 3 rules."); + ok(!searchField.value, "Search filter is cleared"); + ok(!view.styleDocument.querySelectorAll(".ruleview-highlight").length, + "No rules are higlighted"); + + ok(!textPropEditor.expander.getAttribute("open"), "Expander is closed."); + ok(!computed.hasAttribute("filter-open"), + "margin computed list is closed."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js new file mode 100644 index 000000000..911f09ff3 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js @@ -0,0 +1,93 @@ +/* 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 search filter works properly in the computed list +// when modifying the existing search filter value + +const SEARCH = "margin-"; + +const TEST_URI = ` + <style type="text/css"> + #testid { + margin: 4px 0px; + } + .testclass { + background-color: red; + } + </style> + <h1 id="testid" class="testclass">Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); + yield testRemoveTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let ruleEditor = rule.textProps[0].editor; + let computed = ruleEditor.computed; + + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(ruleEditor.expander.getAttribute("open"), "Expander is open."); + ok(!ruleEditor.container.classList.contains("ruleview-highlight"), + "margin text property is not highlighted."); + ok(computed.hasAttribute("filter-open"), "margin computed list is open."); + + ok(computed.children[0].classList.contains("ruleview-highlight"), + "margin-top computed property is correctly highlighted."); + ok(computed.children[1].classList.contains("ruleview-highlight"), + "margin-right computed property is correctly highlighted."); + ok(computed.children[2].classList.contains("ruleview-highlight"), + "margin-bottom computed property is correctly highlighted."); + ok(computed.children[3].classList.contains("ruleview-highlight"), + "margin-left computed property is correctly highlighted."); +} + +function* testRemoveTextInFilter(inspector, view) { + info("Press backspace and set filter text to \"margin\""); + + let win = view.styleWindow; + let searchField = view.searchField; + + searchField.focus(); + EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win); + yield inspector.once("ruleview-filtered"); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let ruleEditor = rule.textProps[0].editor; + let computed = ruleEditor.computed; + + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(!ruleEditor.expander.getAttribute("open"), "Expander is closed."); + ok(ruleEditor.container.classList.contains("ruleview-highlight"), + "margin text property is correctly highlighted."); + ok(!computed.hasAttribute("filter-open"), "margin computed list is closed."); + + ok(computed.children[0].classList.contains("ruleview-highlight"), + "margin-top computed property is correctly highlighted."); + ok(computed.children[1].classList.contains("ruleview-highlight"), + "margin-right computed property is correctly highlighted."); + ok(computed.children[2].classList.contains("ruleview-highlight"), + "margin-bottom computed property is correctly highlighted."); + ok(computed.children[3].classList.contains("ruleview-highlight"), + "margin-left computed property is correctly highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.js new file mode 100644 index 000000000..1d8063419 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.js @@ -0,0 +1,49 @@ +/* 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 search filter works properly in the computed list +// for color values. + +// 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: 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(); + yield selectNode(".testclass", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let ruleEditor = rule.textProps[0].editor; + let computed = ruleEditor.computed; + + is(rule.selectorText, ".testclass", "Second rule is .testclass."); + ok(ruleEditor.expander.getAttribute("open"), "Expander is open."); + ok(!ruleEditor.container.classList.contains("ruleview-highlight"), + "background property is not highlighted."); + ok(computed.hasAttribute("filter-open"), "background computed list is open."); + ok(computed.children[0].classList.contains("ruleview-highlight"), + "background-color computed property is highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.js new file mode 100644 index 000000000..05b8b01eb --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.js @@ -0,0 +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"; + +// Tests that the rule view search filter works properly in the computed list +// for newly modified property values. + +const SEARCH = "0px"; + +const TEST_URI = ` + <style type='text/css'> + #testid { + margin: 4px; + top: 0px; + } + </style> + <h1 id='testid'>Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testModifyPropertyValueFilter(inspector, view); +}); + +function* testModifyPropertyValueFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let propEditor = rule.textProps[0].editor; + let computed = propEditor.computed; + let editor = yield focusEditableField(view, propEditor.valueSpan); + + info("Check that the correct rules are visible"); + 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 = 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."); + ok(!computed.children[0].classList.contains("ruleview-highlight"), + "margin-top computed property is not highlighted."); + ok(computed.children[1].classList.contains("ruleview-highlight"), + "margin-right computed property is correctly highlighted."); + ok(!computed.children[2].classList.contains("ruleview-highlight"), + "margin-bottom computed property is not highlighted."); + ok(computed.children[3].classList.contains("ruleview-highlight"), + "margin-left computed property is correctly highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.js new file mode 100644 index 000000000..c8b1e0869 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.js @@ -0,0 +1,92 @@ +/* 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 expanded computed list for a property remains open after +// clearing the rule view search filter. + +const SEARCH = "0px"; + +const TEST_URI = ` + <style type="text/css"> + #testid { + margin: 4px 0px; + } + .testclass { + background-color: red; + } + </style> + <h1 id="testid" class="testclass">Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testOpenExpanderAndAddTextInFilter(inspector, view); + yield testClearSearchFilter(inspector, view); +}); + +function* testOpenExpanderAndAddTextInFilter(inspector, view) { + let rule = getRuleViewRuleEditor(view, 1).rule; + let ruleEditor = rule.textProps[0].editor; + let computed = ruleEditor.computed; + + info("Opening the computed list of margin property"); + ruleEditor.expander.click(); + + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(ruleEditor.expander.getAttribute("open"), "Expander is open."); + ok(ruleEditor.container.classList.contains("ruleview-highlight"), + "margin text property is correctly highlighted."); + ok(!computed.hasAttribute("filter-open"), + "margin computed list does not contain filter-open class."); + ok(computed.hasAttribute("user-open"), + "margin computed list contains user-open attribute."); + + ok(!computed.children[0].classList.contains("ruleview-highlight"), + "margin-top computed property is not highlighted."); + ok(computed.children[1].classList.contains("ruleview-highlight"), + "margin-right computed property is correctly highlighted."); + ok(!computed.children[2].classList.contains("ruleview-highlight"), + "margin-bottom computed property is not highlighted."); + ok(computed.children[3].classList.contains("ruleview-highlight"), + "margin-left computed property is correctly highlighted."); +} + +function* testClearSearchFilter(inspector, view) { + info("Clearing the search filter"); + + let searchField = view.searchField; + let searchClearButton = view.searchClearButton; + let onRuleViewFiltered = inspector.once("ruleview-filtered"); + + EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, + view.styleWindow); + + yield onRuleViewFiltered; + + info("Check the search filter is cleared and no rules are highlighted"); + is(view.element.children.length, 3, "Should have 3 rules."); + ok(!searchField.value, "Search filter is cleared"); + ok(!view.styleDocument.querySelectorAll(".ruleview-highlight").length, + "No rules are higlighted"); + + let ruleEditor = getRuleViewRuleEditor(view, 1).rule.textProps[0].editor; + let computed = ruleEditor.computed; + + ok(ruleEditor.expander.getAttribute("open"), "Expander is open."); + ok(!computed.hasAttribute("filter-open"), + "margin computed list does not contain filter-open class."); + ok(computed.hasAttribute("user-open"), + "margin computed list contains user-open attribute."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.js new file mode 100644 index 000000000..3e634b76e --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.js @@ -0,0 +1,74 @@ +/* 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 overriden search filter works properly for +// overridden properties. + +const TEST_URI = ` + <style type='text/css'> + #testid { + width: 100%; + } + h1 { + width: 50%; + } + </style> + <h1 id='testid' class='testclass'>Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testFilterOverriddenProperty(inspector, view); +}); + +function* testFilterOverriddenProperty(inspector, ruleView) { + info("Check that the correct rules are visible"); + is(ruleView.element.children.length, 3, "Should have 3 rules."); + + let rule = getRuleViewRuleEditor(ruleView, 1).rule; + let textPropEditor = rule.textProps[0].editor; + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(!textPropEditor.element.classList.contains("ruleview-overridden"), + "width property is not overridden."); + ok(textPropEditor.filterProperty.hidden, + "Overridden search button is hidden."); + + rule = getRuleViewRuleEditor(ruleView, 2).rule; + textPropEditor = rule.textProps[0].editor; + is(rule.selectorText, "h1", "Third rule is h1."); + ok(textPropEditor.element.classList.contains("ruleview-overridden"), + "width property is overridden."); + ok(!textPropEditor.filterProperty.hidden, + "Overridden search button is not hidden."); + + let searchField = ruleView.searchField; + let onRuleViewFiltered = inspector.once("ruleview-filtered"); + + info("Click the overridden search"); + textPropEditor.filterProperty.click(); + yield onRuleViewFiltered; + + info("Check that the overridden search is applied"); + is(searchField.value, "`width`", "The search field value is width."); + + rule = getRuleViewRuleEditor(ruleView, 1).rule; + textPropEditor = rule.textProps[0].editor; + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(textPropEditor.container.classList.contains("ruleview-highlight"), + "width property is correctly highlighted."); + + rule = getRuleViewRuleEditor(ruleView, 2).rule; + textPropEditor = rule.textProps[0].editor; + is(rule.selectorText, "h1", "Third rule is h1."); + ok(textPropEditor.container.classList.contains("ruleview-highlight"), + "width property is correctly highlighted."); + ok(textPropEditor.element.classList.contains("ruleview-overridden"), + "width property is overridden."); + ok(!textPropEditor.filterProperty.hidden, + "Overridden search button is not hidden."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_01.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_01.js new file mode 100644 index 000000000..4dd1c951d --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_01.js @@ -0,0 +1,91 @@ +/* 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 search filter and clear button works properly. + +const TEST_URI = ` + <style type="text/css"> + #testid, h1 { + background-color: #00F !important; + } + .testclass { + width: 100%; + } + </style> + <h1 id="testid" class="testclass">Styled Node</h1> +`; + +const TEST_DATA = [ + { + desc: "Tests that the search filter works properly for property names", + search: "color" + }, + { + desc: "Tests that the search filter works properly for property values", + search: "00F" + }, + { + desc: "Tests that the search filter works properly for property line input", + search: "background-color:#00F" + }, + { + desc: "Tests that the search filter works properly for parsed property " + + "names", + search: "background:" + }, + { + desc: "Tests that the search filter works properly for parsed property " + + "values", + search: ":00F" + }, +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + for (let data of TEST_DATA) { + info(data.desc); + yield setSearchFilter(view, data.search); + yield checkRules(view); + yield clearSearchAndCheckRules(view); + } +} + +function* checkRules(view) { + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + + is(rule.selectorText, "#testid, h1", "Second rule is #testid, h1."); + ok(rule.textProps[0].editor.container.classList + .contains("ruleview-highlight"), + "background-color text property is correctly highlighted."); +} + +function* clearSearchAndCheckRules(view) { + let doc = view.styleDocument; + let win = view.styleWindow; + let searchField = view.searchField; + let searchClearButton = view.searchClearButton; + + info("Clearing the search filter"); + EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win); + yield view.inspector.once("ruleview-filtered"); + + info("Check the search filter is cleared and no rules are highlighted"); + is(view.element.children.length, 3, "Should have 3 rules."); + ok(!searchField.value, "Search filter is cleared."); + ok(!doc.querySelectorAll(".ruleview-highlight").length, + "No rules are higlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_02.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_02.js new file mode 100644 index 000000000..c23e7be62 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_02.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"; + +// Tests that the rule view search filter works properly for keyframe rule +// selectors. + +const SEARCH = "20%"; +const TEST_URI = URL_ROOT + "doc_keyframeanimation.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield selectNode("#boxy", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let ruleEditor = getRuleViewRuleEditor(view, 2, 0); + + is(ruleEditor.rule.domRule.keyText, "20%", "Second rule is 20%."); + ok(ruleEditor.selectorText.classList.contains("ruleview-highlight"), + "20% selector is highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_03.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_03.js new file mode 100644 index 000000000..89280f0eb --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_03.js @@ -0,0 +1,39 @@ +/* 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 search filter works properly for inline styles. + +const SEARCH = "color"; + +const TEST_URI = ` + <style type="text/css"> + #testid { + width: 100%; + } + </style> + <div id="testid" style="background-color:aliceblue">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 testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 1, "Should have 1 rule."); + + let rule = getRuleViewRuleEditor(view, 0).rule; + + is(rule.selectorText, "element", "First rule is inline element."); + ok(rule.textProps[0].editor.container.classList + .contains("ruleview-highlight"), + "background-color text property is correctly highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_04.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_04.js new file mode 100644 index 000000000..5804d74ac --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_04.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 the rule view search filter works properly when modifying the +// existing search filter value. + +const SEARCH = "00F"; + +const TEST_URI = ` + <style type="text/css"> + #testid { + background-color: #00F; + } + .testclass { + width: 100%; + } + </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 testAddTextInFilter(inspector, view); + yield testRemoveTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(rule.textProps[0].editor.container.classList + .contains("ruleview-highlight"), + "background-color text property is correctly highlighted."); +} + +function* testRemoveTextInFilter(inspector, view) { + info("Press backspace and set filter text to \"00\""); + + let win = view.styleWindow; + let searchField = view.searchField; + + searchField.focus(); + EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win); + yield inspector.once("ruleview-filtered"); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 3, "Should have 3 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(rule.textProps[0].editor.container.classList + .contains("ruleview-highlight"), + "background-color text property is correctly highlighted."); + + rule = getRuleViewRuleEditor(view, 2).rule; + + is(rule.selectorText, ".testclass", "Second rule is .testclass."); + ok(rule.textProps[0].editor.container.classList + .contains("ruleview-highlight"), + "width text property is correctly highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_05.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_05.js new file mode 100644 index 000000000..9388dd47e --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_05.js @@ -0,0 +1,33 @@ +/* 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 search filter works properly for stylesheet source. + +const SEARCH = "doc_urls_clickable.css"; +const TEST_URI = URL_ROOT + "doc_urls_clickable.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield selectNode(".relative1", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let source = rule.textProps[0].editor.ruleEditor.source; + + is(rule.selectorText, ".relative1", "Second rule is .relative1."); + ok(source.classList.contains("ruleview-highlight"), + "stylesheet source is correctly highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_06.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_06.js new file mode 100644 index 000000000..67b02ab73 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_06.js @@ -0,0 +1,27 @@ +/* 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 search filter does not highlight the source with +// input that could be parsed as a property line. + +const SEARCH = "doc_urls_clickable.css: url"; +const TEST_URI = URL_ROOT + "doc_urls_clickable.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield selectNode(".relative1", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 1, "Should have 1 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_07.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_07.js new file mode 100644 index 000000000..16b047d8d --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_07.js @@ -0,0 +1,62 @@ +/* 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 search filter works properly for newly modified +// property name. + +const SEARCH = "e"; + +const TEST_URI = ` + <style type='text/css'> + #testid { + width: 100%; + height: 50%; + } + </style> + <h1 id='testid'>Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Enter the test value in the search filter"); + yield setSearchFilter(view, SEARCH); + + info("Focus the width property name"); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let rule = ruleEditor.rule; + let propEditor = rule.textProps[0].editor; + yield focusEditableField(view, propEditor.nameSpan); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(!propEditor.container.classList.contains("ruleview-highlight"), + "width text property is not highlighted."); + ok(rule.textProps[1].editor.container.classList + .contains("ruleview-highlight"), + "height text property is correctly highlighted."); + + info("Change the width property to margin-left"); + EventUtils.sendString("margin-left", view.styleWindow); + + info("Submit the change"); + let onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + ok(propEditor.container.classList.contains("ruleview-highlight"), + "margin-left text property is correctly highlighted."); + + // After pressing return on the property name, the value has been focused + // automatically. Blur it now and wait for the rule-view to refresh to avoid + // pending requests. + onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield onRuleViewChanged; +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_08.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_08.js new file mode 100644 index 000000000..1a3c0de59 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_08.js @@ -0,0 +1,53 @@ +/* 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 search filter works properly for newly modified +// property value. + +const SEARCH = "100%"; + +const TEST_URI = ` + <style type='text/css'> + #testid { + width: 100%; + height: 50%; + } + </style> + <h1 id='testid'>Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Enter the test value in the search filter"); + yield setSearchFilter(view, SEARCH); + + info("Focus the height property value"); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let rule = ruleEditor.rule; + let propEditor = rule.textProps[1].editor; + yield focusEditableField(view, propEditor.valueSpan); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(rule.textProps[0].editor.container.classList + .contains("ruleview-highlight"), + "width text property is correctly highlighted."); + ok(!propEditor.container.classList.contains("ruleview-highlight"), + "height text property is not highlighted."); + + info("Change the height property value to 100%"); + let onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.sendString("100%", view.styleWindow); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + ok(propEditor.container.classList.contains("ruleview-highlight"), + "height text property is correctly highlighted."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js new file mode 100644 index 000000000..620e5d336 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js @@ -0,0 +1,73 @@ +/* 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 search filter works properly for newly added +// property. + +const SEARCH = "100%"; + +const TEST_URI = ` + <style type='text/css'> + #testid { + width: 100%; + height: 50%; + } + </style> + <h1 id='testid'>Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Enter the test value in the search filter"); + yield setSearchFilter(view, SEARCH); + + info("Start entering a new property in the rule"); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let rule = ruleEditor.rule; + let editor = yield focusNewRuleViewProperty(ruleEditor); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(rule.textProps[0].editor.container.classList + .contains("ruleview-highlight"), + "width text property is correctly highlighted."); + ok(!rule.textProps[1].editor.container.classList + .contains("ruleview-highlight"), + "height text property is not highlighted."); + + info("Test creating a new property"); + + info("Entering margin-left in the property name editor"); + // Changing the value doesn't cause a rule-view refresh, no need to wait for + // ruleview-changed here. + editor.input.value = "margin-left"; + + info("Pressing return to commit and focus the new value field"); + let onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onRuleViewChanged; + + // Getting the new value editor after focus + editor = inplaceEditor(view.styleDocument.activeElement); + let propEditor = ruleEditor.rule.textProps[2].editor; + + info("Entering a value and bluring the field to expect a rule change"); + onRuleViewChanged = view.once("ruleview-changed"); + editor.input.value = "100%"; + view.throttle.flush(); + yield onRuleViewChanged; + + onRuleViewChanged = view.once("ruleview-changed"); + editor.input.blur(); + yield onRuleViewChanged; + + ok(propEditor.container.classList.contains("ruleview-highlight"), + "margin-left text property is correctly highlighted."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_10.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_10.js new file mode 100644 index 000000000..ac336591d --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_10.js @@ -0,0 +1,84 @@ +/* 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 search filter works properly for rule selectors. + +const TEST_URI = ` + <style type="text/css"> + html, body, div { + background-color: #00F; + } + #testid { + width: 100%; + } + </style> + <div id="testid" class="testclass">Styled Node</div> +`; + +const TEST_DATA = [ + { + desc: "Tests that the search filter works properly for a single rule " + + "selector", + search: "#test", + selectorText: "#testid", + index: 0 + }, + { + desc: "Tests that the search filter works properly for multiple rule " + + "selectors", + search: "body", + selectorText: "html, body, div", + index: 2 + } +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + for (let data of TEST_DATA) { + info(data.desc); + yield setSearchFilter(view, data.search); + yield checkRules(view, data); + yield clearSearchAndCheckRules(view); + } +} + +function* checkRules(view, data) { + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + is(ruleEditor.rule.selectorText, data.selectorText, + "Second rule is " + data.selectorText + "."); + ok(ruleEditor.selectorText.children[data.index].classList + .contains("ruleview-highlight"), + data.selectorText + " selector is highlighted."); +} + +function* clearSearchAndCheckRules(view) { + let doc = view.styleDocument; + let win = view.styleWindow; + let searchField = view.searchField; + let searchClearButton = view.searchClearButton; + + info("Clearing the search filter"); + EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win); + yield view.inspector.once("ruleview-filtered"); + + info("Check the search filter is cleared and no rules are highlighted"); + is(view.element.children.length, 3, "Should have 3 rules."); + ok(!searchField.value, "Search filter is cleared."); + ok(!doc.querySelectorAll(".ruleview-highlight").length, + "No rules are higlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js new file mode 100644 index 000000000..349f1b9b3 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js @@ -0,0 +1,83 @@ +/* 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 rule view search filter context menu works properly. + +const TEST_INPUT = "h1"; +const TEST_URI = "<h1>test filter context menu</h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view} = yield openRuleView(); + yield selectNode("h1", inspector); + + let win = view.styleWindow; + let searchField = view.searchField; + let searchContextMenu = toolbox.textBoxContextMenuPopup; + ok(searchContextMenu, + "The search filter context menu is loaded in the rule view"); + + let cmdUndo = searchContextMenu.querySelector("[command=cmd_undo]"); + let cmdDelete = searchContextMenu.querySelector("[command=cmd_delete]"); + let cmdSelectAll = searchContextMenu.querySelector("[command=cmd_selectAll]"); + let cmdCut = searchContextMenu.querySelector("[command=cmd_cut]"); + let cmdCopy = searchContextMenu.querySelector("[command=cmd_copy]"); + let cmdPaste = searchContextMenu.querySelector("[command=cmd_paste]"); + + info("Opening context menu"); + + emptyClipboard(); + + let onFocus = once(searchField, "focus"); + searchField.focus(); + yield onFocus; + + let onContextMenuPopup = once(searchContextMenu, "popupshowing"); + EventUtils.synthesizeMouse(searchField, 2, 2, + {type: "contextmenu", button: 2}, win); + yield onContextMenuPopup; + + is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled"); + is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled"); + is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled"); + + // Cut/Copy items are enabled in context menu even if there + // is no selection. See also Bug 1303033 + is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled"); + is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled"); + + if (isWindows()) { + // emptyClipboard only works on Windows (666254), assert paste only for this OS. + is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled"); + } + + info("Closing context menu"); + let onContextMenuHidden = once(searchContextMenu, "popuphidden"); + searchContextMenu.hidePopup(); + yield onContextMenuHidden; + + info("Copy text in search field using the context menu"); + searchField.value = TEST_INPUT; + searchField.select(); + EventUtils.synthesizeMouse(searchField, 2, 2, + {type: "contextmenu", button: 2}, win); + yield onContextMenuPopup; + yield waitForClipboardPromise(() => cmdCopy.click(), TEST_INPUT); + searchContextMenu.hidePopup(); + yield onContextMenuHidden; + + info("Reopen context menu and check command properties"); + EventUtils.synthesizeMouse(searchField, 2, 2, + {type: "contextmenu", button: 2}, win); + yield onContextMenuPopup; + + is(cmdUndo.getAttribute("disabled"), "", "cmdUndo is enabled"); + is(cmdDelete.getAttribute("disabled"), "", "cmdDelete is enabled"); + is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled"); + is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled"); + is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled"); + is(cmdPaste.getAttribute("disabled"), "", "cmdPaste is enabled"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.js new file mode 100644 index 000000000..21848dce8 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.js @@ -0,0 +1,65 @@ +/* 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 search filter escape keypress will clear the search +// field. + +const SEARCH = "00F"; + +const TEST_URI = ` + <style type="text/css"> + #testid { + background-color: #00F; + } + .testclass { + width: 100%; + } + </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 testAddTextInFilter(inspector, view); + yield testEscapeKeypress(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(rule.textProps[0].editor.container.classList + .contains("ruleview-highlight"), + "background-color text property is correctly highlighted."); +} + +function* testEscapeKeypress(inspector, view) { + info("Pressing the escape key on search filter"); + + let doc = view.styleDocument; + let win = view.styleWindow; + let searchField = view.searchField; + let onRuleViewFiltered = inspector.once("ruleview-filtered"); + + searchField.focus(); + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + yield onRuleViewFiltered; + + info("Check the search filter is cleared and no rules are highlighted"); + is(view.element.children.length, 3, "Should have 3 rules."); + ok(!searchField.value, "Search filter is cleared"); + ok(!doc.querySelectorAll(".ruleview-highlight").length, + "No rules are higlighted"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js b/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js new file mode 100644 index 000000000..b3f4ef364 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js @@ -0,0 +1,171 @@ +/* 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 properties can be selected and copied from the rule view + +const osString = Services.appinfo.OS; + +const TEST_URI = ` + <style type="text/css"> + html { + color: #000000; + } + span { + font-variant: small-caps; color: #000000; + } + .nomatches { + color: #ff0000; + } + </style> + <div id="first" style="margin: 10em; + font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA"> + <h1>Some header text</h1> + <p id="salutation" style="font-size: 12pt">hi.</p> + <p id="body" style="font-size: 12pt">I am a test-case. This text exists + solely to provide some things to <span style="color: yellow"> + highlight</span> and <span style="font-weight: bold">count</span> + style list-items in the box at right. If you are reading this, + you should go do something else instead. Maybe read a book. Or better + yet, write some test-cases for another bit of code. + <span style="font-style: italic">some text</span></p> + <p id="closing">more text</p> + <p>even more text</p> + </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 checkCopySelection(view); + yield checkSelectAll(view); + yield checkCopyEditorValue(view); +}); + +function* checkCopySelection(view) { + info("Testing selection copy"); + + let contentDoc = view.styleDocument; + let win = view.styleWindow; + let prop = contentDoc.querySelector(".ruleview-property"); + let values = contentDoc.querySelectorAll(".ruleview-propertyvaluecontainer"); + + let range = contentDoc.createRange(); + range.setStart(prop, 0); + 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]+" + + "}[\\r\\n]+" + + "html {[\\r\\n]+" + + " color: #000000;[\\r\\n]*"; + + let allMenuItems = openStyleContextMenuAndGetAllItems(view, prop); + let menuitemCopy = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy")); + + ok(menuitemCopy.visible, + "Copy menu item is displayed as expected"); + + try { + yield waitForClipboardPromise(() => menuitemCopy.click(), + () => checkClipboardData(expectedPattern)); + } catch (e) { + failedClipboard(expectedPattern); + } +} + +function* checkSelectAll(view) { + info("Testing select-all copy"); + + let contentDoc = view.styleDocument; + let prop = contentDoc.querySelector(".ruleview-property"); + + info("Checking that _SelectAll() then copy returns the correct " + + "clipboard value"); + view._contextmenu._onSelectAll(); + let expectedPattern = "element {[\\r\\n]+" + + " margin: 10em;[\\r\\n]+" + + " font-size: 14pt;[\\r\\n]+" + + " font-family: helvetica, sans-serif;[\\r\\n]+" + + " color: #AAA;[\\r\\n]+" + + "}[\\r\\n]+" + + "html {[\\r\\n]+" + + " color: #000000;[\\r\\n]+" + + "}[\\r\\n]*"; + + let allMenuItems = openStyleContextMenuAndGetAllItems(view, prop); + let menuitemCopy = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy")); + + ok(menuitemCopy.visible, + "Copy menu item is displayed as expected"); + + try { + yield waitForClipboardPromise(() => menuitemCopy.click(), + () => checkClipboardData(expectedPattern)); + } catch (e) { + failedClipboard(expectedPattern); + } +} + +function* checkCopyEditorValue(view) { + info("Testing CSS property editor value copy"); + + let ruleEditor = getRuleViewRuleEditor(view, 0); + let propEditor = ruleEditor.rule.textProps[0].editor; + + let editor = yield focusEditableField(view, propEditor.valueSpan); + + info("Checking that copying a css property value editor returns the correct" + + " clipboard value"); + + let expectedPattern = "10em"; + + let allMenuItems = openStyleContextMenuAndGetAllItems(view, editor.input); + let menuitemCopy = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy")); + + ok(menuitemCopy.visible, + "Copy menu item is displayed as expected"); + + try { + yield waitForClipboardPromise(() => menuitemCopy.click(), + () => checkClipboardData(expectedPattern)); + } catch (e) { + failedClipboard(expectedPattern); + } +} + +function checkClipboardData(expectedPattern) { + let actual = SpecialPowers.getClipboardData("text/unicode"); + let expectedRegExp = new RegExp(expectedPattern, "g"); + return expectedRegExp.test(actual); +} + +function failedClipboard(expectedPattern) { + // Format expected text for comparison + let terminator = osString == "WINNT" ? "\r\n" : "\n"; + expectedPattern = expectedPattern.replace(/\[\\r\\n\][+*]/g, terminator); + expectedPattern = expectedPattern.replace(/\\\(/g, "("); + expectedPattern = expectedPattern.replace(/\\\)/g, ")"); + + let actual = SpecialPowers.getClipboardData("text/unicode"); + + // Trim the right hand side of our strings. This is because expectedPattern + // accounts for windows sometimes adding a newline to our copied data. + expectedPattern = expectedPattern.trimRight(); + actual = actual.trimRight(); + + dump("TEST-UNEXPECTED-FAIL | Clipboard text does not match expected ... " + + "results (escaped for accurate comparison):\n"); + info("Actual: " + escape(actual)); + info("Expected: " + escape(expectedPattern)); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js new file mode 100644 index 000000000..54e25c399 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js @@ -0,0 +1,38 @@ +/* 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 selector highlighter is hidden on page navigation. + +const TEST_URI = ` + <style type="text/css"> + body, p, td { + background: red; + } + </style> + Test the selector highlighter +`; + +const TEST_URI_2 = "data:text/html,<html><body>test</body></html>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + info("Clicking on a selector icon"); + let icon = getRuleViewSelectorHighlighterIcon(view, "body, p, td"); + + let onToggled = view.once("ruleview-selectorhighlighter-toggled"); + EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow); + let isVisible = yield onToggled; + + ok(highlighters.selectorHighlighterShown, "The selectorHighlighterShown is set."); + ok(view.selectorHighlighter, "The selectorhighlighter instance was created"); + ok(isVisible, "The toggle event says the highlighter is visible"); + + yield navigateTo(inspector, TEST_URI_2); + ok(!highlighters.selectorHighlighterShown, "The selectorHighlighterShown is unset."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.js new file mode 100644 index 000000000..4c8853e02 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.js @@ -0,0 +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"; + +// Test that the selector highlighter is created when clicking on a selector +// icon in the rule view. + +const TEST_URI = ` + <style type="text/css"> + body, p, td { + background: red; + } + </style> + Test the selector highlighter +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + + ok(!view.selectorHighlighter, + "No selectorhighlighter exist in the rule-view"); + + info("Clicking on a selector icon"); + let icon = getRuleViewSelectorHighlighterIcon(view, "body, p, td"); + + let onToggled = view.once("ruleview-selectorhighlighter-toggled"); + EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow); + let isVisible = yield onToggled; + + ok(view.selectorHighlighter, "The selectorhighlighter instance was created"); + ok(isVisible, "The toggle event says the highlighter is visible"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_02.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_02.js new file mode 100644 index 000000000..33f73e587 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_02.js @@ -0,0 +1,78 @@ +/* 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 selector highlighter is shown when clicking on a selector icon +// in the rule-view + +// Note that in this test, we mock the highlighter front, merely testing the +// behavior of the style-inspector UI for now + +const TEST_URI = ` + <style type="text/css"> + body { + background: red; + } + p { + color: white; + } + </style> + <p>Testing the selector highlighter</p> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + // Mock the highlighter front to get the reference of the NodeFront + let HighlighterFront = { + isShown: false, + nodeFront: null, + options: null, + show: function (nodeFront, options) { + this.nodeFront = nodeFront; + this.options = options; + this.isShown = true; + }, + hide: function () { + this.nodeFront = null; + this.options = null; + this.isShown = false; + } + }; + + // Inject the mock highlighter in the rule-view + view.selectorHighlighter = HighlighterFront; + + let icon = getRuleViewSelectorHighlighterIcon(view, "body"); + + info("Checking that the HighlighterFront's show/hide methods are called"); + + info("Clicking once on the body selector highlighter icon"); + yield clickSelectorIcon(icon, view); + ok(HighlighterFront.isShown, "The highlighter is shown"); + + info("Clicking once again on the body selector highlighter icon"); + yield clickSelectorIcon(icon, view); + ok(!HighlighterFront.isShown, "The highlighter is hidden"); + + info("Checking that the right NodeFront reference and options are passed"); + yield selectNode("p", inspector); + icon = getRuleViewSelectorHighlighterIcon(view, "p"); + + yield clickSelectorIcon(icon, view); + is(HighlighterFront.nodeFront.tagName, "P", + "The right NodeFront is passed to the highlighter (1)"); + is(HighlighterFront.options.selector, "p", + "The right selector option is passed to the highlighter (1)"); + + yield selectNode("body", inspector); + icon = getRuleViewSelectorHighlighterIcon(view, "body"); + yield clickSelectorIcon(icon, view); + is(HighlighterFront.nodeFront.tagName, "BODY", + "The right NodeFront is passed to the highlighter (2)"); + is(HighlighterFront.options.selector, "body", + "The right selector option is passed to the highlighter (2)"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.js new file mode 100644 index 000000000..1ffbac012 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.js @@ -0,0 +1,78 @@ +/* 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 selector highlighter toggling mechanism works correctly. + +// Note that in this test, we mock the highlighter front, merely testing the +// behavior of the style-inspector UI for now + +const TEST_URI = ` + <style type="text/css"> + div {text-decoration: underline;} + .node-1 {color: red;} + .node-2 {color: green;} + </style> + <div class="node-1">Node 1</div> + <div class="node-2">Node 2</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + // Mock the highlighter front. + let HighlighterFront = { + isShown: false, + show: function () { + this.isShown = true; + }, + hide: function () { + this.isShown = false; + } + }; + + // Inject the mock highlighter in the rule-view + view.selectorHighlighter = HighlighterFront; + + info("Select .node-1 and click on the .node-1 selector icon"); + yield selectNode(".node-1", inspector); + let icon = getRuleViewSelectorHighlighterIcon(view, ".node-1"); + yield clickSelectorIcon(icon, view); + ok(HighlighterFront.isShown, "The highlighter is shown"); + + info("With .node-1 still selected, click again on the .node-1 selector icon"); + yield clickSelectorIcon(icon, view); + ok(!HighlighterFront.isShown, "The highlighter is now hidden"); + + info("With .node-1 still selected, click on the div selector icon"); + icon = getRuleViewSelectorHighlighterIcon(view, "div"); + yield clickSelectorIcon(icon, view); + ok(HighlighterFront.isShown, "The highlighter is shown again"); + + info("With .node-1 still selected, click again on the .node-1 selector icon"); + icon = getRuleViewSelectorHighlighterIcon(view, ".node-1"); + yield clickSelectorIcon(icon, view); + ok(HighlighterFront.isShown, + "The highlighter is shown again since the clicked selector was different"); + + info("Selecting .node-2"); + yield selectNode(".node-2", inspector); + ok(HighlighterFront.isShown, + "The highlighter is still shown after selection"); + + info("With .node-2 selected, click on the div selector icon"); + icon = getRuleViewSelectorHighlighterIcon(view, "div"); + yield clickSelectorIcon(icon, view); + ok(HighlighterFront.isShown, + "The highlighter is shown still since the selected was different"); + + info("Switching back to .node-1 and clicking on the div selector"); + yield selectNode(".node-1", inspector); + icon = getRuleViewSelectorHighlighterIcon(view, "div"); + yield clickSelectorIcon(icon, view); + ok(!HighlighterFront.isShown, + "The highlighter is hidden now that the same selector was clicked"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.js new file mode 100644 index 000000000..b770f8127 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.js @@ -0,0 +1,53 @@ +/* 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 selector highlighter is shown when clicking on a selector icon +// for the 'element {}' rule + +// Note that in this test, we mock the highlighter front, merely testing the +// behavior of the style-inspector UI for now + +const TEST_URI = ` +<p>Testing the selector highlighter for the 'element {}' rule</p> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + // Mock the highlighter front to get the reference of the NodeFront + let HighlighterFront = { + isShown: false, + nodeFront: null, + options: null, + show: function (nodeFront, options) { + this.nodeFront = nodeFront; + this.options = options; + this.isShown = true; + }, + hide: function () { + this.nodeFront = null; + this.options = null; + this.isShown = false; + } + }; + // Inject the mock highlighter in the rule-view + view.selectorHighlighter = HighlighterFront; + + info("Checking that the right NodeFront reference and options are passed"); + yield selectNode("p", inspector); + let icon = getRuleViewSelectorHighlighterIcon(view, "element"); + + yield clickSelectorIcon(icon, view); + is(HighlighterFront.nodeFront.tagName, "P", + "The right NodeFront is passed to the highlighter (1)"); + is(HighlighterFront.options.selector, "body > p:nth-child(1)", + "The right selector option is passed to the highlighter (1)"); + ok(HighlighterFront.isShown, "The toggle event says the highlighter is visible"); + + yield clickSelectorIcon(icon, view); + ok(!HighlighterFront.isShown, "The toggle event says the highlighter is not visible"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_selector_highlight.js b/devtools/client/inspector/rules/test/browser_rules_selector_highlight.js new file mode 100644 index 000000000..91422d57a --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_selector_highlight.js @@ -0,0 +1,144 @@ +/* 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 selector text is highlighted correctly according +// to the components of the selector. + +const TEST_URI = [ + "<style type='text/css'>", + " h1 {}", + " h1#testid {}", + " h1 + p {}", + " div[hidden=\"true\"] {}", + " div[title=\"test\"][checked=true] {}", + " p:empty {}", + " p:lang(en) {}", + " .testclass:active {}", + " .testclass:focus {}", + " .testclass:hover {}", + "</style>", + "<h1>Styled Node</h1>", + "<p>Paragraph</p>", + "<h1 id=\"testid\">Styled Node</h1>", + "<div hidden=\"true\"></div>", + "<div title=\"test\" checked=\"true\"></div>", + "<p></p>", + "<p lang=\"en\">Paragraph<p>", + "<div class=\"testclass\">Styled Node</div>" +].join("\n"); + +const SELECTOR_ATTRIBUTE = "ruleview-selector-attribute"; +const SELECTOR_ELEMENT = "ruleview-selector"; +const SELECTOR_PSEUDO_CLASS = "ruleview-selector-pseudo-class"; +const SELECTOR_PSEUDO_CLASS_LOCK = "ruleview-selector-pseudo-class-lock"; + +const TEST_DATA = [ + { + node: "h1", + expected: [ + { value: "h1", class: SELECTOR_ELEMENT } + ] + }, + { + node: "h1 + p", + expected: [ + { value: "h1 + p", class: SELECTOR_ELEMENT } + ] + }, + { + node: "h1#testid", + expected: [ + { value: "h1#testid", class: SELECTOR_ELEMENT } + ] + }, + { + node: "div[hidden='true']", + expected: [ + { value: "div", class: SELECTOR_ELEMENT }, + { value: "[hidden=\"true\"]", class: SELECTOR_ATTRIBUTE } + ] + }, + { + node: "div[title=\"test\"][checked=\"true\"]", + expected: [ + { value: "div", class: SELECTOR_ELEMENT }, + { value: "[title=\"test\"]", class: SELECTOR_ATTRIBUTE }, + { value: "[checked=\"true\"]", class: SELECTOR_ATTRIBUTE } + ] + }, + { + node: "p:empty", + expected: [ + { value: "p", class: SELECTOR_ELEMENT }, + { value: ":empty", class: SELECTOR_PSEUDO_CLASS } + ] + }, + { + node: "p:lang(en)", + expected: [ + { value: "p", class: SELECTOR_ELEMENT }, + { value: ":lang(en)", class: SELECTOR_PSEUDO_CLASS } + ] + }, + { + node: ".testclass", + pseudoClass: ":active", + expected: [ + { value: ".testclass", class: SELECTOR_ELEMENT }, + { value: ":active", class: SELECTOR_PSEUDO_CLASS_LOCK } + ] + }, + { + node: ".testclass", + pseudoClass: ":focus", + expected: [ + { value: ".testclass", class: SELECTOR_ELEMENT }, + { value: ":focus", class: SELECTOR_PSEUDO_CLASS_LOCK } + ] + }, + { + node: ".testclass", + pseudoClass: ":hover", + expected: [ + { value: ".testclass", class: SELECTOR_ELEMENT }, + { value: ":hover", class: SELECTOR_PSEUDO_CLASS_LOCK } + ] + }, +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + for (let {node, pseudoClass, expected} of TEST_DATA) { + yield selectNode(node, inspector); + + if (pseudoClass) { + let onRefresh = inspector.once("rule-view-refreshed"); + inspector.togglePseudoClass(pseudoClass); + yield onRefresh; + } + + let selectorContainer = + getRuleViewRuleEditor(view, 1).selectorText.firstChild; + + if (selectorContainer.children.length === expected.length) { + for (let i = 0; i < expected.length; i++) { + is(expected[i].value, selectorContainer.children[i].textContent, + "Got expected selector value: " + expected[i].value + " == " + + selectorContainer.children[i].textContent); + is(expected[i].class, selectorContainer.children[i].className, + "Got expected class name: " + expected[i].class + " == " + + selectorContainer.children[i].className); + } + } else { + for (let selector of selectorContainer.children) { + info("Actual selector components: { value: " + selector.textContent + + ", class: " + selector.className + " }\n"); + } + } + } +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js new file mode 100644 index 000000000..dea9fff32 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js @@ -0,0 +1,182 @@ +/* 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 strict search filter and clear button works properly +// in the computed list + +const TEST_URI = ` + <style type="text/css"> + #testid { + margin: 4px 0px 10px 44px; + } + .testclass { + background-color: red; + } + </style> + <h1 id="testid" class="testclass">Styled Node</h1> +`; + +const TEST_DATA = [ + { + desc: "Tests that the strict search filter works properly in the " + + "computed list for property names", + search: "`margin-left`", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: false, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: true + }, + { + desc: "Tests that the strict search filter works properly in the " + + "computed list for property values", + search: "`0px`", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: false, + isMarginRightHighlighted: true, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: false + }, + { + desc: "Tests that the strict search filter works properly in the " + + "computed list for parsed property names", + search: "`margin-left`:", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: false, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: true + }, + { + desc: "Tests that the strict search filter works properly in the " + + "computed list for parsed property values", + search: ":`4px`", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: true, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: false + }, + { + desc: "Tests that the strict search filter works properly in the " + + "computed list for property line input", + search: "`margin-top`:`4px`", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: true, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: false + }, + { + desc: "Tests that the strict search filter works properly in the " + + "computed list for a parsed strict property name and non-strict " + + "property value", + search: "`margin-top`:4px", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: true, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: false + }, + { + desc: "Tests that the strict search filter works properly in the " + + "computed list for a parsed strict property value and non-strict " + + "property name", + search: "i:`4px`", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: true, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: false + }, +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + for (let data of TEST_DATA) { + info(data.desc); + yield setSearchFilter(view, data.search); + yield checkRules(view, data); + yield clearSearchAndCheckRules(view); + } +} + +function* checkRules(view, data) { + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let textPropEditor = rule.textProps[0].editor; + let computed = textPropEditor.computed; + + is(rule.selectorText, "#testid", "Second rule is #testid."); + is(!!textPropEditor.expander.getAttribute("open"), data.isExpanderOpen, + "Got correct expander state."); + is(computed.hasAttribute("filter-open"), data.isFilterOpen, + "Got correct expanded state for margin computed list."); + is(textPropEditor.container.classList.contains("ruleview-highlight"), + data.isMarginHighlighted, + "Got correct highlight for margin text property."); + + is(computed.children[0].classList.contains("ruleview-highlight"), + data.isMarginTopHighlighted, + "Got correct highlight for margin-top computed property."); + is(computed.children[1].classList.contains("ruleview-highlight"), + data.isMarginRightHighlighted, + "Got correct highlight for margin-right computed property."); + is(computed.children[2].classList.contains("ruleview-highlight"), + data.isMarginBottomHighlighted, + "Got correct highlight for margin-bottom computed property."); + is(computed.children[3].classList.contains("ruleview-highlight"), + data.isMarginLeftHighlighted, + "Got correct highlight for margin-left computed property."); +} + +function* clearSearchAndCheckRules(view) { + let win = view.styleWindow; + let searchField = view.searchField; + let searchClearButton = view.searchClearButton; + + let rule = getRuleViewRuleEditor(view, 1).rule; + let textPropEditor = rule.textProps[0].editor; + let computed = textPropEditor.computed; + + info("Clearing the search filter"); + EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win); + yield view.inspector.once("ruleview-filtered"); + + info("Check the search filter is cleared and no rules are highlighted"); + is(view.element.children.length, 3, "Should have 3 rules."); + ok(!searchField.value, "Search filter is cleared"); + ok(!view.styleDocument.querySelectorAll(".ruleview-highlight").length, + "No rules are higlighted"); + + ok(!textPropEditor.expander.getAttribute("open"), "Expander is closed."); + ok(!computed.hasAttribute("filter-open"), + "margin computed list is closed."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js new file mode 100644 index 000000000..50948e174 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js @@ -0,0 +1,130 @@ +/* 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 strict search filter works properly for property +// names. + +const TEST_URI = ` + <style type="text/css"> + #testid { + width: 2%; + color: red; + } + .testclass { + width: 22%; + background-color: #00F; + } + </style> + <h1 id="testid" class="testclass">Styled Node</h1> +`; + +const TEST_DATA = [ + { + desc: "Tests that the strict search filter works properly for property " + + "names", + search: "`color`", + ruleCount: 2, + propertyIndex: 1 + }, + { + desc: "Tests that the strict search filter works properly for property " + + "values", + search: "`2%`", + ruleCount: 2, + propertyIndex: 0 + }, + { + desc: "Tests that the strict search filter works properly for parsed " + + "property names", + search: "`color`:", + ruleCount: 2, + propertyIndex: 1 + }, + { + desc: "Tests that the strict search filter works properly for parsed " + + "property values", + search: ":`2%`", + ruleCount: 2, + propertyIndex: 0 + }, + { + desc: "Tests that the strict search filter works properly for property " + + "line input", + search: "`width`:`2%`", + ruleCount: 2, + propertyIndex: 0 + }, + { + desc: "Tests that the search filter works properly for a parsed strict " + + "property name and non-strict property value.", + search: "`width`:2%", + ruleCount: 3, + propertyIndex: 0 + }, + { + desc: "Tests that the search filter works properly for a parsed strict " + + "property value and non-strict property name.", + search: "i:`2%`", + ruleCount: 2, + propertyIndex: 0 + } +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + for (let data of TEST_DATA) { + info(data.desc); + yield setSearchFilter(view, data.search); + yield checkRules(view, data); + yield clearSearchAndCheckRules(view); + } +} + +function* checkRules(view, data) { + info("Check that the correct rules are visible"); + is(view.element.children.length, data.ruleCount, + "Should have " + data.ruleCount + " rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(rule.textProps[data.propertyIndex].editor.container.classList + .contains("ruleview-highlight"), + "Text property is correctly highlighted."); + + if (data.ruleCount > 2) { + rule = getRuleViewRuleEditor(view, 2).rule; + is(rule.selectorText, ".testclass", "Third rule is .testclass."); + ok(rule.textProps[data.propertyIndex].editor.container.classList + .contains("ruleview-highlight"), + "Text property is correctly highlighted."); + } +} + +function* clearSearchAndCheckRules(view) { + let doc = view.styleDocument; + let win = view.styleWindow; + let searchField = view.searchField; + let searchClearButton = view.searchClearButton; + + info("Clearing the search filter"); + EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win); + yield view.inspector.once("ruleview-filtered"); + + info("Check the search filter is cleared and no rules are highlighted"); + is(view.element.children.length, 3, "Should have 3 rules."); + ok(!searchField.value, "Search filter is cleared."); + ok(!doc.querySelectorAll(".ruleview-highlight").length, + "No rules are higlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js new file mode 100644 index 000000000..0c76f0518 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js @@ -0,0 +1,34 @@ +/* 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 strict search filter works properly for stylesheet +// source. + +const SEARCH = "`doc_urls_clickable.css:1`"; +const TEST_URI = URL_ROOT + "doc_urls_clickable.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield selectNode(".relative1", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let source = rule.textProps[0].editor.ruleEditor.source; + + is(rule.selectorText, ".relative1", "Second rule is .relative1."); + ok(source.classList.contains("ruleview-highlight"), + "stylesheet source is correctly highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.js b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.js new file mode 100644 index 000000000..0326b0e9c --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.js @@ -0,0 +1,44 @@ +/* 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 strict search filter works properly for selector +// values. + +const SEARCH = "`.testclass`"; + +const TEST_URI = ` + <style type="text/css"> + .testclass1 { + background-color: #00F; + } + .testclass { + color: red; + } + </style> + <h1 id="testid" class="testclass testclass1">Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + is(ruleEditor.rule.selectorText, ".testclass", "Second rule is .testclass."); + ok(ruleEditor.selectorText.children[0].classList + .contains("ruleview-highlight"), ".testclass selector is highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js b/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js new file mode 100644 index 000000000..927deb8ce --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js @@ -0,0 +1,203 @@ +/* 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"; + +// FIXME: Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejection should be fixed. +thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source"); + +// Test the links from the rule-view to the styleeditor + +const STYLESHEET_URL = "data:text/css," + encodeURIComponent( + ["#first {", + "color: blue", + "}"].join("\n")); + +const EXTERNAL_STYLESHEET_FILE_NAME = "doc_style_editor_link.css"; +const EXTERNAL_STYLESHEET_URL = URL_ROOT + EXTERNAL_STYLESHEET_FILE_NAME; + +const DOCUMENT_URL = "data:text/html;charset=utf-8," + encodeURIComponent(` + <html> + <head> + <title>Rule view style editor link test</title> + <style type="text/css"> + html { color: #000000; } + div { font-variant: small-caps; color: #000000; } + .nomatches {color: #ff0000;}</style> <div id="first" style="margin: 10em; + font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA"> + </style> + <style> + div { font-weight: bold; } + </style> + <link rel="stylesheet" type="text/css" href="${STYLESHEET_URL}"> + <link rel="stylesheet" type="text/css" href="${EXTERNAL_STYLESHEET_URL}"> + </head> + <body> + <h1>Some header text</h1> + <p id="salutation" style="font-size: 12pt">hi.</p> + <p id="body" style="font-size: 12pt">I am a test-case. This text exists + solely to provide some things to + <span style="color: yellow" class="highlight"> + highlight</span> and <span style="font-weight: bold">count</span> + style list-items in the box at right. If you are reading this, + you should go do something else instead. Maybe read a book. Or better + yet, write some test-cases for another bit of code. + <span style="font-style: italic">some text</span></p> + <p id="closing">more text</p> + <p>even more text</p> + </div> + </body> + </html> +`); + +add_task(function* () { + yield addTab(DOCUMENT_URL); + let {toolbox, inspector, view, testActor} = yield openRuleView(); + yield selectNode("div", inspector); + + yield testInlineStyle(view); + yield testFirstInlineStyleSheet(view, toolbox, testActor); + yield testSecondInlineStyleSheet(view, toolbox, testActor); + yield testExternalStyleSheet(view, toolbox, testActor); + yield testDisabledStyleEditor(view, toolbox); +}); + +function* testInlineStyle(view) { + info("Testing inline style"); + + let onTab = waitForTab(); + info("Clicking on the first link in the rule-view"); + clickLinkByIndex(view, 0); + + let tab = yield onTab; + + let tabURI = tab.linkedBrowser.documentURI.spec; + ok(tabURI.startsWith("view-source:"), "View source tab is open"); + info("Closing tab"); + gBrowser.removeTab(tab); +} + +function* testFirstInlineStyleSheet(view, toolbox, testActor) { + info("Testing inline stylesheet"); + + info("Listening for toolbox switch to the styleeditor"); + let onSwitch = waitForStyleEditor(toolbox); + + info("Clicking an inline stylesheet"); + clickLinkByIndex(view, 4); + let editor = yield onSwitch; + + ok(true, "Switched to the style-editor panel in the toolbox"); + + yield validateStyleEditorSheet(editor, 0, testActor); +} + +function* testSecondInlineStyleSheet(view, toolbox, testActor) { + info("Testing second inline stylesheet"); + + info("Waiting for the stylesheet editor to be selected"); + let panel = toolbox.getCurrentPanel(); + let onSelected = panel.UI.once("editor-selected"); + + info("Switching back to the inspector panel in the toolbox"); + yield toolbox.selectTool("inspector"); + + info("Clicking on second inline stylesheet link"); + testRuleViewLinkLabel(view); + clickLinkByIndex(view, 3); + let editor = yield onSelected; + + is(toolbox.currentToolId, "styleeditor", + "The style editor is selected again"); + yield validateStyleEditorSheet(editor, 1, testActor); +} + +function* testExternalStyleSheet(view, toolbox, testActor) { + info("Testing external stylesheet"); + + info("Waiting for the stylesheet editor to be selected"); + let panel = toolbox.getCurrentPanel(); + let onSelected = panel.UI.once("editor-selected"); + + info("Switching back to the inspector panel in the toolbox"); + yield toolbox.selectTool("inspector"); + + info("Clicking on an external stylesheet link"); + testRuleViewLinkLabel(view); + clickLinkByIndex(view, 1); + let editor = yield onSelected; + + is(toolbox.currentToolId, "styleeditor", + "The style editor is selected again"); + yield validateStyleEditorSheet(editor, 2, testActor); +} + +function* validateStyleEditorSheet(editor, expectedSheetIndex, testActor) { + info("validating style editor stylesheet"); + is(editor.styleSheet.styleSheetIndex, expectedSheetIndex, + "loaded stylesheet index matches document stylesheet"); + + let href = editor.styleSheet.href || editor.styleSheet.nodeHref; + + let expectedHref = yield testActor.eval( + `content.document.styleSheets[${expectedSheetIndex}].href || + content.document.location.href`); + + is(href, expectedHref, "loaded stylesheet href matches document stylesheet"); +} + +function* testDisabledStyleEditor(view, toolbox) { + info("Testing with the style editor disabled"); + + info("Switching to the inspector panel in the toolbox"); + yield toolbox.selectTool("inspector"); + + info("Disabling the style editor"); + Services.prefs.setBoolPref("devtools.styleeditor.enabled", false); + gDevTools.emit("tool-unregistered", "styleeditor"); + + info("Clicking on a link"); + testUnselectableRuleViewLink(view, 1); + clickLinkByIndex(view, 1); + + is(toolbox.currentToolId, "inspector", "The click should have no effect"); + + info("Enabling the style editor"); + Services.prefs.setBoolPref("devtools.styleeditor.enabled", true); + gDevTools.emit("tool-registered", "styleeditor"); + + info("Clicking on a link"); + let onStyleEditorSelected = toolbox.once("styleeditor-selected"); + clickLinkByIndex(view, 1); + yield onStyleEditorSelected; + is(toolbox.currentToolId, "styleeditor", "Style Editor should be selected"); + + Services.prefs.clearUserPref("devtools.styleeditor.enabled"); +} + +function testRuleViewLinkLabel(view) { + let link = getRuleViewLinkByIndex(view, 2); + let labelElem = link.querySelector(".ruleview-rule-source-label"); + let value = labelElem.textContent; + let tooltipText = labelElem.getAttribute("title"); + + is(value, EXTERNAL_STYLESHEET_FILE_NAME + ":1", + "rule view stylesheet display value matches filename and line number"); + is(tooltipText, EXTERNAL_STYLESHEET_URL + ":1", + "rule view stylesheet tooltip text matches the full URI path"); +} + +function testUnselectableRuleViewLink(view, index) { + let link = getRuleViewLinkByIndex(view, index); + let unselectable = link.hasAttribute("unselectable"); + + ok(unselectable, "Rule view is unselectable"); +} + +function clickLinkByIndex(view, index) { + let link = getRuleViewLinkByIndex(view, index); + link.scrollIntoView(); + link.click(); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_urls-clickable.js b/devtools/client/inspector/rules/test/browser_rules_urls-clickable.js new file mode 100644 index 000000000..fb1211e3c --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_urls-clickable.js @@ -0,0 +1,70 @@ +/* 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 to make sure that URLs are clickable in the rule view + +const TEST_URI = URL_ROOT + "doc_urls_clickable.html"; +const TEST_IMAGE = URL_ROOT + "doc_test_image.png"; +const BASE_64_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAA" + + "FCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAA" + + "BJRU5ErkJggg=="; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield selectNodes(inspector, view); +}); + +function* selectNodes(inspector, ruleView) { + let relative1 = ".relative1"; + let relative2 = ".relative2"; + let absolute = ".absolute"; + let inline = ".inline"; + let base64 = ".base64"; + let noimage = ".noimage"; + let inlineresolved = ".inline-resolved"; + + yield selectNode(relative1, inspector); + let relativeLink = ruleView.styleDocument + .querySelector(".ruleview-propertyvaluecontainer a"); + ok(relativeLink, "Link exists for relative1 node"); + is(relativeLink.getAttribute("href"), TEST_IMAGE, "href matches"); + + yield selectNode(relative2, inspector); + relativeLink = ruleView.styleDocument + .querySelector(".ruleview-propertyvaluecontainer a"); + ok(relativeLink, "Link exists for relative2 node"); + is(relativeLink.getAttribute("href"), TEST_IMAGE, "href matches"); + + yield selectNode(absolute, inspector); + let absoluteLink = ruleView.styleDocument + .querySelector(".ruleview-propertyvaluecontainer a"); + ok(absoluteLink, "Link exists for absolute node"); + is(absoluteLink.getAttribute("href"), TEST_IMAGE, "href matches"); + + yield selectNode(inline, inspector); + let inlineLink = ruleView.styleDocument + .querySelector(".ruleview-propertyvaluecontainer a"); + ok(inlineLink, "Link exists for inline node"); + is(inlineLink.getAttribute("href"), TEST_IMAGE, "href matches"); + + yield selectNode(base64, inspector); + let base64Link = ruleView.styleDocument + .querySelector(".ruleview-propertyvaluecontainer a"); + ok(base64Link, "Link exists for base64 node"); + is(base64Link.getAttribute("href"), BASE_64_URL, "href matches"); + + yield selectNode(inlineresolved, inspector); + let inlineResolvedLink = ruleView.styleDocument + .querySelector(".ruleview-propertyvaluecontainer a"); + ok(inlineResolvedLink, "Link exists for style tag node"); + is(inlineResolvedLink.getAttribute("href"), TEST_IMAGE, "href matches"); + + yield selectNode(noimage, inspector); + let noimageLink = ruleView.styleDocument + .querySelector(".ruleview-propertyvaluecontainer a"); + ok(!noimageLink, "There is no link for the node with no background image"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js new file mode 100644 index 000000000..e1bafff9b --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js @@ -0,0 +1,58 @@ +/* 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 user agent styles are never editable via +// the UI + +const TEST_URI = ` + <blockquote type=cite> + <pre _moz_quote=true> + inspect <a href='foo' style='color:orange'>user agent</a> styles + </pre> + </blockquote> +`; + +var PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles"; + +add_task(function* () { + info("Starting the test with the pref set to true before toolbox is opened"); + Services.prefs.setBoolPref(PREF_UA_STYLES, true); + + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + yield userAgentStylesUneditable(inspector, view); + + info("Resetting " + PREF_UA_STYLES); + Services.prefs.clearUserPref(PREF_UA_STYLES); +}); + +function* userAgentStylesUneditable(inspector, view) { + info("Making sure that UI is not editable for user agent styles"); + + 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"); + + let firstProp = rule.textProps.filter(p => !p.invisible)[0]; + + ok(!firstProp.editor.nameSpan._editable, + "nameSpan is not 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"); + } + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js new file mode 100644 index 000000000..6852e3c03 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js @@ -0,0 +1,183 @@ +/* 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 user agent styles are inspectable via rule view if +// it is preffed on. + +var PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles"; +const { PrefObserver } = require("devtools/client/styleeditor/utils"); + +const TEST_URI = URL_ROOT + "doc_author-sheet.html"; + +const TEST_DATA = [ + { + selector: "blockquote", + numUserRules: 1, + numUARules: 0 + }, + { + selector: "pre", + numUserRules: 1, + numUARules: 0 + }, + { + selector: "input[type=range]", + numUserRules: 1, + numUARules: 0 + }, + { + selector: "input[type=number]", + numUserRules: 1, + numUARules: 0 + }, + { + selector: "input[type=color]", + numUserRules: 1, + numUARules: 0 + }, + { + selector: "input[type=text]", + numUserRules: 1, + numUARules: 0 + }, + { + selector: "progress", + numUserRules: 1, + numUARules: 0 + }, + // Note that some tests below assume that the "a" selector is the + // last test in TEST_DATA. + { + selector: "a", + numUserRules: 3, + numUARules: 0 + } +]; + +add_task(function* () { + requestLongerTimeout(4); + + info("Starting the test with the pref set to true before toolbox is opened"); + yield setUserAgentStylesPref(true); + + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + + info("Making sure that UA styles are visible on initial load"); + yield userAgentStylesVisible(inspector, view); + + info("Making sure that setting the pref to false hides UA styles"); + yield setUserAgentStylesPref(false); + yield userAgentStylesNotVisible(inspector, view); + + info("Making sure that resetting the pref to true shows UA styles again"); + yield setUserAgentStylesPref(true); + yield userAgentStylesVisible(inspector, view); + + info("Resetting " + PREF_UA_STYLES); + Services.prefs.clearUserPref(PREF_UA_STYLES); +}); + +function* setUserAgentStylesPref(val) { + info("Setting the pref " + PREF_UA_STYLES + " to: " + val); + + // Reset the pref and wait for PrefObserver to callback so UI + // has a chance to get updated. + let oncePrefChanged = defer(); + let prefObserver = new PrefObserver("devtools."); + prefObserver.on(PREF_UA_STYLES, oncePrefChanged.resolve); + Services.prefs.setBoolPref(PREF_UA_STYLES, val); + yield oncePrefChanged.promise; + prefObserver.off(PREF_UA_STYLES, oncePrefChanged.resolve); +} + +function* userAgentStylesVisible(inspector, view) { + info("Making sure that user agent styles are currently visible"); + + let userRules; + let uaRules; + + for (let data of TEST_DATA) { + yield selectNode(data.selector, inspector); + yield compareAppliedStylesWithUI(inspector, view, "ua"); + + userRules = view._elementStyle.rules.filter(rule=>rule.editor.isEditable); + uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable); + is(userRules.length, data.numUserRules, "Correct number of user rules"); + ok(uaRules.length > data.numUARules, "Has UA rules"); + } + + ok(userRules.some(rule => rule.matchedSelectors.length === 1), + "There is an inline style for element in user styles"); + + // These tests rely on the "a" selector being the last test in + // TEST_DATA. + ok(uaRules.some(rule => { + return rule.matchedSelectors.indexOf(":any-link") !== -1; + }), "There is a rule for :any-link"); + ok(uaRules.some(rule => { + return rule.matchedSelectors.indexOf("*|*:link") !== -1; + }), "There is a rule for *|*:link"); + ok(uaRules.some(rule => { + return rule.matchedSelectors.length === 1; + }), "Inline styles for ua styles"); +} + +function* userAgentStylesNotVisible(inspector, view) { + info("Making sure that user agent styles are not currently visible"); + + let userRules; + let uaRules; + + for (let data of TEST_DATA) { + yield selectNode(data.selector, inspector); + yield compareAppliedStylesWithUI(inspector, view); + + userRules = view._elementStyle.rules.filter(rule=>rule.editor.isEditable); + uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable); + is(userRules.length, data.numUserRules, "Correct number of user rules"); + is(uaRules.length, data.numUARules, "No UA rules"); + } +} + +function* compareAppliedStylesWithUI(inspector, view, filter) { + info("Making sure that UI is consistent with pageStyle.getApplied"); + + let entries = yield inspector.pageStyle.getApplied( + inspector.selection.nodeFront, + { + inherited: true, + matchedSelectors: true, + filter: filter + } + ); + + // We may see multiple entries that map to a given rule; filter the + // duplicates here to match what the UI does. + let entryMap = new Map(); + for (let entry of entries) { + entryMap.set(entry.rule, entry); + } + entries = [...entryMap.values()]; + + let elementStyle = view._elementStyle; + is(elementStyle.rules.length, entries.length, + "Should have correct number of rules (" + entries.length + ")"); + + entries = entries.sort((a, b) => { + return (a.pseudoElement || "z") > (b.pseudoElement || "z"); + }); + + entries.forEach((entry, i) => { + let elementStyleRule = elementStyle.rules[i]; + is(elementStyleRule.inherited, entry.inherited, + "Same inherited (" + entry.inherited + ")"); + is(elementStyleRule.isSystem, entry.isSystem, + "Same isSystem (" + entry.isSystem + ")"); + is(elementStyleRule.editor.isEditable, !entry.isSystem, + "Editor isEditable opposite of UA (" + entry.isSystem + ")"); + }); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_user-property-reset.js b/devtools/client/inspector/rules/test/browser_rules_user-property-reset.js new file mode 100644 index 000000000..62b1d927c --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_user-property-reset.js @@ -0,0 +1,90 @@ +/* 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 user set style properties can be changed from the markup-view and +// don't survive page reload + +const TEST_URI = ` + <p id='id1' style='width:200px;'>element 1</p> + <p id='id2' style='width:100px;'>element 2</p> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = yield openRuleView(); + + yield selectNode("#id1", inspector); + yield modifyRuleViewWidth("300px", view, inspector); + yield assertRuleAndMarkupViewWidth("id1", "300px", view, inspector); + + yield selectNode("#id2", inspector); + yield assertRuleAndMarkupViewWidth("id2", "100px", view, inspector); + yield modifyRuleViewWidth("50px", view, inspector); + yield assertRuleAndMarkupViewWidth("id2", "50px", view, inspector); + + yield reloadPage(inspector, testActor); + + yield selectNode("#id1", inspector); + yield assertRuleAndMarkupViewWidth("id1", "200px", view, inspector); + yield selectNode("#id2", inspector); + yield assertRuleAndMarkupViewWidth("id2", "100px", view, inspector); +}); + +function getStyleRule(ruleView) { + return ruleView.styleDocument.querySelector(".ruleview-rule"); +} + +function* modifyRuleViewWidth(value, ruleView, inspector) { + info("Getting the property value element"); + let valueSpan = getStyleRule(ruleView) + .querySelector(".ruleview-propertyvalue"); + + info("Focusing the property value to set it to edit mode"); + let editor = yield focusEditableField(ruleView, valueSpan.parentNode); + + ok(editor.input, "The inplace-editor field is ready"); + info("Setting the new value"); + editor.input.value = value; + + info("Pressing return and waiting for the field to blur and for the " + + "markup-view to show the mutation"); + let onBlur = once(editor.input, "blur", true); + let onStyleChanged = waitForStyleModification(inspector); + EventUtils.sendKey("return"); + yield onBlur; + yield onStyleChanged; + + info("Escaping out of the new property field that has been created after " + + "the value was edited"); + let onNewFieldBlur = once(ruleView.styleDocument.activeElement, "blur", true); + EventUtils.sendKey("escape"); + yield onNewFieldBlur; +} + +function* getContainerStyleAttrValue(id, {walker, markup}) { + let front = yield walker.querySelector(walker.rootNode, "#" + id); + let container = markup.getContainer(front); + + let attrIndex = 0; + for (let attrName of container.elt.querySelectorAll(".attr-name")) { + if (attrName.textContent === "style") { + return container.elt.querySelectorAll(".attr-value")[attrIndex]; + } + attrIndex++; + } + return undefined; +} + +function* assertRuleAndMarkupViewWidth(id, value, ruleView, inspector) { + let valueSpan = getStyleRule(ruleView) + .querySelector(".ruleview-propertyvalue"); + is(valueSpan.textContent, value, + "Rule-view style width is " + value + " as expected"); + + let attr = yield getContainerStyleAttrValue(id, inspector); + is(attr.textContent.replace(/\s/g, ""), + "width:" + value + ";", "Markup-view style attribute width is " + value); +} diff --git a/devtools/client/inspector/rules/test/doc_author-sheet.html b/devtools/client/inspector/rules/test/doc_author-sheet.html new file mode 100644 index 000000000..f8c2eadd5 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_author-sheet.html @@ -0,0 +1,39 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>authored sheet test</title> + + <style> + pre a { + color: orange; + } + </style> + + <script> + "use strict"; + var gIOService = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + + var style = "data:text/css,a { background-color: seagreen; }"; + var uri = gIOService.newURI(style, null, null); + var windowUtils = SpecialPowers.wrap(window) + .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor) + .getInterface(SpecialPowers.Ci.nsIDOMWindowUtils); + windowUtils.loadSheet(uri, windowUtils.AUTHOR_SHEET); + </script> + +</head> +<body> + <input type=text placeholder=test></input> + <input type=color></input> + <input type=range></input> + <input type=number></input> + <progress></progress> + <blockquote type=cite> + <pre _moz_quote=true> + inspect <a href="foo">user agent</a> styles + </pre> + </blockquote> +</body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_blob_stylesheet.html b/devtools/client/inspector/rules/test/doc_blob_stylesheet.html new file mode 100644 index 000000000..c9973993b --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_blob_stylesheet.html @@ -0,0 +1,39 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> +</html> +<html> +<head> + <meta charset="utf-8"> + <title>Blob stylesheet sourcemap</title> +</head> +<body> +<h1>Test</h1> +<script> +"use strict"; + +var cssContent = `body { + background-color: black; +} +body > h1 { + color: white; +} +` + +"/*# sourceMappingURL=data:application/json;base64,ewoidmVyc2lvbiI6IDMsCiJtYX" + +"BwaW5ncyI6ICJBQUFBLElBQUs7RUFDSCxnQkFBZ0IsRUFBRSxLQUFLOztBQUN2QixTQUFPO0VBQ0" + +"wsS0FBSyxFQUFFLEtBQUsiLAoic291cmNlcyI6IFsidGVzdC5zY3NzIl0sCiJzb3VyY2VzQ29udG" + +"VudCI6IFsiYm9keSB7XG4gIGJhY2tncm91bmQtY29sb3I6IGJsYWNrO1xuICAmID4gaDEge1xuIC" + +"AgIGNvbG9yOiB3aGl0ZTsgIFxuICB9XG59XG4iXSwKIm5hbWVzIjogW10sCiJmaWxlIjogInRlc3" + +"QuY3NzIgp9Cg== */"; +var cssBlob = new Blob([cssContent], {type: "text/css"}); +var url = URL.createObjectURL(cssBlob); + +var head = document.querySelector("head"); +var link = document.createElement("link"); +link.rel = "stylesheet"; +link.type = "text/css"; +link.href = url; +head.appendChild(link); +</script> +</body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet.html b/devtools/client/inspector/rules/test/doc_content_stylesheet.html new file mode 100644 index 000000000..3ea65f606 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_content_stylesheet.html @@ -0,0 +1,35 @@ +<html> +<head> + <title>test</title> + + <link href="./doc_content_stylesheet_linked.css" rel="stylesheet" type="text/css"> + + <script> + /* eslint no-unused-vars: [2, {"vars": "local"}] */ + "use strict"; + // Load script.css + function loadCSS() { + let link = document.createElement("link"); + link.rel = "stylesheet"; + link.type = "text/css"; + link.href = "./doc_content_stylesheet_script.css"; + document.getElementsByTagName("head")[0].appendChild(link); + } + </script> + + <style> + table { + border: 1px solid #000; + } + </style> +</head> +<body onload="loadCSS();"> + <table id="target"> + <tr> + <td> + <h3>Simple test</h3> + </td> + </tr> + </table> +</body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet_imported.css b/devtools/client/inspector/rules/test/doc_content_stylesheet_imported.css new file mode 100644 index 000000000..ea1a3d986 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_content_stylesheet_imported.css @@ -0,0 +1,5 @@ +@import url("./doc_content_stylesheet_imported2.css"); + +#target { + text-decoration: underline; +} diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet_imported2.css b/devtools/client/inspector/rules/test/doc_content_stylesheet_imported2.css new file mode 100644 index 000000000..77c73299e --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_content_stylesheet_imported2.css @@ -0,0 +1,3 @@ +#target { + text-decoration: underline; +} diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet_linked.css b/devtools/client/inspector/rules/test/doc_content_stylesheet_linked.css new file mode 100644 index 000000000..712ba78fb --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_content_stylesheet_linked.css @@ -0,0 +1,3 @@ +table { + border-collapse: collapse; +} diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet_script.css b/devtools/client/inspector/rules/test/doc_content_stylesheet_script.css new file mode 100644 index 000000000..5aa5e2c6c --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_content_stylesheet_script.css @@ -0,0 +1,5 @@ +@import url("./doc_content_stylesheet_imported.css"); + +table { + opacity: 1; +} diff --git a/devtools/client/inspector/rules/test/doc_copystyles.css b/devtools/client/inspector/rules/test/doc_copystyles.css new file mode 100644 index 000000000..83f0c87b1 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_copystyles.css @@ -0,0 +1,11 @@ +/* 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/. */ + +html, body, #testid { + color: #F00; + background-color: #00F; + font-size: 12px; + border-color: #00F !important; + --var: "*/"; +} diff --git a/devtools/client/inspector/rules/test/doc_copystyles.html b/devtools/client/inspector/rules/test/doc_copystyles.html new file mode 100644 index 000000000..da1b4c0b3 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_copystyles.html @@ -0,0 +1,11 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> + <head> + <title>Test case for copying stylesheet in rule-view</title> + <link rel="stylesheet" type="text/css" href="doc_copystyles.css"/> + </head> + <body> + <div id='testid'>Styled Node</div> + </body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_cssom.html b/devtools/client/inspector/rules/test/doc_cssom.html new file mode 100644 index 000000000..28de66d7d --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_cssom.html @@ -0,0 +1,22 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>CSSOM test</title> + + <script> + "use strict"; + window.onload = function () { + let x = document.styleSheets[0]; + x.insertRule("div { color: seagreen; }", 1); + }; + </script> + + <style> + span { } + </style> +</head> +<body> + <div id="target"> the ocean </div> +</body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_custom.html b/devtools/client/inspector/rules/test/doc_custom.html new file mode 100644 index 000000000..09bf501d5 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_custom.html @@ -0,0 +1,33 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> + <head> + <style> + #testidSimple { + --background-color: blue; + } + .testclassSimple { + --background-color: green; + } + + .testclassImportant { + --background-color: green !important; + } + #testidImportant { + --background-color: blue; + } + + #testidDisable { + --background-color: blue; + } + .testclassDisable { + --background-color: green; + } + </style> + </head> + <body> + <div id="testidSimple" class="testclassSimple">Styled Node</div> + <div id="testidImportant" class="testclassImportant">Styled Node</div> + <div id="testidDisable" class="testclassDisable">Styled Node</div> + </body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_filter.html b/devtools/client/inspector/rules/test/doc_filter.html new file mode 100644 index 000000000..cb2df9feb --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_filter.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<!-- 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/. --> +<html> +<head> + <title>Bug 1055181 - CSS Filter Editor Widget</title> + <style> + body { + filter: blur(2px) contrast(2); + } + </style> +</head> diff --git a/devtools/client/inspector/rules/test/doc_frame_script.js b/devtools/client/inspector/rules/test/doc_frame_script.js new file mode 100644 index 000000000..88da043f1 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_frame_script.js @@ -0,0 +1,113 @@ +/* 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/ */ +/* globals addMessageListener, sendAsyncMessage */ + +"use strict"; + +// A helper frame-script for brower/devtools/styleinspector tests. +// +// Most listeners in the script expect "Test:"-namespaced messages from chrome, +// then execute code upon receiving, and immediately send back a message. +// This is so that chrome test code can execute code in content and wait for a +// response this way: +// let response = yield executeInContent(browser, "Test:msgName", data, true); +// The response message should have the same name "Test:msgName" +// +// Some listeners do not send a response message back. + +var {utils: Cu} = Components; + +var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +var defer = require("devtools/shared/defer"); + +/** + * Get a value for a given property name in a css rule in a stylesheet, given + * their indexes + * @param {Object} data Expects a data object with the following properties + * - {Number} styleSheetIndex + * - {Number} ruleIndex + * - {String} name + * @return {String} The value, if found, null otherwise + */ +addMessageListener("Test:GetRulePropertyValue", function (msg) { + let {name, styleSheetIndex, ruleIndex} = msg.data; + let value = null; + + dumpn("Getting the value for property name " + name + " in sheet " + + styleSheetIndex + " and rule " + ruleIndex); + + let sheet = content.document.styleSheets[styleSheetIndex]; + if (sheet) { + let rule = sheet.cssRules[ruleIndex]; + if (rule) { + value = rule.style.getPropertyValue(name); + } + } + + sendAsyncMessage("Test:GetRulePropertyValue", value); +}); + +/** + * Get the property value from the computed style for an element. + * @param {Object} data Expects a data object with the following properties + * - {String} selector: The selector used to obtain the element. + * - {String} pseudo: pseudo id to query, or null. + * - {String} name: name of the property + * @return {String} The value, if found, null otherwise + */ +addMessageListener("Test:GetComputedStylePropertyValue", function (msg) { + let {selector, pseudo, name} = msg.data; + let element = content.document.querySelector(selector); + let value = content.document.defaultView.getComputedStyle(element, pseudo) + .getPropertyValue(name); + sendAsyncMessage("Test:GetComputedStylePropertyValue", value); +}); + +/** + * Wait the property value from the computed style for an element and + * compare it with the expected value + * @param {Object} data Expects a data object with the following properties + * - {String} selector: The selector used to obtain the element. + * - {String} pseudo: pseudo id to query, or null. + * - {String} name: name of the property + * - {String} expected: the expected value for property + */ +addMessageListener("Test:WaitForComputedStylePropertyValue", function (msg) { + let {selector, pseudo, name, expected} = msg.data; + let element = content.document.querySelector(selector); + waitForSuccess(() => { + let value = content.document.defaultView.getComputedStyle(element, pseudo) + .getPropertyValue(name); + + return value === expected; + }).then(() => { + sendAsyncMessage("Test:WaitForComputedStylePropertyValue"); + }); +}); + +var dumpn = msg => dump(msg + "\n"); + +/** + * Polls a given function waiting for it to return true. + * + * @param {Function} validatorFn A validator function that returns a boolean. + * This is called every few milliseconds to check if the result is true. When + * it is true, the promise resolves. + * @return a promise that resolves when the function returned true or rejects + * if the timeout is reached + */ +function waitForSuccess(validatorFn) { + let def = defer(); + + function wait(fn) { + if (fn()) { + def.resolve(); + } else { + setTimeout(() => wait(fn), 200); + } + } + wait(validatorFn); + + return def.promise; +} diff --git a/devtools/client/inspector/rules/test/doc_inline_sourcemap.html b/devtools/client/inspector/rules/test/doc_inline_sourcemap.html new file mode 100644 index 000000000..cb107d424 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_inline_sourcemap.html @@ -0,0 +1,18 @@ +<!doctype html> +<html> +<head> + <title>CSS source maps in inline stylesheets</title> +</head> +<body> + <div>CSS source maps in inline stylesheets</div> + <style> +div { + color: #ff0066; } + +span { + background-color: #EEE; } + +/*# sourceMappingURL=doc_sourcemaps.css.map */ + </style> +</body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_invalid_sourcemap.css b/devtools/client/inspector/rules/test/doc_invalid_sourcemap.css new file mode 100644 index 000000000..ff96a6b54 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_invalid_sourcemap.css @@ -0,0 +1,3 @@ +div { color: gold; }
+
+/*# sourceMappingURL=this-source-map-does-not-exist.css.map */
\ No newline at end of file diff --git a/devtools/client/inspector/rules/test/doc_invalid_sourcemap.html b/devtools/client/inspector/rules/test/doc_invalid_sourcemap.html new file mode 100644 index 000000000..2e6422bec --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_invalid_sourcemap.html @@ -0,0 +1,11 @@ +<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Invalid source map</title>
+ <link rel="stylesheet" type="text/css" href="doc_invalid_sourcemap.css">
+</head>
+<body>
+ <div>invalid source map</div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html b/devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html new file mode 100644 index 000000000..8fce04584 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>keyframe line numbers test</title> + <style type="text/css"> +div { + animation-duration: 1s; + animation-iteration-count: infinite; + animation-direction: alternate; + animation-name: CC; +} + +span { + animation-duration: 3s; + animation-iteration-count: infinite; + animation-direction: alternate; + animation-name: DD; +} + +@keyframes CC { + from { + background: #ffffff; + } + to { + background: #f0c; + } +} + +@keyframes DD { + from { + background: seagreen; + } + to { + background: chartreuse; + } +} + </style> +</head> +<body> + <div id="outer"> + <span id="inner">lizards</div> + </div> +</body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_keyframeanimation.css b/devtools/client/inspector/rules/test/doc_keyframeanimation.css new file mode 100644 index 000000000..64582ed35 --- /dev/null +++ b/devtools/client/inspector/rules/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/devtools/client/inspector/rules/test/doc_keyframeanimation.html b/devtools/client/inspector/rules/test/doc_keyframeanimation.html new file mode 100644 index 000000000..4e02c32f0 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_keyframeanimation.html @@ -0,0 +1,13 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> + <head> + <title>test case for keyframes rule in rule-view</title> + <link rel="stylesheet" type="text/css" href="doc_keyframeanimation.css"/> + </head> + <body> + <div id="pacman"></div> + <div id="boxy" class="circle"></div> + <div id="moxy" class="circle"></div> + </body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_media_queries.html b/devtools/client/inspector/rules/test/doc_media_queries.html new file mode 100644 index 000000000..1adb8bc7a --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_media_queries.html @@ -0,0 +1,24 @@ +<html> +<head> + <title>test</title> + <script type="application/javascript;version=1.7"> + + </script> + <style> + div { + width: 1000px; + height: 100px; + background-color: #f00; + } + + @media screen and (min-width: 1px) { + div { + width: 200px; + } + } + </style> +</head> +<body> +<div></div> +</body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_pseudoelement.html b/devtools/client/inspector/rules/test/doc_pseudoelement.html new file mode 100644 index 000000000..6145d4bf1 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_pseudoelement.html @@ -0,0 +1,131 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> + <head> + <style> + +body { + color: #333; +} + +.box { + float:left; + width: 128px; + height: 128px; + background: #ddd; + padding: 32px; + margin: 32px; + position:relative; +} + +.box:first-line { + color: orange; + background: red; +} + +.box:first-letter { + color: green; +} + +* { + cursor: default; +} + +nothing { + cursor: pointer; +} + +p::-moz-selection { + color: white; + background: black; +} +p::selection { + color: white; + background: black; +} + +p:first-line { + background: blue; +} +p:first-letter { + color: red; + font-size: 130%; +} + +.box:before { + background: green; + content: " "; + position: absolute; + height:32px; + width:32px; +} + +.box:after { + background: red; + content: " "; + position: absolute; + border-radius: 50%; + height:32px; + width:32px; + top: 50%; + left: 50%; + margin-top: -16px; + margin-left: -16px; +} + +.topleft:before { + top:0; + left:0; +} + +.topleft:first-line { + color: orange; +} +.topleft::selection { + color: orange; +} + +.topright:before { + top:0; + right:0; +} + +.bottomright:before { + bottom:10px; + right:10px; + color: red; +} + +.bottomright:before { + bottom:0; + right:0; +} + +.bottomleft:before { + bottom:0; + left:0; +} + + </style> + </head> + <body> + <h1>ruleview pseudoelement($("test"));</h1> + + <div id="topleft" class="box topleft"> + <p>Top Left<br />Position</p> + </div> + + <div id="topright" class="box topright"> + <p>Top Right<br />Position</p> + </div> + + <div id="bottomright" class="box bottomright"> + <p>Bottom Right<br />Position</p> + </div> + + <div id="bottomleft" class="box bottomleft"> + <p>Bottom Left<br />Position</p> + </div> + + </body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_ruleLineNumbers.html b/devtools/client/inspector/rules/test/doc_ruleLineNumbers.html new file mode 100644 index 000000000..5a157f384 --- /dev/null +++ b/devtools/client/inspector/rules/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> diff --git a/devtools/client/inspector/rules/test/doc_sourcemaps.css b/devtools/client/inspector/rules/test/doc_sourcemaps.css new file mode 100644 index 000000000..a9b437a40 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_sourcemaps.css @@ -0,0 +1,7 @@ +div { + color: #ff0066; } + +span { + background-color: #EEE; } + +/*# sourceMappingURL=doc_sourcemaps.css.map */
\ No newline at end of file diff --git a/devtools/client/inspector/rules/test/doc_sourcemaps.css.map b/devtools/client/inspector/rules/test/doc_sourcemaps.css.map new file mode 100644 index 000000000..0f7486fd9 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_sourcemaps.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAGA,GAAI;EACF,KAAK,EAHU,OAAI;;AAMrB,IAAK;EACH,gBAAgB,EAAE,IAAI", +"sources": ["doc_sourcemaps.scss"], +"names": [], +"file": "doc_sourcemaps.css" +} diff --git a/devtools/client/inspector/rules/test/doc_sourcemaps.html b/devtools/client/inspector/rules/test/doc_sourcemaps.html new file mode 100644 index 000000000..0014e55fe --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_sourcemaps.html @@ -0,0 +1,11 @@ +<!doctype html> +<html> +<head> + <title>testcase for testing CSS source maps</title> + <link rel="stylesheet" type="text/css" href="simple.css"/> + <link rel="stylesheet" type="text/css" href="doc_sourcemaps.css"/> +</head> +<body> + <div>source maps <span>testcase</span></div> +</body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_sourcemaps.scss b/devtools/client/inspector/rules/test/doc_sourcemaps.scss new file mode 100644 index 000000000..0ff6c471b --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_sourcemaps.scss @@ -0,0 +1,10 @@ + +$paulrougetpink: #f06; + +div { + color: $paulrougetpink; +} + +span { + background-color: #EEE; +}
\ No newline at end of file diff --git a/devtools/client/inspector/rules/test/doc_style_editor_link.css b/devtools/client/inspector/rules/test/doc_style_editor_link.css new file mode 100644 index 000000000..e49e1f587 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_style_editor_link.css @@ -0,0 +1,3 @@ +div { + opacity: 1; +}
\ No newline at end of file diff --git a/devtools/client/inspector/rules/test/doc_test_image.png b/devtools/client/inspector/rules/test/doc_test_image.png Binary files differnew file mode 100644 index 000000000..769c63634 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_test_image.png diff --git a/devtools/client/inspector/rules/test/doc_urls_clickable.css b/devtools/client/inspector/rules/test/doc_urls_clickable.css new file mode 100644 index 000000000..04315b2c3 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_urls_clickable.css @@ -0,0 +1,9 @@ +.relative1 { + background-image: url(./doc_test_image.png); +} +.absolute { + background: url("http://example.com/browser/devtools/client/inspector/rules/test/doc_test_image.png"); +} +.base64 { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='); +} diff --git a/devtools/client/inspector/rules/test/doc_urls_clickable.html b/devtools/client/inspector/rules/test/doc_urls_clickable.html new file mode 100644 index 000000000..b0265a703 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_urls_clickable.html @@ -0,0 +1,30 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> + <head> + + <link href="./doc_urls_clickable.css" rel="stylesheet" type="text/css"> + + <style> + .relative2 { + background-image: url(doc_test_image.png); + } + </style> + </head> + <body> + + <div class="relative1">Background image #1 with relative path (loaded from external css)</div> + + <div class="relative2">Background image #2 with relative path (loaded from style tag)</div> + + <div class="absolute">Background image with absolute path (loaded from external css)</div> + + <div class="base64">Background image with base64 url (loaded from external css)</div> + + <div class="inline" style="background: url(doc_test_image.png);">Background image with relative path (loaded from style attribute)</div> + + <div class="inline-resolved" style="background-image: url(./doc_test_image.png)">Background image with resolved relative path (loaded from style attribute)</div> + + <div class="noimage">No background image :(</div> + </body> +</html> diff --git a/devtools/client/inspector/rules/test/head.js b/devtools/client/inspector/rules/test/head.js new file mode 100644 index 000000000..5e5ede09b --- /dev/null +++ b/devtools/client/inspector/rules/test/head.js @@ -0,0 +1,840 @@ +/* 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/ */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from ../../test/head.js */ +"use strict"; + +// Import the inspector's head.js first (which itself imports shared-head.js). +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js", + this); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.defaultColorUnit"); +}); + +var {getInplaceEditorForSpan: inplaceEditor} = + require("devtools/client/shared/inplace-editor"); + +const ROOT_TEST_DIR = getRootDirectory(gTestPath); +const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js"; + +const STYLE_INSPECTOR_L10N + = new LocalizationHelper("devtools/shared/locales/styleinspector.properties"); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.defaultColorUnit"); +}); + +/** + * The rule-view tests rely on a frame-script to be injected in the content test + * page. So override the shared-head's addTab to load the frame script after the + * tab was added. + * FIXME: Refactor the rule-view tests to use the testActor instead of a frame + * script, so they can run on remote targets too. + */ +var _addTab = addTab; +addTab = function (url) { + return _addTab(url).then(tab => { + info("Loading the helper frame script " + FRAME_SCRIPT_URL); + let browser = tab.linkedBrowser; + browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false); + return tab; + }); +}; + +/** + * Wait for a content -> chrome message on the message manager (the window + * messagemanager is used). + * + * @param {String} name + * The message name + * @return {Promise} A promise that resolves to the response data when the + * message has been received + */ +function waitForContentMessage(name) { + info("Expecting message " + name + " from content"); + + let mm = gBrowser.selectedBrowser.messageManager; + + let def = defer(); + mm.addMessageListener(name, function onMessage(msg) { + mm.removeMessageListener(name, onMessage); + def.resolve(msg.data); + }); + return def.promise; +} + +/** + * Send an async message to the frame script (chrome -> content) and wait for a + * response message with the same name (content -> chrome). + * + * @param {String} name + * The message name. Should be one of the messages defined + * in doc_frame_script.js + * @param {Object} data + * Optional data to send along + * @param {Object} objects + * Optional CPOW objects to send along + * @param {Boolean} expectResponse + * If set to false, don't wait for a response with the same name + * from the content script. Defaults to true. + * @return {Promise} Resolves to the response data if a response is expected, + * immediately resolves otherwise + */ +function executeInContent(name, data = {}, objects = {}, + expectResponse = true) { + info("Sending message " + name + " to content"); + let mm = gBrowser.selectedBrowser.messageManager; + + mm.sendAsyncMessage(name, data, objects); + if (expectResponse) { + return waitForContentMessage(name); + } + + return promise.resolve(); +} + +/** + * Send an async message to the frame script and get back the requested + * computed style property. + * + * @param {String} selector + * The selector used to obtain the element. + * @param {String} pseudo + * pseudo id to query, or null. + * @param {String} name + * name of the property. + */ +function* getComputedStyleProperty(selector, pseudo, propName) { + return yield executeInContent("Test:GetComputedStylePropertyValue", + {selector, + pseudo, + name: propName}); +} + +/** + * Get an element's inline style property value. + * @param {TestActor} testActor + * @param {String} selector + * The selector used to obtain the element. + * @param {String} name + * name of the property. + */ +function getStyle(testActor, selector, propName) { + return testActor.eval(` + content.document.querySelector("${selector}") + .style.getPropertyValue("${propName}"); + `); +} + +/** + * Send an async message to the frame script and wait until the requested + * computed style property has the expected value. + * + * @param {String} selector + * The selector used to obtain the element. + * @param {String} pseudo + * pseudo id to query, or null. + * @param {String} prop + * name of the property. + * @param {String} expected + * expected value of property + * @param {String} name + * the name used in test message + */ +function* waitForComputedStyleProperty(selector, pseudo, name, expected) { + return yield executeInContent("Test:WaitForComputedStylePropertyValue", + {selector, + pseudo, + expected, + name}); +} + +/** + * Given an inplace editable element, click to switch it to edit mode, wait for + * focus + * + * @return a promise that resolves to the inplace-editor element when ready + */ +var focusEditableField = Task.async(function* (ruleView, editable, xOffset = 1, + yOffset = 1, options = {}) { + let onFocus = once(editable.parentNode, "focus", true); + info("Clicking on editable field to turn to edit mode"); + EventUtils.synthesizeMouse(editable, xOffset, yOffset, options, + editable.ownerDocument.defaultView); + yield onFocus; + + info("Editable field gained focus, returning the input field now"); + let onEdit = inplaceEditor(editable.ownerDocument.activeElement); + + return onEdit; +}); + +/** + * When a tooltip is closed, this ends up "commiting" the value changed within + * the tooltip (e.g. the color in case of a colorpicker) which, in turn, ends up + * setting the value of the corresponding css property in the rule-view. + * Use this function to close the tooltip and make sure the test waits for the + * ruleview-changed event. + * @param {SwatchBasedEditorTooltip} editorTooltip + * @param {CSSRuleView} view + */ +function* hideTooltipAndWaitForRuleViewChanged(editorTooltip, view) { + let onModified = view.once("ruleview-changed"); + let onHidden = editorTooltip.tooltip.once("hidden"); + editorTooltip.hide(); + yield onModified; + yield onHidden; +} + +/** + * Polls a given generator function waiting for it to return true. + * + * @param {Function} validatorFn + * A validator generator function that returns a boolean. + * This is called every few milliseconds to check if the result is true. + * When it is true, the promise resolves. + * @param {String} name + * Optional name of the test. This is used to generate + * the success and failure messages. + * @return a promise that resolves when the function returned true or rejects + * if the timeout is reached + */ +var waitForSuccess = Task.async(function* (validatorFn, desc = "untitled") { + let i = 0; + while (true) { + info("Checking: " + desc); + if (yield validatorFn()) { + ok(true, "Success: " + desc); + break; + } + i++; + if (i > 10) { + ok(false, "Failure: " + desc); + break; + } + yield new Promise(r => setTimeout(r, 200)); + } +}); + +/** + * Get the DOMNode for a css rule in the rule-view that corresponds to the given + * selector + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view for which the rule + * object is wanted + * @return {DOMNode} + */ +function getRuleViewRule(view, selectorText) { + let rule; + for (let r of view.styleDocument.querySelectorAll(".ruleview-rule")) { + let selector = r.querySelector(".ruleview-selectorcontainer, " + + ".ruleview-selector-matched"); + if (selector && selector.textContent === selectorText) { + rule = r; + break; + } + } + + return rule; +} + +/** + * Get references to the name and value span nodes corresponding to a given + * selector and property name in the rule-view + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view to look for the property in + * @param {String} propertyName + * The name of the property + * @return {Object} An object like {nameSpan: DOMNode, valueSpan: DOMNode} + */ +function getRuleViewProperty(view, selectorText, propertyName) { + let prop; + + let rule = getRuleViewRule(view, selectorText); + if (rule) { + // Look for the propertyName in that rule element + for (let p of rule.querySelectorAll(".ruleview-property")) { + let nameSpan = p.querySelector(".ruleview-propertyname"); + let valueSpan = p.querySelector(".ruleview-propertyvalue"); + + if (nameSpan.textContent === propertyName) { + prop = {nameSpan: nameSpan, valueSpan: valueSpan}; + break; + } + } + } + return prop; +} + +/** + * Get the text value of the property corresponding to a given selector and name + * in the rule-view + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view to look for the property in + * @param {String} propertyName + * The name of the property + * @return {String} The property value + */ +function getRuleViewPropertyValue(view, selectorText, propertyName) { + return getRuleViewProperty(view, selectorText, propertyName) + .valueSpan.textContent; +} + +/** + * Get a reference to the selector DOM element corresponding to a given selector + * in the rule-view + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view to look for + * @return {DOMNode} The selector DOM element + */ +function getRuleViewSelector(view, selectorText) { + let rule = getRuleViewRule(view, selectorText); + return rule.querySelector(".ruleview-selector, .ruleview-selector-matched"); +} + +/** + * Get a reference to the selectorhighlighter icon DOM element corresponding to + * a given selector in the rule-view + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view to look for + * @return {DOMNode} The selectorhighlighter icon DOM element + */ +function getRuleViewSelectorHighlighterIcon(view, selectorText) { + let rule = getRuleViewRule(view, selectorText); + return rule.querySelector(".ruleview-selectorhighlighter"); +} + +/** + * Simulate a color change in a given color picker tooltip, and optionally wait + * for a given element in the page to have its style changed as a result. + * Note that this function assumes that the colorpicker popup is already open + * and it won't close it after having selected the new color. + * + * @param {RuleView} ruleView + * The related rule view instance + * @param {SwatchColorPickerTooltip} colorPicker + * @param {Array} newRgba + * The new color to be set [r, g, b, a] + * @param {Object} expectedChange + * Optional object that needs the following props: + * - {String} selector The selector to the element in the page that + * will have its style changed. + * - {String} name The style name that will be changed + * - {String} value The expected style value + * The style will be checked like so: getComputedStyle(element)[name] === value + */ +var simulateColorPickerChange = Task.async(function* (ruleView, colorPicker, + newRgba, expectedChange) { + let onComputedStyleChanged; + if (expectedChange) { + let {selector, name, value} = expectedChange; + onComputedStyleChanged = waitForComputedStyleProperty(selector, null, name, value); + } + let onRuleViewChanged = ruleView.once("ruleview-changed"); + info("Getting the spectrum colorpicker object"); + let spectrum = colorPicker.spectrum; + info("Setting the new color"); + spectrum.rgb = newRgba; + info("Applying the change"); + spectrum.updateUI(); + spectrum.onChange(); + info("Waiting for rule-view to update"); + yield onRuleViewChanged; + + if (expectedChange) { + info("Waiting for the style to be applied on the page"); + yield onComputedStyleChanged; + } +}); + +/** + * Open the color picker popup for a given property in a given rule and + * simulate a color change. Optionally wait for a given element in the page to + * have its style changed as a result. + * + * @param {RuleView} view + * The related rule view instance + * @param {Number} ruleIndex + * Which rule to target in the rule view + * @param {Number} propIndex + * Which property to target in the rule + * @param {Array} newRgba + * The new color to be set [r, g, b, a] + * @param {Object} expectedChange + * Optional object that needs the following props: + * - {String} selector The selector to the element in the page that + * will have its style changed. + * - {String} name The style name that will be changed + * - {String} value The expected style value + * The style will be checked like so: getComputedStyle(element)[name] === value + */ +var openColorPickerAndSelectColor = Task.async(function* (view, ruleIndex, + propIndex, newRgba, expectedChange) { + let ruleEditor = getRuleViewRuleEditor(view, ruleIndex); + let propEditor = ruleEditor.rule.textProps[propIndex].editor; + let swatch = propEditor.valueSpan.querySelector(".ruleview-colorswatch"); + let cPicker = view.tooltips.colorPicker; + + info("Opening the colorpicker by clicking the color swatch"); + let onColorPickerReady = cPicker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + yield simulateColorPickerChange(view, cPicker, newRgba, expectedChange); + + return {propEditor, swatch, cPicker}; +}); + +/** + * Open the cubicbezier popup for a given property in a given rule and + * simulate a curve change. Optionally wait for a given element in the page to + * have its style changed as a result. + * + * @param {RuleView} view + * The related rule view instance + * @param {Number} ruleIndex + * Which rule to target in the rule view + * @param {Number} propIndex + * Which property to target in the rule + * @param {Array} coords + * The new coordinates to be used, e.g. [0.1, 2, 0.9, -1] + * @param {Object} expectedChange + * Optional object that needs the following props: + * - {String} selector The selector to the element in the page that + * will have its style changed. + * - {String} name The style name that will be changed + * - {String} value The expected style value + * The style will be checked like so: getComputedStyle(element)[name] === value + */ +var openCubicBezierAndChangeCoords = Task.async(function* (view, ruleIndex, + propIndex, coords, expectedChange) { + let ruleEditor = getRuleViewRuleEditor(view, ruleIndex); + let propEditor = ruleEditor.rule.textProps[propIndex].editor; + let swatch = propEditor.valueSpan.querySelector(".ruleview-bezierswatch"); + let bezierTooltip = view.tooltips.cubicBezier; + + info("Opening the cubicBezier by clicking the swatch"); + let onBezierWidgetReady = bezierTooltip.once("ready"); + swatch.click(); + yield onBezierWidgetReady; + + let widget = yield bezierTooltip.widget; + + info("Simulating a change of curve in the widget"); + let onRuleViewChanged = view.once("ruleview-changed"); + widget.coordinates = coords; + yield onRuleViewChanged; + + if (expectedChange) { + info("Waiting for the style to be applied on the page"); + let {selector, name, value} = expectedChange; + yield waitForComputedStyleProperty(selector, null, name, value); + } + + return {propEditor, swatch, bezierTooltip}; +}); + +/** + * Get a rule-link from the rule-view given its index + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {Number} index + * The index of the link to get + * @return {DOMNode} The link if any at this index + */ +function getRuleViewLinkByIndex(view, index) { + let links = view.styleDocument.querySelectorAll(".ruleview-rule-source"); + return links[index]; +} + +/** + * Get rule-link text from the rule-view given its index + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {Number} index + * The index of the link to get + * @return {String} The string at this index + */ +function getRuleViewLinkTextByIndex(view, index) { + let link = getRuleViewLinkByIndex(view, index); + return link.querySelector(".ruleview-rule-source-label").textContent; +} + +/** + * Simulate adding a new property in an existing rule in the rule-view. + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {Number} ruleIndex + * The index of the rule to use. Note that if ruleIndex is 0, you might + * want to also listen to markupmutation events in your test since + * that's going to change the style attribute of the selected node. + * @param {String} name + * The name for the new property + * @param {String} value + * The value for the new property + * @param {String} commitValueWith + * Which key should be used to commit the new value. VK_RETURN is used by + * default, but tests might want to use another key to test cancelling + * for exemple. + * @param {Boolean} blurNewProperty + * After the new value has been added, a new property would have been + * focused. This parameter is true by default, and that causes the new + * property to be blurred. Set to false if you don't want this. + * @return {TextProperty} The instance of the TextProperty that was added + */ +var addProperty = Task.async(function* (view, ruleIndex, name, value, + commitValueWith = "VK_RETURN", + blurNewProperty = true) { + info("Adding new property " + name + ":" + value + " to rule " + ruleIndex); + + let ruleEditor = getRuleViewRuleEditor(view, ruleIndex); + let editor = yield focusNewRuleViewProperty(ruleEditor); + let numOfProps = ruleEditor.rule.textProps.length; + + info("Adding name " + name); + editor.input.value = name; + let onNameAdded = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onNameAdded; + + // Focus has moved to the value inplace-editor automatically. + editor = inplaceEditor(view.styleDocument.activeElement); + let textProps = ruleEditor.rule.textProps; + let textProp = textProps[textProps.length - 1]; + + is(ruleEditor.rule.textProps.length, numOfProps + 1, + "A new test property was added"); + is(editor, inplaceEditor(textProp.editor.valueSpan), + "The inplace editor appeared for the value"); + + info("Adding value " + value); + // Setting the input value schedules a preview to be shown in 10ms which + // triggers a ruleview-changed event (see bug 1209295). + let onPreview = view.once("ruleview-changed"); + editor.input.value = value; + view.throttle.flush(); + yield onPreview; + + let onValueAdded = view.once("ruleview-changed"); + EventUtils.synthesizeKey(commitValueWith, {}, view.styleWindow); + yield onValueAdded; + + if (blurNewProperty) { + view.styleDocument.activeElement.blur(); + } + + return textProp; +}); + +/** + * Simulate changing the value of a property in a rule in the rule-view. + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {TextProperty} textProp + * The instance of the TextProperty to be changed + * @param {String} value + * The new value to be used. If null is passed, then the value will be + * deleted + * @param {Boolean} blurNewProperty + * After the value has been changed, a new property would have been + * focused. This parameter is true by default, and that causes the new + * property to be blurred. Set to false if you don't want this. + */ +var setProperty = Task.async(function* (view, textProp, value, + blurNewProperty = true) { + yield focusEditableField(view, textProp.editor.valueSpan); + + let onPreview = view.once("ruleview-changed"); + if (value === null) { + EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow); + } else { + EventUtils.sendString(value, view.styleWindow); + } + view.throttle.flush(); + yield onPreview; + + let onValueDone = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onValueDone; + + if (blurNewProperty) { + view.styleDocument.activeElement.blur(); + } +}); + +/** + * Simulate removing a property from an existing rule in the rule-view. + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {TextProperty} textProp + * The instance of the TextProperty to be removed + * @param {Boolean} blurNewProperty + * After the property has been removed, a new property would have been + * focused. This parameter is true by default, and that causes the new + * property to be blurred. Set to false if you don't want this. + */ +var removeProperty = Task.async(function* (view, textProp, + blurNewProperty = true) { + yield focusEditableField(view, textProp.editor.nameSpan); + + let onModifications = view.once("ruleview-changed"); + info("Deleting the property name now"); + EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onModifications; + + if (blurNewProperty) { + view.styleDocument.activeElement.blur(); + } +}); + +/** + * Simulate clicking the enable/disable checkbox next to a property in a rule. + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {TextProperty} textProp + * The instance of the TextProperty to be enabled/disabled + */ +var togglePropStatus = Task.async(function* (view, textProp) { + let onRuleViewRefreshed = view.once("ruleview-changed"); + textProp.editor.enable.click(); + yield onRuleViewRefreshed; +}); + +/** + * Click on a rule-view's close brace to focus a new property name editor + * + * @param {RuleEditor} ruleEditor + * An instance of RuleEditor that will receive the new property + * @return a promise that resolves to the newly created editor when ready and + * focused + */ +var focusNewRuleViewProperty = Task.async(function* (ruleEditor) { + info("Clicking on a close ruleEditor brace to start editing a new property"); + + // Use bottom alignment to avoid scrolling out of the parent element area. + ruleEditor.closeBrace.scrollIntoView(false); + let editor = yield focusEditableField(ruleEditor.ruleView, + ruleEditor.closeBrace); + + is(inplaceEditor(ruleEditor.newPropSpan), editor, + "Focused editor is the new property editor."); + + return editor; +}); + +/** + * Create a new property name in the rule-view, focusing a new property editor + * by clicking on the close brace, and then entering the given text. + * Keep in mind that the rule-view knows how to handle strings with multiple + * properties, so the input text may be like: "p1:v1;p2:v2;p3:v3". + * + * @param {RuleEditor} ruleEditor + * The instance of RuleEditor that will receive the new property(ies) + * @param {String} inputValue + * The text to be entered in the new property name field + * @return a promise that resolves when the new property name has been entered + * and once the value field is focused + */ +var createNewRuleViewProperty = Task.async(function* (ruleEditor, inputValue) { + info("Creating a new property editor"); + let editor = yield focusNewRuleViewProperty(ruleEditor); + + info("Entering the value " + inputValue); + editor.input.value = inputValue; + + info("Submitting the new value and waiting for value field focus"); + let onFocus = once(ruleEditor.element, "focus", true); + EventUtils.synthesizeKey("VK_RETURN", {}, + ruleEditor.element.ownerDocument.defaultView); + yield onFocus; +}); + +/** + * Set the search value for the rule-view filter styles search box. + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} searchValue + * The filter search value + * @return a promise that resolves when the rule-view is filtered for the + * search term + */ +var setSearchFilter = Task.async(function* (view, searchValue) { + info("Setting filter text to \"" + searchValue + "\""); + let win = view.styleWindow; + let searchField = view.searchField; + searchField.focus(); + synthesizeKeys(searchValue, win); + yield view.inspector.once("ruleview-filtered"); +}); + +/** + * Reload the current page and wait for the inspector to be initialized after + * the navigation + * + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox + * @param {TestActor} testActor + * The current instance of the TestActor + */ +function* reloadPage(inspector, testActor) { + let onNewRoot = inspector.once("new-root"); + yield testActor.reload(); + yield onNewRoot; + yield inspector.markup._waitForChildren(); +} + +/** + * Create a new rule by clicking on the "add rule" button. + * This will leave the selector inplace-editor active. + * + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox + * @param {CssRuleView} view + * The instance of the rule-view panel + * @return a promise that resolves after the rule has been added + */ +function* addNewRule(inspector, view) { + info("Adding the new rule using the button"); + view.addRuleButton.click(); + + info("Waiting for rule view to change"); + yield view.once("ruleview-changed"); +} + +/** + * Create a new rule by clicking on the "add rule" button, dismiss the editor field and + * verify that the selector is correct. + * + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} expectedSelector + * The value we expect the selector to have + * @param {Number} expectedIndex + * The index we expect the rule to have in the rule-view + * @return a promise that resolves after the rule has been added + */ +function* addNewRuleAndDismissEditor(inspector, view, expectedSelector, expectedIndex) { + yield addNewRule(inspector, view); + + info("Getting the new rule at index " + expectedIndex); + let ruleEditor = getRuleViewRuleEditor(view, expectedIndex); + let editor = ruleEditor.selectorText.ownerDocument.activeElement; + is(editor.value, expectedSelector, + "The editor for the new selector has the correct value: " + expectedSelector); + + info("Pressing escape to leave the editor"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + + is(ruleEditor.selectorText.textContent, expectedSelector, + "The new selector has the correct text: " + expectedSelector); +} + +/** + * Simulate a sequence of non-character keys (return, escape, tab) and wait for + * a given element to receive the focus. + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {DOMNode} element + * The element that should be focused + * @param {Array} keys + * Array of non-character keys, the part that comes after "DOM_VK_" eg. + * "RETURN", "ESCAPE" + * @return a promise that resolves after the element received the focus + */ +function* sendKeysAndWaitForFocus(view, element, keys) { + let onFocus = once(element, "focus", true); + for (let key of keys) { + EventUtils.sendKey(key, view.styleWindow); + } + yield onFocus; +} + +/** + * Open the style editor context menu and return all of it's items in a flat array + * @param {CssRuleView} view + * The instance of the rule-view panel + * @return An array of MenuItems + */ +function openStyleContextMenuAndGetAllItems(view, target) { + let menu = view._contextmenu._openMenu({target: target}); + + // Flatten all menu items into a single array to make searching through it easier + let allItems = [].concat.apply([], menu.items.map(function addItem(item) { + if (item.submenu) { + return addItem(item.submenu.items); + } + return item; + })); + + return allItems; +} + +/** + * Wait for a markupmutation event on the inspector that is for a style modification. + * @param {InspectorPanel} inspector + * @return {Promise} + */ +function waitForStyleModification(inspector) { + return new Promise(function (resolve) { + function checkForStyleModification(name, mutations) { + for (let mutation of mutations) { + if (mutation.type === "attributes" && mutation.attributeName === "style") { + inspector.off("markupmutation", checkForStyleModification); + resolve(); + return; + } + } + } + inspector.on("markupmutation", checkForStyleModification); + }); +} + +/** + * Click on the selector icon + * @param {DOMNode} icon + * @param {CSSRuleView} view + */ +function* clickSelectorIcon(icon, view) { + let onToggled = view.once("ruleview-selectorhighlighter-toggled"); + EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow); + yield onToggled; +} + +/** + * Make sure window is properly focused before sending a key event. + * @param {Window} win + * @param {Event} key + */ +function focusAndSendKey(win, key) { + win.document.documentElement.focus(); + EventUtils.sendKey(key, win); +} diff --git a/devtools/client/inspector/rules/views/moz.build b/devtools/client/inspector/rules/views/moz.build new file mode 100644 index 000000000..ac0a24d76 --- /dev/null +++ b/devtools/client/inspector/rules/views/moz.build @@ -0,0 +1,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/. + +DevToolsModules( + 'rule-editor.js', + 'text-property-editor.js', +) diff --git a/devtools/client/inspector/rules/views/rule-editor.js b/devtools/client/inspector/rules/views/rule-editor.js new file mode 100644 index 000000000..2587bf19c --- /dev/null +++ b/devtools/client/inspector/rules/views/rule-editor.js @@ -0,0 +1,620 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {l10n} = require("devtools/shared/inspector/css-logic"); +const {ELEMENT_STYLE} = require("devtools/shared/specs/styles"); +const {PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils"); +const {Rule} = require("devtools/client/inspector/rules/models/rule"); +const {InplaceEditor, editableField, editableItem} = + require("devtools/client/shared/inplace-editor"); +const {TextPropertyEditor} = + require("devtools/client/inspector/rules/views/text-property-editor"); +const { + createChild, + blurOnMultipleProperties, + promiseWarn +} = require("devtools/client/inspector/shared/utils"); +const { + parseDeclarations, + parsePseudoClassesAndAttributes, + SELECTOR_ATTRIBUTE, + SELECTOR_ELEMENT, + SELECTOR_PSEUDO_CLASS +} = require("devtools/shared/css/parsing-utils"); +const promise = require("promise"); +const Services = require("Services"); +const EventEmitter = require("devtools/shared/event-emitter"); +const {Task} = require("devtools/shared/task"); + +const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties"; +const {LocalizationHelper} = require("devtools/shared/l10n"); +const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES); + +/** + * RuleEditor is responsible for the following: + * Owns a Rule object and creates a list of TextPropertyEditors + * for its TextProperties. + * Manages creation of new text properties. + * + * One step of a RuleEditor's instantiation is figuring out what's the original + * source link to the parent stylesheet (in case of source maps). This step is + * asynchronous and is triggered as soon as the RuleEditor is instantiated (see + * updateSourceLink). If you need to know when the RuleEditor is done with this, + * you need to listen to the source-link-updated event. + * + * @param {CssRuleView} ruleView + * The CssRuleView containg the document holding this rule editor. + * @param {Rule} rule + * The Rule object we're editing. + */ +function RuleEditor(ruleView, rule) { + EventEmitter.decorate(this); + + this.ruleView = ruleView; + this.doc = this.ruleView.styleDocument; + this.toolbox = this.ruleView.inspector.toolbox; + this.rule = rule; + + this.isEditable = !rule.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.updateSourceLink = this.updateSourceLink.bind(this); + + this.rule.domRule.on("location-changed", this._locationChanged); + this.toolbox.on("tool-registered", this.updateSourceLink); + this.toolbox.on("tool-unregistered", this.updateSourceLink); + + this._create(); +} + +RuleEditor.prototype = { + destroy: function () { + this.rule.domRule.off("location-changed"); + this.toolbox.off("tool-registered", this.updateSourceLink); + this.toolbox.off("tool-unregistered", this.updateSourceLink); + }, + + get isSelectorEditable() { + let trait = this.isEditable && + this.ruleView.inspector.target.client.traits.selectorEditable && + this.rule.domRule.type !== ELEMENT_STYLE && + this.rule.domRule.type !== CSSRule.KEYFRAME_RULE; + + // Do not allow editing anonymousselectors until we can + // detect mutations on pseudo elements in Bug 1034110. + return trait && !this.rule.elementStyle.element.isAnonymous; + }, + + _create: function () { + this.element = this.doc.createElement("div"); + this.element.className = "ruleview-rule theme-separator"; + this.element.setAttribute("uneditable", !this.isEditable); + this.element.setAttribute("unmatched", this.rule.isUnmatched); + this.element._ruleEditor = this; + + // Give a relative position for the inplace editor's measurement + // span to be placed absolutely against. + this.element.style.position = "relative"; + + // Add the source link. + this.source = createChild(this.element, "div", { + class: "ruleview-rule-source theme-link" + }); + this.source.addEventListener("click", function () { + if (this.source.hasAttribute("unselectable")) { + return; + } + let rule = this.rule.domRule; + this.ruleView.emit("ruleview-linked-clicked", rule); + }.bind(this)); + let sourceLabel = this.doc.createElement("span"); + sourceLabel.classList.add("ruleview-rule-source-label"); + this.source.appendChild(sourceLabel); + + this.updateSourceLink(); + + let code = createChild(this.element, "div", { + class: "ruleview-code" + }); + + let header = createChild(code, "div", {}); + + this.selectorText = createChild(header, "span", { + class: "ruleview-selectorcontainer theme-fg-color3", + tabindex: this.isSelectorEditable ? "0" : "-1", + }); + + if (this.isSelectorEditable) { + this.selectorText.addEventListener("click", event => { + // Clicks within the selector shouldn't propagate any further. + event.stopPropagation(); + }, false); + + editableField({ + element: this.selectorText, + done: this._onSelectorDone, + cssProperties: this.rule.cssProperties, + contextMenu: this.ruleView.inspector.onTextBoxContextMenu + }); + } + + if (this.rule.domRule.type !== CSSRule.KEYFRAME_RULE) { + let selector = this.rule.domRule.selectors + ? this.rule.domRule.selectors.join(", ") + : this.ruleView.inspector.selectionCssSelector; + + let selectorHighlighter = createChild(header, "span", { + class: "ruleview-selectorhighlighter" + + (this.ruleView.highlighters.selectorHighlighterShown === selector ? + " highlighted" : ""), + title: l10n("rule.selectorHighlighter.tooltip") + }); + selectorHighlighter.addEventListener("click", () => { + this.ruleView.toggleSelectorHighlighter(selectorHighlighter, selector); + }); + } + + this.openBrace = createChild(header, "span", { + class: "ruleview-ruleopen", + textContent: " {" + }); + + this.propertyList = createChild(code, "ul", { + class: "ruleview-propertylist" + }); + + this.populate(); + + this.closeBrace = createChild(code, "div", { + class: "ruleview-ruleclose", + tabindex: this.isEditable ? "0" : "-1", + textContent: "}" + }); + + if (this.isEditable) { + // A newProperty editor should only be created when no editor was + // previously displayed. Since the editors are cleared on blur, + // check this.ruleview.isEditing on mousedown + this._ruleViewIsEditing = false; + + code.addEventListener("mousedown", () => { + this._ruleViewIsEditing = this.ruleView.isEditing; + }); + + code.addEventListener("click", () => { + let selection = this.doc.defaultView.getSelection(); + if (selection.isCollapsed && !this._ruleViewIsEditing) { + this.newProperty(); + } + // Cleanup the _ruleViewIsEditing flag + this._ruleViewIsEditing = false; + }, false); + + this.element.addEventListener("mousedown", () => { + this.doc.defaultView.focus(); + }, false); + + // 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 () { + 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 : title; + let sourceLine = this.rule.ruleLine > 0 ? ":" + this.rule.ruleLine : ""; + + sourceLabel.setAttribute("title", sourceHref + sourceLine); + + if (this.toolbox.isToolRegistered("styleeditor")) { + this.source.removeAttribute("unselectable"); + } else { + this.source.setAttribute("unselectable", true); + } + + if (this.rule.isSystem) { + let uaLabel = STYLE_INSPECTOR_L10N.getStr("rule.userAgentStyles"); + sourceLabel.textContent = 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") { + this.source.setAttribute("unselectable", "true"); + sourceLabel.textContent = uaLabel; + sourceLabel.removeAttribute("title"); + } + } else { + sourceLabel.textContent = title; + if (this.rule.ruleLine === -1 && this.rule.domRule.parentStyleSheet) { + this.source.setAttribute("unselectable", "true"); + } + } + + let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); + if (showOrig && !this.rule.isSystem && + this.rule.domRule.type !== ELEMENT_STYLE) { + // Only get the original source link if the right pref is set, if the rule + // isn't a system rule and if it isn't an inline rule. + this.rule.getOriginalSourceStrings().then((strings) => { + sourceLabel.textContent = strings.short; + sourceLabel.setAttribute("title", strings.full); + }, e => console.error(e)).then(() => { + this.emit("source-link-updated"); + }); + } else { + // If we're not getting the original source link, then we can emit the + // event immediately (but still asynchronously to give consumers a chance + // to register it after having instantiated the RuleEditor). + promise.resolve().then(() => { + this.emit("source-link-updated"); + }); + } + }, + + /** + * Update the rule editor with the contents of the rule. + */ + populate: function () { + // Clear out existing viewers. + while (this.selectorText.hasChildNodes()) { + this.selectorText.removeChild(this.selectorText.lastChild); + } + + // If selector text comes from a css rule, highlight selectors that + // actually match. For custom selector text (such as for the 'element' + // 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 === CSSRule.KEYFRAME_RULE) { + this.selectorText.textContent = this.rule.domRule.keyText; + } else { + this.rule.domRule.selectors.forEach((selector, i) => { + if (i !== 0) { + createChild(this.selectorText, "span", { + class: "ruleview-selector-separator", + textContent: ", " + }); + } + + let containerClass = + (this.rule.matchedSelectors.indexOf(selector) > -1) ? + "ruleview-selector-matched" : "ruleview-selector-unmatched"; + let selectorContainer = createChild(this.selectorText, "span", { + class: containerClass + }); + + let parsedSelector = parsePseudoClassesAndAttributes(selector); + + for (let selectorText of parsedSelector) { + let selectorClass = ""; + + switch (selectorText.type) { + case SELECTOR_ATTRIBUTE: + selectorClass = "ruleview-selector-attribute"; + break; + case SELECTOR_ELEMENT: + selectorClass = "ruleview-selector"; + break; + case SELECTOR_PSEUDO_CLASS: + selectorClass = [":active", ":focus", ":hover"].some( + pseudo => selectorText.value === pseudo) ? + "ruleview-selector-pseudo-class-lock" : + "ruleview-selector-pseudo-class"; + break; + default: + break; + } + + createChild(selectorContainer, "span", { + textContent: selectorText.value, + class: selectorClass + }); + } + }); + } + + for (let prop of this.rule.textProps) { + if (!prop.editor && !prop.invisible) { + let editor = new TextPropertyEditor(this, prop); + this.propertyList.appendChild(editor.element); + } + } + }, + + /** + * Programatically add a new property to the rule. + * + * @param {String} name + * Property name. + * @param {String} value + * Property value. + * @param {String} priority + * Property priority. + * @param {Boolean} enabled + * True if the property should be enabled. + * @param {TextProperty} siblingProp + * Optional, property next to which the new property will be added. + * @return {TextProperty} + * The new property + */ + addProperty: function (name, value, priority, enabled, siblingProp) { + let prop = this.rule.createProperty(name, value, priority, enabled, + siblingProp); + let index = this.rule.textProps.indexOf(prop); + let editor = new TextPropertyEditor(this, prop); + + // Insert this node before the DOM node that is currently at its new index + // in the property list. There is currently one less node in the DOM than + // in the property list, so this causes it to appear after siblingProp. + // If there is no node at its index, as is the case where this is the last + // node being inserted, then this behaves as appendChild. + this.propertyList.insertBefore(editor.element, + this.propertyList.children[index]); + + return prop; + }, + + /** + * Programatically add a list of new properties to the rule. Focus the UI + * to the proper location after adding (either focus the value on the + * last property if it is empty, or create a new property and focus it). + * + * @param {Array} properties + * Array of properties, which are objects with this signature: + * { + * name: {string}, + * value: {string}, + * priority: {string} + * } + * @param {TextProperty} siblingProp + * Optional, the property next to which all new props should be added. + */ + addProperties: function (properties, siblingProp) { + if (!properties || !properties.length) { + return; + } + + let lastProp = siblingProp; + for (let p of properties) { + let isCommented = Boolean(p.commentOffsets); + let enabled = !isCommented; + lastProp = this.addProperty(p.name, p.value, p.priority, enabled, + lastProp); + } + + // Either focus on the last value if incomplete, or start a new one. + if (lastProp && lastProp.value.trim() === "") { + lastProp.editor.valueSpan.click(); + } else { + this.newProperty(); + } + }, + + /** + * Create a text input for a property name. If a non-empty property + * name is given, we'll create a real TextProperty and add it to the + * rule. + */ + newProperty: function () { + // If we're already creating a new property, ignore this. + if (!this.closeBrace.hasAttribute("tabindex")) { + return; + } + + // While we're editing a new property, it doesn't make sense to + // start a second new property editor, so disable focusing the + // close brace for now. + this.closeBrace.removeAttribute("tabindex"); + + this.newPropItem = createChild(this.propertyList, "li", { + class: "ruleview-property ruleview-newproperty", + }); + + this.newPropSpan = createChild(this.newPropItem, "span", { + class: "ruleview-propertyname", + tabindex: "0" + }); + + this.multipleAddedProperties = null; + + this.editor = new InplaceEditor({ + element: this.newPropSpan, + done: this._onNewProperty, + destroy: this._newPropertyDestroy, + advanceChars: ":", + contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY, + popup: this.ruleView.popup, + cssProperties: this.rule.cssProperties, + contextMenu: this.ruleView.inspector.onTextBoxContextMenu + }); + + // Auto-close the input if multiple rules get pasted into new property. + this.editor.input.addEventListener("paste", + blurOnMultipleProperties(this.rule.cssProperties), false); + }, + + /** + * Called when the new property input has been dismissed. + * + * @param {String} value + * The value in the editor. + * @param {Boolean} commit + * True if the value should be committed. + */ + _onNewProperty: function (value, commit) { + if (!value || !commit) { + return; + } + + // parseDeclarations allows for name-less declarations, but in the present + // case, we're creating a new declaration, it doesn't make sense to accept + // these entries + this.multipleAddedProperties = + parseDeclarations(this.rule.cssProperties.isKnown, value, true) + .filter(d => d.name); + + // Blur the editor field now and deal with adding declarations later when + // the field gets destroyed (see _newPropertyDestroy) + this.editor.input.blur(); + }, + + /** + * Called when the new property editor is destroyed. + * This is where the properties (type TextProperty) are actually being + * added, since we want to wait until after the inplace editor `destroy` + * event has been fired to keep consistent UI state. + */ + _newPropertyDestroy: function () { + // We're done, make the close brace focusable again. + this.closeBrace.setAttribute("tabindex", "0"); + + this.propertyList.removeChild(this.newPropItem); + delete this.newPropItem; + delete this.newPropSpan; + + // If properties were added, we want to focus the proper element. + // If the last new property has no value, focus the value on it. + // Otherwise, start a new property and focus that field. + if (this.multipleAddedProperties && this.multipleAddedProperties.length) { + this.addProperties(this.multipleAddedProperties); + } + }, + + /** + * Called when the selector's inplace editor is closed. + * Ignores the change if the user pressed escape, otherwise + * commits it. + * + * @param {String} value + * The value contained in the editor. + * @param {Boolean} commit + * True if the change should be applied. + * @param {Number} direction + * The move focus direction number. + */ + _onSelectorDone: Task.async(function* (value, commit, direction) { + if (!commit || this.isEditing || value === "" || + value === this.rule.selectorText) { + return; + } + + let ruleView = this.ruleView; + let elementStyle = ruleView._elementStyle; + let element = elementStyle.element; + let supportsUnmatchedRules = + this.rule.domRule.supportsModifySelectorUnmatched; + + this.isEditing = true; + + try { + let response = yield this.rule.domRule.modifySelector(element, value); + + if (!supportsUnmatchedRules) { + this.isEditing = false; + + if (response) { + this.ruleView.refreshPanel(); + } + return; + } + + // We recompute the list of applied styles, because editing a + // selector might cause this rule's position to change. + let applied = yield elementStyle.pageStyle.getApplied(element, { + inherited: true, + matchedSelectors: true, + filter: elementStyle.showUserAgentStyles ? "ua" : undefined + }); + + this.isEditing = false; + + let {ruleProps, isMatching} = response; + if (!ruleProps) { + // Notify for changes, even when nothing changes, + // just to allow tests being able to track end of this request. + ruleView.emit("ruleview-invalid-selector"); + return; + } + + ruleProps.isUnmatched = !isMatching; + let newRule = new Rule(elementStyle, ruleProps); + let editor = new RuleEditor(ruleView, newRule); + let rules = elementStyle.rules; + + let newRuleIndex = applied.findIndex((r) => r.rule == ruleProps.rule); + let oldIndex = rules.indexOf(this.rule); + + // If the selector no longer matches, then we leave the rule in + // the same relative position. + if (newRuleIndex === -1) { + newRuleIndex = oldIndex; + } + + // Remove the old rule and insert the new rule. + rules.splice(oldIndex, 1); + rules.splice(newRuleIndex, 0, newRule); + elementStyle._changed(); + elementStyle.markOverriddenAll(); + + // We install the new editor in place of the old -- you might + // think we would replicate the list-modification logic above, + // but that is complicated due to the way the UI installs + // pseudo-element rules and the like. + this.element.parentNode.replaceChild(editor.element, this.element); + + // Remove highlight for modified selector + if (ruleView.highlighters.selectorHighlighterShown) { + ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon, + ruleView.highlighters.selectorHighlighterShown); + } + + editor._moveSelectorFocus(direction); + } catch (err) { + this.isEditing = false; + promiseWarn(err); + } + }), + + /** + * Handle moving the focus change after a tab or return keypress in the + * selector inplace editor. + * + * @param {Number} direction + * The move focus direction number. + */ + _moveSelectorFocus: function (direction) { + if (!direction || direction === Services.focus.MOVEFOCUS_BACKWARD) { + return; + } + + if (this.rule.textProps.length > 0) { + this.rule.textProps[0].editor.nameSpan.click(); + } else { + this.propertyList.click(); + } + } +}; + +exports.RuleEditor = RuleEditor; diff --git a/devtools/client/inspector/rules/views/text-property-editor.js b/devtools/client/inspector/rules/views/text-property-editor.js new file mode 100644 index 000000000..d3015f931 --- /dev/null +++ b/devtools/client/inspector/rules/views/text-property-editor.js @@ -0,0 +1,880 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {l10n} = require("devtools/shared/inspector/css-logic"); +const {getCssProperties} = require("devtools/shared/fronts/css-properties"); +const {InplaceEditor, editableField} = + require("devtools/client/shared/inplace-editor"); +const { + createChild, + appendText, + advanceValidate, + blurOnMultipleProperties +} = require("devtools/client/inspector/shared/utils"); +const { + parseDeclarations, + parseSingleValue, +} = require("devtools/shared/css/parsing-utils"); +const Services = require("Services"); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; + +const SHARED_SWATCH_CLASS = "ruleview-swatch"; +const COLOR_SWATCH_CLASS = "ruleview-colorswatch"; +const BEZIER_SWATCH_CLASS = "ruleview-bezierswatch"; +const FILTER_SWATCH_CLASS = "ruleview-filterswatch"; +const ANGLE_SWATCH_CLASS = "ruleview-angleswatch"; + +/* + * An actionable element is an element which on click triggers a specific action + * (e.g. shows a color tooltip, opens a link, …). + */ +const ACTIONABLE_ELEMENTS_SELECTORS = [ + `.${COLOR_SWATCH_CLASS}`, + `.${BEZIER_SWATCH_CLASS}`, + `.${FILTER_SWATCH_CLASS}`, + `.${ANGLE_SWATCH_CLASS}`, + "a" +]; + +/** + * TextPropertyEditor is responsible for the following: + * Owns a TextProperty object. + * Manages changes to the TextProperty. + * Can be expanded to display computed properties. + * Can mark a property disabled or enabled. + * + * @param {RuleEditor} ruleEditor + * The rule editor that owns this TextPropertyEditor. + * @param {TextProperty} property + * The text property to edit. + */ +function TextPropertyEditor(ruleEditor, property) { + this.ruleEditor = ruleEditor; + this.ruleView = this.ruleEditor.ruleView; + this.doc = this.ruleEditor.doc; + this.popup = this.ruleView.popup; + this.prop = property; + this.prop.editor = this; + this.browserWindow = this.doc.defaultView.top; + this._populatedComputed = false; + this._hasPendingClick = false; + this._clickedElementOptions = null; + + const toolbox = this.ruleView.inspector.toolbox; + this.cssProperties = getCssProperties(toolbox); + + this._onEnableClicked = this._onEnableClicked.bind(this); + this._onExpandClicked = this._onExpandClicked.bind(this); + this._onStartEditing = this._onStartEditing.bind(this); + this._onNameDone = this._onNameDone.bind(this); + this._onValueDone = this._onValueDone.bind(this); + this._onSwatchCommit = this._onSwatchCommit.bind(this); + this._onSwatchPreview = this._onSwatchPreview.bind(this); + this._onSwatchRevert = this._onSwatchRevert.bind(this); + this._onValidate = this.ruleView.throttle(this._previewValue, 10, this); + this.update = this.update.bind(this); + this.updatePropertyState = this.updatePropertyState.bind(this); + + this._create(); + this.update(); +} + +TextPropertyEditor.prototype = { + /** + * Boolean indicating if the name or value is being currently edited. + */ + get editing() { + return !!(this.nameSpan.inplaceEditor || this.valueSpan.inplaceEditor || + this.ruleView.tooltips.isEditing) || this.popup.isOpen; + }, + + /** + * Get the rule to the current text property + */ + get rule() { + return this.prop.rule; + }, + + /** + * Create the property editor's DOM. + */ + _create: function () { + this.element = this.doc.createElementNS(HTML_NS, "li"); + this.element.classList.add("ruleview-property"); + this.element._textPropertyEditor = this; + + this.container = createChild(this.element, "div", { + class: "ruleview-propertycontainer" + }); + + // The enable checkbox will disable or enable the rule. + this.enable = createChild(this.container, "div", { + class: "ruleview-enableproperty theme-checkbox", + tabindex: "-1" + }); + + // Click to expand the computed properties of the text property. + this.expander = createChild(this.container, "span", { + class: "ruleview-expander theme-twisty" + }); + this.expander.addEventListener("click", this._onExpandClicked, true); + + this.nameContainer = createChild(this.container, "span", { + class: "ruleview-namecontainer" + }); + + // Property name, editable when focused. Property name + // is committed when the editor is unfocused. + this.nameSpan = createChild(this.nameContainer, "span", { + class: "ruleview-propertyname theme-fg-color5", + tabindex: this.ruleEditor.isEditable ? "0" : "-1", + }); + + appendText(this.nameContainer, ": "); + + // Create a span that will hold the property and semicolon. + // Use this span to create a slightly larger click target + // for the value. + this.valueContainer = createChild(this.container, "span", { + class: "ruleview-propertyvaluecontainer" + }); + + // Property value, editable when focused. Changes to the + // property value are applied as they are typed, and reverted + // if the user presses escape. + this.valueSpan = createChild(this.valueContainer, "span", { + class: "ruleview-propertyvalue theme-fg-color1", + tabindex: this.ruleEditor.isEditable ? "0" : "-1", + }); + + // Storing the TextProperty on the elements for easy access + // (for instance by the tooltip) + this.valueSpan.textProperty = this.prop; + this.nameSpan.textProperty = this.prop; + + // If the value is a color property we need to put it through the parser + // so that colors can be coerced into the default color type. This prevents + // us from thinking that when colors are coerced they have been changed by + // the user. + let outputParser = this.ruleView._outputParser; + let frag = outputParser.parseCssProperty(this.prop.name, this.prop.value); + let parsedValue = frag.textContent; + + // Save the initial value as the last committed value, + // for restoring after pressing escape. + this.committed = { name: this.prop.name, + value: parsedValue, + priority: this.prop.priority }; + + appendText(this.valueContainer, ";"); + + this.warning = createChild(this.container, "div", { + class: "ruleview-warning", + hidden: "", + title: l10n("rule.warning.title"), + }); + + // Filter button that filters for the current property name and is + // displayed when the property is overridden by another rule. + this.filterProperty = createChild(this.container, "div", { + class: "ruleview-overridden-rule-filter", + hidden: "", + title: l10n("rule.filterProperty.title"), + }); + + this.filterProperty.addEventListener("click", event => { + this.ruleEditor.ruleView.setFilterStyles("`" + this.prop.name + "`"); + event.stopPropagation(); + }, false); + + // Holds the viewers for the computed properties. + // will be populated in |_updateComputed|. + this.computed = createChild(this.element, "ul", { + class: "ruleview-computedlist", + }); + + // Only bind event handlers if the rule is editable. + if (this.ruleEditor.isEditable) { + this.enable.addEventListener("click", this._onEnableClicked, true); + + this.nameContainer.addEventListener("click", (event) => { + // Clicks within the name shouldn't propagate any further. + event.stopPropagation(); + + // Forward clicks on nameContainer to the editable nameSpan + if (event.target === this.nameContainer) { + this.nameSpan.click(); + } + }, false); + + editableField({ + start: this._onStartEditing, + element: this.nameSpan, + done: this._onNameDone, + destroy: this.updatePropertyState, + advanceChars: ":", + contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY, + popup: this.popup, + cssProperties: this.cssProperties, + contextMenu: this.ruleView.inspector.onTextBoxContextMenu + }); + + // Auto blur name field on multiple CSS rules get pasted in. + this.nameContainer.addEventListener("paste", + blurOnMultipleProperties(this.cssProperties), false); + + this.valueContainer.addEventListener("click", (event) => { + // Clicks within the value shouldn't propagate any further. + event.stopPropagation(); + + // Forward clicks on valueContainer to the editable valueSpan + if (event.target === this.valueContainer) { + this.valueSpan.click(); + } + }, false); + + // The mousedown event could trigger a blur event on nameContainer, which + // will trigger a call to the update function. The update function clears + // valueSpan's markup. Thus the regular click event does not bubble up, and + // listener's callbacks are not called. + // So we need to remember where the user clicks in order to re-trigger the click + // after the valueSpan's markup is re-populated. We only need to track this for + // valueSpan's child elements, because direct click on valueSpan will always + // trigger a click event. + this.valueSpan.addEventListener("mousedown", (event) => { + let clickedEl = event.target; + if (clickedEl === this.valueSpan) { + return; + } + this._hasPendingClick = true; + + let matchedSelector = ACTIONABLE_ELEMENTS_SELECTORS.find( + (selector) => clickedEl.matches(selector)); + if (matchedSelector) { + let similarElements = [...this.valueSpan.querySelectorAll(matchedSelector)]; + this._clickedElementOptions = { + selector: matchedSelector, + index: similarElements.indexOf(clickedEl) + }; + } + }, false); + + this.valueSpan.addEventListener("mouseup", (event) => { + this._clickedElementOptions = null; + this._hasPendingClick = false; + }, false); + + this.valueSpan.addEventListener("click", (event) => { + let target = event.target; + + if (target.nodeName === "a") { + event.stopPropagation(); + event.preventDefault(); + this.browserWindow.openUILinkIn(target.href, "tab"); + } + }, false); + + editableField({ + start: this._onStartEditing, + element: this.valueSpan, + done: this._onValueDone, + destroy: this.update, + validate: this._onValidate, + advanceChars: advanceValidate, + contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE, + property: this.prop, + popup: this.popup, + multiline: true, + maxWidth: () => this.container.getBoundingClientRect().width, + cssProperties: this.cssProperties, + contextMenu: this.ruleView.inspector.onTextBoxContextMenu + }); + } + }, + + /** + * Get the path from which to resolve requests for this + * rule's stylesheet. + * + * @return {String} the stylesheet's href. + */ + get sheetHref() { + let domRule = this.rule.domRule; + if (domRule) { + return domRule.href || domRule.nodeHref; + } + return undefined; + }, + + /** + * Populate the span based on changes to the TextProperty. + */ + update: function () { + if (this.ruleView.isDestroyed) { + return; + } + + this.updatePropertyState(); + + let name = this.prop.name; + this.nameSpan.textContent = name; + + // Combine the property's value and priority into one string for + // the value. + let store = this.rule.elementStyle.store; + let val = store.userProperties.getProperty(this.rule.style, name, + this.prop.value); + if (this.prop.priority) { + val += " !" + this.prop.priority; + } + + let propDirty = store.userProperties.contains(this.rule.style, name); + + if (propDirty) { + this.element.setAttribute("dirty", ""); + } else { + this.element.removeAttribute("dirty"); + } + + let outputParser = this.ruleView._outputParser; + let parserOptions = { + angleClass: "ruleview-angle", + angleSwatchClass: SHARED_SWATCH_CLASS + " " + ANGLE_SWATCH_CLASS, + bezierClass: "ruleview-bezier", + bezierSwatchClass: SHARED_SWATCH_CLASS + " " + BEZIER_SWATCH_CLASS, + colorClass: "ruleview-color", + colorSwatchClass: SHARED_SWATCH_CLASS + " " + COLOR_SWATCH_CLASS, + filterClass: "ruleview-filter", + filterSwatchClass: SHARED_SWATCH_CLASS + " " + FILTER_SWATCH_CLASS, + gridClass: "ruleview-grid", + defaultColorType: !propDirty, + urlClass: "theme-link", + baseURI: this.sheetHref + }; + let frag = outputParser.parseCssProperty(name, val, parserOptions); + this.valueSpan.innerHTML = ""; + this.valueSpan.appendChild(frag); + + this.ruleView.emit("property-value-updated", this.valueSpan); + + // Attach the color picker tooltip to the color swatches + this._colorSwatchSpans = + this.valueSpan.querySelectorAll("." + COLOR_SWATCH_CLASS); + if (this.ruleEditor.isEditable) { + for (let span of this._colorSwatchSpans) { + // Adding this swatch to the list of swatches our colorpicker + // knows about + this.ruleView.tooltips.colorPicker.addSwatch(span, { + onShow: this._onStartEditing, + onPreview: this._onSwatchPreview, + onCommit: this._onSwatchCommit, + onRevert: this._onSwatchRevert + }); + span.on("unit-change", this._onSwatchCommit); + let title = l10n("rule.colorSwatch.tooltip"); + span.setAttribute("title", title); + } + } + + // Attach the cubic-bezier tooltip to the bezier swatches + this._bezierSwatchSpans = + this.valueSpan.querySelectorAll("." + BEZIER_SWATCH_CLASS); + if (this.ruleEditor.isEditable) { + for (let span of this._bezierSwatchSpans) { + // Adding this swatch to the list of swatches our colorpicker + // knows about + this.ruleView.tooltips.cubicBezier.addSwatch(span, { + onShow: this._onStartEditing, + onPreview: this._onSwatchPreview, + onCommit: this._onSwatchCommit, + onRevert: this._onSwatchRevert + }); + let title = l10n("rule.bezierSwatch.tooltip"); + span.setAttribute("title", title); + } + } + + // Attach the filter editor tooltip to the filter swatch + let span = this.valueSpan.querySelector("." + FILTER_SWATCH_CLASS); + if (this.ruleEditor.isEditable) { + if (span) { + parserOptions.filterSwatch = true; + + this.ruleView.tooltips.filterEditor.addSwatch(span, { + onShow: this._onStartEditing, + onPreview: this._onSwatchPreview, + onCommit: this._onSwatchCommit, + onRevert: this._onSwatchRevert + }, outputParser, parserOptions); + let title = l10n("rule.filterSwatch.tooltip"); + span.setAttribute("title", title); + } + } + + this.angleSwatchSpans = + this.valueSpan.querySelectorAll("." + ANGLE_SWATCH_CLASS); + if (this.ruleEditor.isEditable) { + for (let angleSpan of this.angleSwatchSpans) { + angleSpan.on("unit-change", this._onSwatchCommit); + let title = l10n("rule.angleSwatch.tooltip"); + angleSpan.setAttribute("title", title); + } + } + + let gridToggle = this.valueSpan.querySelector(".ruleview-grid"); + if (gridToggle) { + gridToggle.setAttribute("title", l10n("rule.gridToggle.tooltip")); + if (this.ruleView.highlighters.gridHighlighterShown === + this.ruleView.inspector.selection.nodeFront) { + gridToggle.classList.add("active"); + } + } + + // Now that we have updated the property's value, we might have a pending + // click on the value container. If we do, we have to trigger a click event + // on the right element. + if (this._hasPendingClick) { + this._hasPendingClick = false; + let elToClick; + + if (this._clickedElementOptions !== null) { + let {selector, index} = this._clickedElementOptions; + elToClick = this.valueSpan.querySelectorAll(selector)[index]; + + this._clickedElementOptions = null; + } + + if (!elToClick) { + elToClick = this.valueSpan; + } + elToClick.click(); + } + + // Populate the computed styles. + this._updateComputed(); + + // Update the rule property highlight. + this.ruleView._updatePropertyHighlight(this); + }, + + _onStartEditing: function () { + this.element.classList.remove("ruleview-overridden"); + this.filterProperty.hidden = true; + this.enable.style.visibility = "hidden"; + }, + + /** + * Update the visibility of the enable checkbox, the warning indicator and + * the filter property, as well as the overriden state of the property. + */ + updatePropertyState: function () { + if (this.prop.enabled) { + this.enable.style.removeProperty("visibility"); + this.enable.setAttribute("checked", ""); + } else { + this.enable.style.visibility = "visible"; + this.enable.removeAttribute("checked"); + } + + this.warning.hidden = this.editing || this.isValid(); + this.filterProperty.hidden = this.editing || + !this.isValid() || + !this.prop.overridden || + this.ruleEditor.rule.isUnmatched; + + if (!this.editing && + (this.prop.overridden || !this.prop.enabled || + !this.prop.isKnownProperty())) { + this.element.classList.add("ruleview-overridden"); + } else { + this.element.classList.remove("ruleview-overridden"); + } + }, + + /** + * Update the indicator for computed styles. The computed styles themselves + * are populated on demand, when they become visible. + */ + _updateComputed: function () { + this.computed.innerHTML = ""; + + let showExpander = this.prop.computed.some(c => c.name !== this.prop.name); + this.expander.style.visibility = showExpander ? "visible" : "hidden"; + + this._populatedComputed = false; + if (this.expander.hasAttribute("open")) { + this._populateComputed(); + } + }, + + /** + * Populate the list of computed styles. + */ + _populateComputed: function () { + if (this._populatedComputed) { + return; + } + this._populatedComputed = true; + + for (let computed of this.prop.computed) { + // Don't bother to duplicate information already + // shown in the text property. + if (computed.name === this.prop.name) { + continue; + } + + let li = createChild(this.computed, "li", { + class: "ruleview-computed" + }); + + if (computed.overridden) { + li.classList.add("ruleview-overridden"); + } + + createChild(li, "span", { + class: "ruleview-propertyname theme-fg-color5", + textContent: computed.name + }); + appendText(li, ": "); + + let outputParser = this.ruleView._outputParser; + let frag = outputParser.parseCssProperty( + computed.name, computed.value, { + colorSwatchClass: "ruleview-swatch ruleview-colorswatch", + urlClass: "theme-link", + baseURI: this.sheetHref + } + ); + + // Store the computed property value that was parsed for output + computed.parsedValue = frag.textContent; + + createChild(li, "span", { + class: "ruleview-propertyvalue theme-fg-color1", + child: frag + }); + + appendText(li, ";"); + + // Store the computed style element for easy access when highlighting + // styles + computed.element = li; + } + }, + + /** + * Handles clicks on the disabled property. + */ + _onEnableClicked: function (event) { + let checked = this.enable.hasAttribute("checked"); + if (checked) { + this.enable.removeAttribute("checked"); + } else { + this.enable.setAttribute("checked", ""); + } + this.prop.setEnabled(!checked); + event.stopPropagation(); + }, + + /** + * Handles clicks on the computed property expander. If the computed list is + * open due to user expanding or style filtering, collapse the computed list + * and close the expander. Otherwise, add user-open attribute which is used to + * expand the computed list and tracks whether or not the computed list is + * expanded by manually by the user. + */ + _onExpandClicked: function (event) { + if (this.computed.hasAttribute("filter-open") || + this.computed.hasAttribute("user-open")) { + this.expander.removeAttribute("open"); + this.computed.removeAttribute("filter-open"); + this.computed.removeAttribute("user-open"); + } else { + this.expander.setAttribute("open", "true"); + this.computed.setAttribute("user-open", ""); + this._populateComputed(); + } + + event.stopPropagation(); + }, + + /** + * Expands the computed list when a computed property is matched by the style + * filtering. The filter-open attribute is used to track whether or not the + * computed list was toggled opened by the filter. + */ + expandForFilter: function () { + if (!this.computed.hasAttribute("user-open")) { + this.expander.setAttribute("open", "true"); + this.computed.setAttribute("filter-open", ""); + this._populateComputed(); + } + }, + + /** + * Collapses the computed list that was expanded by style filtering. + */ + collapseForFilter: function () { + this.computed.removeAttribute("filter-open"); + + if (!this.computed.hasAttribute("user-open")) { + this.expander.removeAttribute("open"); + } + }, + + /** + * Called when the property name's inplace editor is closed. + * Ignores the change if the user pressed escape, otherwise + * commits it. + * + * @param {String} value + * The value contained in the editor. + * @param {Boolean} commit + * True if the change should be applied. + * @param {Number} direction + * The move focus direction number. + */ + _onNameDone: function (value, commit, direction) { + let isNameUnchanged = (!commit && !this.ruleEditor.isEditing) || + this.committed.name === value; + if (this.prop.value && isNameUnchanged) { + return; + } + + // Remove a property if the name is empty + if (!value.trim()) { + this.remove(direction); + return; + } + + // Remove a property if the property value is empty and the property + // value is not about to be focused + if (!this.prop.value && + direction !== Services.focus.MOVEFOCUS_FORWARD) { + this.remove(direction); + return; + } + + // Adding multiple rules inside of name field overwrites the current + // property with the first, then adds any more onto the property list. + let properties = parseDeclarations(this.cssProperties.isKnown, value); + + if (properties.length) { + this.prop.setName(properties[0].name); + this.committed.name = this.prop.name; + + if (!this.prop.enabled) { + this.prop.setEnabled(true); + } + + if (properties.length > 1) { + this.prop.setValue(properties[0].value, properties[0].priority); + this.ruleEditor.addProperties(properties.slice(1), this.prop); + } + } + }, + + /** + * Remove property from style and the editors from DOM. + * Begin editing next or previous available property given the focus + * direction. + * + * @param {Number} direction + * The move focus direction number. + */ + remove: function (direction) { + if (this._colorSwatchSpans && this._colorSwatchSpans.length) { + for (let span of this._colorSwatchSpans) { + this.ruleView.tooltips.colorPicker.removeSwatch(span); + span.off("unit-change", this._onSwatchCommit); + } + } + + if (this.angleSwatchSpans && this.angleSwatchSpans.length) { + for (let span of this.angleSwatchSpans) { + span.off("unit-change", this._onSwatchCommit); + } + } + + this.element.parentNode.removeChild(this.element); + this.ruleEditor.rule.editClosestTextProperty(this.prop, direction); + this.nameSpan.textProperty = null; + this.valueSpan.textProperty = null; + this.prop.remove(); + }, + + /** + * Called when a value editor closes. If the user pressed escape, + * revert to the value this property had before editing. + * + * @param {String} value + * The value contained in the editor. + * @param {Boolean} commit + * True if the change should be applied. + * @param {Number} direction + * The move focus direction number. + */ + _onValueDone: function (value = "", commit, direction) { + let parsedProperties = this._getValueAndExtraProperties(value); + let val = parseSingleValue(this.cssProperties.isKnown, + 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 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; + } + + if (this.isDisplayGrid()) { + this.ruleView.highlighters._hideGridHighlighter(); + } + + // First, set this property value (common case, only modified a property) + this.prop.setValue(val.value, val.priority); + + if (!this.prop.enabled) { + this.prop.setEnabled(true); + } + + this.committed.value = this.prop.value; + this.committed.priority = this.prop.priority; + + // If needed, add any new properties after this.prop. + this.ruleEditor.addProperties(parsedProperties.propertiesToAdd, this.prop); + + // If the input value is empty and the focus is moving forward to the next + // editable field, then remove the whole property. + // A timeout is used here to accurately check the state, since the inplace + // editor `done` and `destroy` events fire before the next editor + // is focused. + if (!value.trim() && direction !== Services.focus.MOVEFOCUS_BACKWARD) { + setTimeout(() => { + if (!this.editing) { + this.remove(direction); + } + }, 0); + } + }, + + /** + * Called when the swatch editor wants to commit a value change. + */ + _onSwatchCommit: function () { + this._onValueDone(this.valueSpan.textContent, true); + this.update(); + }, + + /** + * Called when the swatch editor wants to preview a value change. + */ + _onSwatchPreview: function () { + 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._previewValue(this.prop.value, true); + 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 + * { firstValue: "red", propertiesToAdd: [{ name: "width", value: "100px" }] } + * + * @param {String} value + * The string to parse + * @return {Object} An object with the following properties: + * firstValue: A string containing a simple value, like + * "red" or "100px!important" + * propertiesToAdd: An array with additional properties, following the + * parseDeclarations format of {name,value,priority} + */ + _getValueAndExtraProperties: function (value) { + // The inplace editor will prevent manual typing of multiple properties, + // but we need to deal with the case during a paste event. + // Adding multiple properties inside of value editor sets value with the + // first, then adds any more onto the property list (below this property). + let firstValue = value; + let propertiesToAdd = []; + + let properties = parseDeclarations(this.cssProperties.isKnown, value); + + // Check to see if the input string can be parsed as multiple properties + if (properties.length) { + // Get the first property value (if any), and any remaining + // properties (if any) + if (!properties[0].name && properties[0].value) { + firstValue = properties[0].value; + propertiesToAdd = properties.slice(1); + } else if (properties[0].name && properties[0].value) { + // In some cases, the value could be a property:value pair + // itself. Join them as one value string and append + // potentially following properties + firstValue = properties[0].name + ": " + properties[0].value; + propertiesToAdd = properties.slice(1); + } + } + + return { + propertiesToAdd: propertiesToAdd, + firstValue: firstValue + }; + }, + + /** + * Live preview this property, without committing changes. + * + * @param {String} value + * The value to set the current property to. + * @param {Boolean} reverting + * True if we're reverting the previously previewed value + */ + _previewValue: function (value, reverting = false) { + // Since function call is throttled, we need to make sure we are still + // editing, and any selector modifications have been completed + if (!reverting && (!this.editing || this.ruleEditor.isEditing)) { + return; + } + + let val = parseSingleValue(this.cssProperties.isKnown, value); + this.ruleEditor.rule.previewPropertyValue(this.prop, val.value, + val.priority); + }, + + /** + * 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 this.prop.isValid(); + }, + + /** + * Returns true if the property is a `display: grid` declaration. + * + * @return {Boolean} true if the property is a `display: grid` declaration. + */ + isDisplayGrid: function () { + return this.prop.name === "display" && this.prop.value === "grid"; + } +}; + +exports.TextPropertyEditor = TextPropertyEditor; diff --git a/devtools/client/inspector/shared/dom-node-preview.js b/devtools/client/inspector/shared/dom-node-preview.js new file mode 100644 index 000000000..d96384785 --- /dev/null +++ b/devtools/client/inspector/shared/dom-node-preview.js @@ -0,0 +1,352 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const {Task} = require("devtools/shared/task"); +const EventEmitter = require("devtools/shared/event-emitter"); +const {createNode} = require("devtools/client/animationinspector/utils"); +const { LocalizationHelper } = require("devtools/shared/l10n"); + +const STRINGS_URI = "devtools/client/locales/inspector.properties"; +const L10N = new LocalizationHelper(STRINGS_URI); + +/** + * UI component responsible for displaying a preview of a dom node. + * @param {InspectorPanel} inspector Requires a reference to the inspector-panel + * to highlight and select the node, as well as refresh it when there are + * mutations. + * @param {Object} options Supported properties are: + * - compact {Boolean} Defaults to false. + * By default, nodes are previewed like <tag id="id" class="class"> + * If true, nodes will be previewed like tag#id.class instead. + */ +function DomNodePreview(inspector, options = {}) { + this.inspector = inspector; + this.options = options; + + this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this); + this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this); + this.onSelectElClick = this.onSelectElClick.bind(this); + this.onMarkupMutations = this.onMarkupMutations.bind(this); + this.onHighlightElClick = this.onHighlightElClick.bind(this); + this.onHighlighterLocked = this.onHighlighterLocked.bind(this); + + EventEmitter.decorate(this); +} + +exports.DomNodePreview = DomNodePreview; + +DomNodePreview.prototype = { + init: function (containerEl) { + let document = containerEl.ownerDocument; + + // Init the markup for displaying the target node. + this.el = createNode({ + parent: containerEl, + attributes: { + "class": "animation-target" + } + }); + + // Icon to select the node in the inspector. + this.highlightNodeEl = createNode({ + parent: this.el, + nodeType: "span", + attributes: { + "class": "node-highlighter", + "title": L10N.getStr("inspector.nodePreview.highlightNodeLabel") + } + }); + + // Wrapper used for mouseover/out event handling. + this.previewEl = createNode({ + parent: this.el, + nodeType: "span", + attributes: { + "title": L10N.getStr("inspector.nodePreview.selectNodeLabel") + } + }); + + if (!this.options.compact) { + this.previewEl.appendChild(document.createTextNode("<")); + } + + // Only used for ::before and ::after pseudo-elements. + this.pseudoEl = createNode({ + parent: this.previewEl, + nodeType: "span", + attributes: { + "class": "pseudo-element theme-fg-color5" + } + }); + + // Tag name. + this.tagNameEl = createNode({ + parent: this.previewEl, + nodeType: "span", + attributes: { + "class": "tag-name theme-fg-color3" + } + }); + + // Id attribute container. + this.idEl = createNode({ + parent: this.previewEl, + nodeType: "span" + }); + + if (!this.options.compact) { + createNode({ + parent: this.idEl, + nodeType: "span", + attributes: { + "class": "attribute-name theme-fg-color2" + }, + textContent: "id" + }); + this.idEl.appendChild(document.createTextNode("=\"")); + } else { + createNode({ + parent: this.idEl, + nodeType: "span", + attributes: { + "class": "theme-fg-color6" + }, + textContent: "#" + }); + } + + createNode({ + parent: this.idEl, + nodeType: "span", + attributes: { + "class": "attribute-value theme-fg-color6" + } + }); + + if (!this.options.compact) { + this.idEl.appendChild(document.createTextNode("\"")); + } + + // Class attribute container. + this.classEl = createNode({ + parent: this.previewEl, + nodeType: "span" + }); + + if (!this.options.compact) { + createNode({ + parent: this.classEl, + nodeType: "span", + attributes: { + "class": "attribute-name theme-fg-color2" + }, + textContent: "class" + }); + this.classEl.appendChild(document.createTextNode("=\"")); + } else { + createNode({ + parent: this.classEl, + nodeType: "span", + attributes: { + "class": "theme-fg-color6" + }, + textContent: "." + }); + } + + createNode({ + parent: this.classEl, + nodeType: "span", + attributes: { + "class": "attribute-value theme-fg-color6" + } + }); + + if (!this.options.compact) { + this.classEl.appendChild(document.createTextNode("\"")); + this.previewEl.appendChild(document.createTextNode(">")); + } + + this.startListeners(); + }, + + startListeners: function () { + // Init events for highlighting and selecting the node. + this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver); + this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut); + this.previewEl.addEventListener("click", this.onSelectElClick); + this.highlightNodeEl.addEventListener("click", this.onHighlightElClick); + + // Start to listen for markupmutation events. + this.inspector.on("markupmutation", this.onMarkupMutations); + + // Listen to the target node highlighter. + HighlighterLock.on("highlighted", this.onHighlighterLocked); + }, + + stopListeners: function () { + HighlighterLock.off("highlighted", this.onHighlighterLocked); + this.inspector.off("markupmutation", this.onMarkupMutations); + this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver); + this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut); + this.previewEl.removeEventListener("click", this.onSelectElClick); + this.highlightNodeEl.removeEventListener("click", this.onHighlightElClick); + }, + + destroy: function () { + HighlighterLock.unhighlight().catch(e => console.error(e)); + + this.stopListeners(); + + this.el.remove(); + this.el = this.tagNameEl = this.idEl = this.classEl = this.pseudoEl = null; + this.highlightNodeEl = this.previewEl = null; + this.nodeFront = this.inspector = null; + }, + + get highlighterUtils() { + if (this.inspector && this.inspector.toolbox) { + return this.inspector.toolbox.highlighterUtils; + } + return null; + }, + + onPreviewMouseOver: function () { + if (!this.nodeFront || !this.highlighterUtils) { + return; + } + this.highlighterUtils.highlightNodeFront(this.nodeFront) + .catch(e => console.error(e)); + }, + + onPreviewMouseOut: function () { + if (!this.nodeFront || !this.highlighterUtils) { + return; + } + this.highlighterUtils.unhighlight() + .catch(e => console.error(e)); + }, + + onSelectElClick: function () { + if (!this.nodeFront) { + return; + } + this.inspector.selection.setNodeFront(this.nodeFront, "dom-node-preview"); + }, + + onHighlightElClick: function (e) { + e.stopPropagation(); + + let classList = this.highlightNodeEl.classList; + let isHighlighted = classList.contains("selected"); + + if (isHighlighted) { + classList.remove("selected"); + HighlighterLock.unhighlight().then(() => { + this.emit("target-highlighter-unlocked"); + }, error => console.error(error)); + } else { + classList.add("selected"); + HighlighterLock.highlight(this).then(() => { + this.emit("target-highlighter-locked"); + }, error => console.error(error)); + } + }, + + onHighlighterLocked: function (e, domNodePreview) { + if (domNodePreview !== this) { + this.highlightNodeEl.classList.remove("selected"); + } + }, + + onMarkupMutations: function (e, mutations) { + if (!this.nodeFront) { + return; + } + + for (let {target} of mutations) { + if (target === this.nodeFront) { + // Re-render with the same nodeFront to update the output. + this.render(this.nodeFront); + break; + } + } + }, + + render: function (nodeFront) { + this.nodeFront = nodeFront; + let {displayName, attributes} = nodeFront; + + if (nodeFront.isPseudoElement) { + this.pseudoEl.textContent = nodeFront.isBeforePseudoElement + ? "::before" + : "::after"; + this.pseudoEl.style.display = "inline"; + this.tagNameEl.style.display = "none"; + } else { + this.tagNameEl.textContent = displayName; + this.pseudoEl.style.display = "none"; + this.tagNameEl.style.display = "inline"; + } + + let idIndex = attributes.findIndex(({name}) => name === "id"); + if (idIndex > -1 && attributes[idIndex].value) { + this.idEl.querySelector(".attribute-value").textContent = + attributes[idIndex].value; + this.idEl.style.display = "inline"; + } else { + this.idEl.style.display = "none"; + } + + let classIndex = attributes.findIndex(({name}) => name === "class"); + if (classIndex > -1 && attributes[classIndex].value) { + let value = attributes[classIndex].value; + if (this.options.compact) { + value = value.split(" ").join("."); + } + + this.classEl.querySelector(".attribute-value").textContent = value; + this.classEl.style.display = "inline"; + } else { + this.classEl.style.display = "none"; + } + } +}; + +/** + * HighlighterLock is a helper used to lock the highlighter on DOM nodes in the + * page. + * It instantiates a new highlighter that is then shared amongst all instances + * of DomNodePreview. This is useful because that means showing the highlighter + * on one node will unhighlight the previously highlighted one, but will not + * interfere with the default inspector highlighter. + */ +var HighlighterLock = { + highlighter: null, + isShown: false, + + highlight: Task.async(function* (animationTargetNode) { + if (!this.highlighter) { + let util = animationTargetNode.inspector.toolbox.highlighterUtils; + this.highlighter = yield util.getHighlighterByType("BoxModelHighlighter"); + } + + yield this.highlighter.show(animationTargetNode.nodeFront); + this.isShown = true; + this.emit("highlighted", animationTargetNode); + }), + + unhighlight: Task.async(function* () { + if (!this.highlighter || !this.isShown) { + return; + } + + yield this.highlighter.hide(); + this.isShown = false; + this.emit("unhighlighted"); + }) +}; + +EventEmitter.decorate(HighlighterLock); diff --git a/devtools/client/inspector/shared/highlighters-overlay.js b/devtools/client/inspector/shared/highlighters-overlay.js new file mode 100644 index 000000000..c054c72af --- /dev/null +++ b/devtools/client/inspector/shared/highlighters-overlay.js @@ -0,0 +1,315 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * The highlighter overlays are in-content highlighters that appear when hovering over + * property values. + */ + +const promise = require("promise"); +const EventEmitter = require("devtools/shared/event-emitter"); +const { VIEW_NODE_VALUE_TYPE } = require("devtools/client/inspector/shared/node-types"); + +/** + * Manages all highlighters in the style-inspector. + * + * @param {CssRuleView|CssComputedView} view + * Either the rule-view or computed-view panel + */ +function HighlightersOverlay(view) { + this.view = view; + + let {CssRuleView} = require("devtools/client/inspector/rules/rules"); + this.isRuleView = view instanceof CssRuleView; + + this.highlighters = {}; + + // NodeFront of the grid container that is highlighted. + this.gridHighlighterShown = null; + // Name of the highlighter shown on mouse hover. + this.hoveredHighlighterShown = null; + // Name of the selector highlighter shown. + this.selectorHighlighterShown = null; + + this.highlighterUtils = this.view.inspector.toolbox.highlighterUtils; + + // Only initialize the overlay if at least one of the highlighter types is + // supported. + this.supportsHighlighters = + this.highlighterUtils.supportsCustomHighlighters(); + + this._onClick = this._onClick.bind(this); + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseOut = this._onMouseOut.bind(this); + this._onWillNavigate = this._onWillNavigate.bind(this); + + EventEmitter.decorate(this); +} + +HighlightersOverlay.prototype = { + /** + * Add the highlighters overlay to the view. This will start tracking mouse + * movements and display highlighters when needed. + */ + addToView: function () { + if (!this.supportsHighlighters || this._isStarted || this._isDestroyed) { + return; + } + + let el = this.view.element; + el.addEventListener("click", this._onClick, true); + el.addEventListener("mousemove", this._onMouseMove, false); + el.addEventListener("mouseout", this._onMouseOut, false); + el.ownerDocument.defaultView.addEventListener("mouseout", this._onMouseOut, false); + + if (this.isRuleView) { + this.view.inspector.target.on("will-navigate", this._onWillNavigate); + } + + this._isStarted = true; + }, + + /** + * Remove the overlay from the current view. This will stop tracking mouse + * movement and showing highlighters. + */ + removeFromView: function () { + if (!this.supportsHighlighters || !this._isStarted || this._isDestroyed) { + return; + } + + let el = this.view.element; + el.removeEventListener("click", this._onClick, true); + el.removeEventListener("mousemove", this._onMouseMove, false); + el.removeEventListener("mouseout", this._onMouseOut, false); + + if (this.isRuleView) { + this.view.inspector.target.off("will-navigate", this._onWillNavigate); + } + + this._isStarted = false; + }, + + _onClick: function (event) { + // Bail out if the target is not a grid property value. + if (!this._isDisplayGridValue(event.target)) { + return; + } + + event.stopPropagation(); + + this._getHighlighter("CssGridHighlighter").then(highlighter => { + let node = this.view.inspector.selection.nodeFront; + + // Toggle off the grid highlighter if the grid highlighter toggle is clicked + // for the current highlighted grid. + if (node === this.gridHighlighterShown) { + return highlighter.hide(); + } + + return highlighter.show(node); + }).then(isGridShown => { + // Toggle all the grid icons in the current rule view. + for (let gridIcon of this.view.element.querySelectorAll(".ruleview-grid")) { + gridIcon.classList.toggle("active", isGridShown); + } + + if (isGridShown) { + this.gridHighlighterShown = this.view.inspector.selection.nodeFront; + this.emit("highlighter-shown"); + } else { + this.gridHighlighterShown = null; + this.emit("highlighter-hidden"); + } + }).catch(e => console.error(e)); + }, + + _onMouseMove: function (event) { + // Bail out if the target is the same as for the last mousemove. + if (event.target === this._lastHovered) { + return; + } + + // Only one highlighter can be displayed at a time, hide the currently shown. + this._hideHoveredHighlighter(); + + this._lastHovered = event.target; + + let nodeInfo = this.view.getNodeInfo(event.target); + if (!nodeInfo) { + return; + } + + // Choose the type of highlighter required for the hovered node. + let type; + if (this._isRuleViewTransform(nodeInfo) || + this._isComputedViewTransform(nodeInfo)) { + type = "CssTransformHighlighter"; + } + + if (type) { + this.hoveredHighlighterShown = type; + let node = this.view.inspector.selection.nodeFront; + this._getHighlighter(type) + .then(highlighter => highlighter.show(node)) + .then(shown => { + if (shown) { + this.emit("highlighter-shown"); + } + }); + } + }, + + _onMouseOut: function (event) { + // Only hide the highlighter if the mouse leaves the currently hovered node. + if (!this._lastHovered || + (event && this._lastHovered.contains(event.relatedTarget))) { + return; + } + + // Otherwise, hide the highlighter. + this._lastHovered = null; + this._hideHoveredHighlighter(); + }, + + /** + * Clear saved highlighter shown properties on will-navigate. + */ + _onWillNavigate: function () { + this.gridHighlighterShown = null; + this.hoveredHighlighterShown = null; + this.selectorHighlighterShown = null; + }, + + /** + * Is the current hovered node a css transform property value in the rule-view. + * + * @param {Object} nodeInfo + * @return {Boolean} + */ + _isRuleViewTransform: function (nodeInfo) { + let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE && + nodeInfo.value.property === "transform"; + let isEnabled = nodeInfo.value.enabled && + !nodeInfo.value.overridden && + !nodeInfo.value.pseudoElement; + return this.isRuleView && isTransform && isEnabled; + }, + + /** + * Is the current hovered node a css transform property value in the + * computed-view. + * + * @param {Object} nodeInfo + * @return {Boolean} + */ + _isComputedViewTransform: function (nodeInfo) { + let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE && + nodeInfo.value.property === "transform"; + return !this.isRuleView && isTransform; + }, + + /** + * Is the current clicked node a grid display property value in the + * rule-view. + * + * @param {DOMNode} node + * @return {Boolean} + */ + _isDisplayGridValue: function (node) { + return this.isRuleView && node.classList.contains("ruleview-grid"); + }, + + /** + * Hide the currently shown grid highlighter. + */ + _hideGridHighlighter: function () { + if (!this.gridHighlighterShown || !this.highlighters.CssGridHighlighter) { + return; + } + + let onHidden = this.highlighters.CssGridHighlighter.hide(); + if (onHidden) { + onHidden.then(null, e => console.error(e)); + } + + this.gridHighlighterShown = null; + this.emit("highlighter-hidden"); + }, + + /** + * Hide the currently shown hovered highlighter. + */ + _hideHoveredHighlighter: function () { + if (!this.hoveredHighlighterShown || + !this.highlighters[this.hoveredHighlighterShown]) { + return; + } + + // For some reason, the call to highlighter.hide doesn't always return a + // promise. This causes some tests to fail when trying to install a + // rejection handler on the result of the call. To avoid this, check + // whether the result is truthy before installing the handler. + let onHidden = this.highlighters[this.hoveredHighlighterShown].hide(); + if (onHidden) { + onHidden.then(null, e => console.error(e)); + } + + this.hoveredHighlighterShown = null; + this.emit("highlighter-hidden"); + }, + + /** + * Get a highlighter front given a type. It will only be initialized once. + * + * @param {String} type + * The highlighter type. One of this.highlighters. + * @return {Promise} that resolves to the highlighter + */ + _getHighlighter: function (type) { + let utils = this.highlighterUtils; + + if (this.highlighters[type]) { + return promise.resolve(this.highlighters[type]); + } + + return utils.getHighlighterByType(type).then(highlighter => { + this.highlighters[type] = highlighter; + return highlighter; + }); + }, + + /** + * Destroy this overlay instance, removing it from the view and destroying + * all initialized highlighters. + */ + destroy: function () { + this.removeFromView(); + + for (let type in this.highlighters) { + if (this.highlighters[type]) { + this.highlighters[type].finalize(); + this.highlighters[type] = null; + } + } + + this.highlighters = null; + + this.gridHighlighterShown = null; + this.hoveredHighlighterShown = null; + this.selectorHighlighterShown = null; + + this.highlighterUtils = null; + this.isRuleView = null; + this.view = null; + + this._isDestroyed = true; + } +}; + +module.exports = HighlightersOverlay; diff --git a/devtools/client/inspector/shared/moz.build b/devtools/client/inspector/shared/moz.build new file mode 100644 index 000000000..fd2239b60 --- /dev/null +++ b/devtools/client/inspector/shared/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DevToolsModules( + 'dom-node-preview.js', + 'highlighters-overlay.js', + 'node-types.js', + 'style-inspector-menu.js', + 'tooltips-overlay.js', + 'utils.js' +) + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/devtools/client/inspector/shared/node-types.js b/devtools/client/inspector/shared/node-types.js new file mode 100644 index 000000000..4f31ee9fe --- /dev/null +++ b/devtools/client/inspector/shared/node-types.js @@ -0,0 +1,17 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Types of nodes used in the rule and omputed view. + */ + +exports.VIEW_NODE_SELECTOR_TYPE = 1; +exports.VIEW_NODE_PROPERTY_TYPE = 2; +exports.VIEW_NODE_VALUE_TYPE = 3; +exports.VIEW_NODE_IMAGE_URL_TYPE = 4; +exports.VIEW_NODE_LOCATION_TYPE = 5; diff --git a/devtools/client/inspector/shared/style-inspector-menu.js b/devtools/client/inspector/shared/style-inspector-menu.js new file mode 100644 index 000000000..975074609 --- /dev/null +++ b/devtools/client/inspector/shared/style-inspector-menu.js @@ -0,0 +1,510 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils"); +const Services = require("Services"); +const {Task} = require("devtools/shared/task"); + +const Menu = require("devtools/client/framework/menu"); +const MenuItem = require("devtools/client/framework/menu-item"); + +const { + VIEW_NODE_SELECTOR_TYPE, + VIEW_NODE_PROPERTY_TYPE, + VIEW_NODE_VALUE_TYPE, + VIEW_NODE_IMAGE_URL_TYPE, + VIEW_NODE_LOCATION_TYPE, +} = require("devtools/client/inspector/shared/node-types"); +const clipboardHelper = require("devtools/shared/platform/clipboard"); + +const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties"; +const {LocalizationHelper} = require("devtools/shared/l10n"); +const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES); + +const PREF_ENABLE_MDN_DOCS_TOOLTIP = + "devtools.inspector.mdnDocsTooltip.enabled"; + +/** + * Style inspector context menu + * + * @param {RuleView|ComputedView} view + * RuleView or ComputedView instance controlling this menu + * @param {Object} options + * Option menu configuration + */ +function StyleInspectorMenu(view, options) { + this.view = view; + this.inspector = this.view.inspector; + this.styleDocument = this.view.styleDocument; + this.styleWindow = this.view.styleWindow; + + this.isRuleView = options.isRuleView; + + this._onAddNewRule = this._onAddNewRule.bind(this); + this._onCopy = this._onCopy.bind(this); + this._onCopyColor = this._onCopyColor.bind(this); + this._onCopyImageDataUrl = this._onCopyImageDataUrl.bind(this); + this._onCopyLocation = this._onCopyLocation.bind(this); + this._onCopyPropertyDeclaration = this._onCopyPropertyDeclaration.bind(this); + this._onCopyPropertyName = this._onCopyPropertyName.bind(this); + this._onCopyPropertyValue = this._onCopyPropertyValue.bind(this); + this._onCopyRule = this._onCopyRule.bind(this); + this._onCopySelector = this._onCopySelector.bind(this); + this._onCopyUrl = this._onCopyUrl.bind(this); + this._onSelectAll = this._onSelectAll.bind(this); + this._onShowMdnDocs = this._onShowMdnDocs.bind(this); + this._onToggleOrigSources = this._onToggleOrigSources.bind(this); +} + +module.exports = StyleInspectorMenu; + +StyleInspectorMenu.prototype = { + /** + * Display the style inspector context menu + */ + show: function (event) { + try { + this._openMenu({ + target: event.explicitOriginalTarget, + screenX: event.screenX, + screenY: event.screenY, + }); + } catch (e) { + console.error(e); + } + }, + + _openMenu: function ({ target, screenX = 0, screenY = 0 } = { }) { + // In the sidebar we do not have this.styleDocument.popupNode + // so we need to save the node ourselves. + this.styleDocument.popupNode = target; + this.styleWindow.focus(); + + let menu = new Menu(); + + let menuitemCopy = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy"), + accesskey: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy.accessKey"), + click: () => { + this._onCopy(); + }, + disabled: !this._hasTextSelected(), + }); + let menuitemCopyLocation = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyLocation"), + click: () => { + this._onCopyLocation(); + }, + visible: false, + }); + let menuitemCopyRule = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyRule"), + click: () => { + this._onCopyRule(); + }, + visible: this.isRuleView, + }); + let copyColorAccessKey = "styleinspector.contextmenu.copyColor.accessKey"; + let menuitemCopyColor = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyColor"), + accesskey: STYLE_INSPECTOR_L10N.getStr(copyColorAccessKey), + click: () => { + this._onCopyColor(); + }, + visible: this._isColorPopup(), + }); + let copyUrlAccessKey = "styleinspector.contextmenu.copyUrl.accessKey"; + let menuitemCopyUrl = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyUrl"), + accesskey: STYLE_INSPECTOR_L10N.getStr(copyUrlAccessKey), + click: () => { + this._onCopyUrl(); + }, + visible: this._isImageUrl(), + }); + let copyImageAccessKey = "styleinspector.contextmenu.copyImageDataUrl.accessKey"; + let menuitemCopyImageDataUrl = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyImageDataUrl"), + accesskey: STYLE_INSPECTOR_L10N.getStr(copyImageAccessKey), + click: () => { + this._onCopyImageDataUrl(); + }, + visible: this._isImageUrl(), + }); + let copyPropDeclarationLabel = "styleinspector.contextmenu.copyPropertyDeclaration"; + let menuitemCopyPropertyDeclaration = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr(copyPropDeclarationLabel), + click: () => { + this._onCopyPropertyDeclaration(); + }, + visible: false, + }); + let menuitemCopyPropertyName = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyName"), + click: () => { + this._onCopyPropertyName(); + }, + visible: false, + }); + let menuitemCopyPropertyValue = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyValue"), + click: () => { + this._onCopyPropertyValue(); + }, + visible: false, + }); + let menuitemCopySelector = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copySelector"), + click: () => { + this._onCopySelector(); + }, + visible: false, + }); + + this._clickedNodeInfo = this._getClickedNodeInfo(); + if (this.isRuleView && this._clickedNodeInfo) { + switch (this._clickedNodeInfo.type) { + case VIEW_NODE_PROPERTY_TYPE : + menuitemCopyPropertyDeclaration.visible = true; + menuitemCopyPropertyName.visible = true; + break; + case VIEW_NODE_VALUE_TYPE : + menuitemCopyPropertyDeclaration.visible = true; + menuitemCopyPropertyValue.visible = true; + break; + case VIEW_NODE_SELECTOR_TYPE : + menuitemCopySelector.visible = true; + break; + case VIEW_NODE_LOCATION_TYPE : + menuitemCopyLocation.visible = true; + break; + } + } + + menu.append(menuitemCopy); + menu.append(menuitemCopyLocation); + menu.append(menuitemCopyRule); + menu.append(menuitemCopyColor); + menu.append(menuitemCopyUrl); + menu.append(menuitemCopyImageDataUrl); + menu.append(menuitemCopyPropertyDeclaration); + menu.append(menuitemCopyPropertyName); + menu.append(menuitemCopyPropertyValue); + menu.append(menuitemCopySelector); + + menu.append(new MenuItem({ + type: "separator", + })); + + // Select All + let selectAllAccessKey = "styleinspector.contextmenu.selectAll.accessKey"; + let menuitemSelectAll = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.selectAll"), + accesskey: STYLE_INSPECTOR_L10N.getStr(selectAllAccessKey), + click: () => { + this._onSelectAll(); + }, + }); + menu.append(menuitemSelectAll); + + menu.append(new MenuItem({ + type: "separator", + })); + + // Add new rule + let addRuleAccessKey = "styleinspector.contextmenu.addNewRule.accessKey"; + let menuitemAddRule = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.addNewRule"), + accesskey: STYLE_INSPECTOR_L10N.getStr(addRuleAccessKey), + click: () => { + this._onAddNewRule(); + }, + visible: this.isRuleView, + disabled: !this.isRuleView || + this.inspector.selection.isAnonymousNode(), + }); + menu.append(menuitemAddRule); + + // Show MDN Docs + let mdnDocsAccessKey = "styleinspector.contextmenu.showMdnDocs.accessKey"; + let menuitemShowMdnDocs = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs"), + accesskey: STYLE_INSPECTOR_L10N.getStr(mdnDocsAccessKey), + click: () => { + this._onShowMdnDocs(); + }, + visible: (Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP) && + this._isPropertyName()), + }); + menu.append(menuitemShowMdnDocs); + + // Show Original Sources + let sourcesAccessKey = "styleinspector.contextmenu.toggleOrigSources.accessKey"; + let menuitemSources = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.toggleOrigSources"), + accesskey: STYLE_INSPECTOR_L10N.getStr(sourcesAccessKey), + click: () => { + this._onToggleOrigSources(); + }, + type: "checkbox", + checked: Services.prefs.getBoolPref(PREF_ORIG_SOURCES), + }); + menu.append(menuitemSources); + + menu.popup(screenX, screenY, this.inspector._toolbox); + return menu; + }, + + _hasTextSelected: function () { + let hasTextSelected; + let selection = this.styleWindow.getSelection(); + + let node = this._getClickedNode(); + if (node.nodeName == "input" || node.nodeName == "textarea") { + let { selectionStart, selectionEnd } = node; + hasTextSelected = isFinite(selectionStart) && isFinite(selectionEnd) + && selectionStart !== selectionEnd; + } else { + hasTextSelected = selection.toString() && !selection.isCollapsed; + } + + return hasTextSelected; + }, + + /** + * Get the type of the currently clicked node + */ + _getClickedNodeInfo: function () { + let node = this._getClickedNode(); + return this.view.getNodeInfo(node); + }, + + /** + * A helper that determines if the popup was opened with a click to a color + * value and saves the color to this._colorToCopy. + * + * @return {Boolean} + * true if click on color opened the popup, false otherwise. + */ + _isColorPopup: function () { + this._colorToCopy = ""; + + let container = this._getClickedNode(); + if (!container) { + return false; + } + + let isColorNode = el => el.dataset && "color" in el.dataset; + + while (!isColorNode(container)) { + container = container.parentNode; + if (!container) { + return false; + } + } + + this._colorToCopy = container.dataset.color; + return true; + }, + + _isPropertyName: function () { + let nodeInfo = this._getClickedNodeInfo(); + if (!nodeInfo) { + return false; + } + return nodeInfo.type == VIEW_NODE_PROPERTY_TYPE; + }, + + /** + * Check if the current node (clicked node) is an image URL + * + * @return {Boolean} true if the node is an image url + */ + _isImageUrl: function () { + let nodeInfo = this._getClickedNodeInfo(); + if (!nodeInfo) { + return false; + } + return nodeInfo.type == VIEW_NODE_IMAGE_URL_TYPE; + }, + + /** + * Get the DOM Node container for the current popupNode. + * If popupNode is a textNode, return the parent node, otherwise return + * popupNode itself. + * + * @return {DOMNode} + */ + _getClickedNode: function () { + let container = null; + let node = this.styleDocument.popupNode; + + if (node) { + let isTextNode = node.nodeType == node.TEXT_NODE; + container = isTextNode ? node.parentElement : node; + } + + return container; + }, + + /** + * Select all text. + */ + _onSelectAll: function () { + let selection = this.styleWindow.getSelection(); + selection.selectAllChildren(this.view.element); + }, + + /** + * Copy the most recently selected color value to clipboard. + */ + _onCopy: function () { + this.view.copySelection(this.styleDocument.popupNode); + }, + + /** + * Copy the most recently selected color value to clipboard. + */ + _onCopyColor: function () { + clipboardHelper.copyString(this._colorToCopy); + }, + + /* + * Retrieve the url for the selected image and copy it to the clipboard + */ + _onCopyUrl: function () { + if (!this._clickedNodeInfo) { + return; + } + + clipboardHelper.copyString(this._clickedNodeInfo.value.url); + }, + + /** + * Retrieve the image data for the selected image url and copy it to the + * clipboard + */ + _onCopyImageDataUrl: Task.async(function* () { + if (!this._clickedNodeInfo) { + return; + } + + let message; + try { + let inspectorFront = this.inspector.inspector; + let imageUrl = this._clickedNodeInfo.value.url; + let data = yield inspectorFront.getImageDataFromURL(imageUrl); + message = yield data.data.string(); + } catch (e) { + message = + STYLE_INSPECTOR_L10N.getStr("styleinspector.copyImageDataUrlError"); + } + + clipboardHelper.copyString(message); + }), + + /** + * Show docs from MDN for a CSS property. + */ + _onShowMdnDocs: function () { + let cssPropertyName = this.styleDocument.popupNode.textContent; + let anchor = this.styleDocument.popupNode.parentNode; + let cssDocsTooltip = this.view.tooltips.cssDocs; + cssDocsTooltip.show(anchor, cssPropertyName); + }, + + /** + * Add a new rule to the current element. + */ + _onAddNewRule: function () { + this.view._onAddRule(); + }, + + /** + * Copy the rule source location of the current clicked node. + */ + _onCopyLocation: function () { + if (!this._clickedNodeInfo) { + return; + } + + clipboardHelper.copyString(this._clickedNodeInfo.value); + }, + + /** + * Copy the rule property declaration of the current clicked node. + */ + _onCopyPropertyDeclaration: function () { + if (!this._clickedNodeInfo) { + return; + } + + let textProp = this._clickedNodeInfo.value.textProperty; + clipboardHelper.copyString(textProp.stringifyProperty()); + }, + + /** + * Copy the rule property name of the current clicked node. + */ + _onCopyPropertyName: function () { + if (!this._clickedNodeInfo) { + return; + } + + clipboardHelper.copyString(this._clickedNodeInfo.value.property); + }, + + /** + * Copy the rule property value of the current clicked node. + */ + _onCopyPropertyValue: function () { + if (!this._clickedNodeInfo) { + return; + } + + clipboardHelper.copyString(this._clickedNodeInfo.value.value); + }, + + /** + * Copy the rule of the current clicked node. + */ + _onCopyRule: function () { + let ruleEditor = + this.styleDocument.popupNode.parentNode.offsetParent._ruleEditor; + let rule = ruleEditor.rule; + clipboardHelper.copyString(rule.stringifyRule()); + }, + + /** + * Copy the rule selector of the current clicked node. + */ + _onCopySelector: function () { + if (!this._clickedNodeInfo) { + return; + } + + clipboardHelper.copyString(this._clickedNodeInfo.value); + }, + + /** + * Toggle the original sources pref. + */ + _onToggleOrigSources: function () { + let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); + Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled); + }, + + destroy: function () { + this.popupNode = null; + this.styleDocument.popupNode = null; + this.view = null; + this.inspector = null; + this.styleDocument = null; + this.styleWindow = null; + } +}; diff --git a/devtools/client/inspector/shared/test/.eslintrc.js b/devtools/client/inspector/shared/test/.eslintrc.js new file mode 100644 index 000000000..698ae9181 --- /dev/null +++ b/devtools/client/inspector/shared/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../../.eslintrc.mochitests.js" +}; diff --git a/devtools/client/inspector/shared/test/browser.ini b/devtools/client/inspector/shared/test/browser.ini new file mode 100644 index 000000000..ce85ee80e --- /dev/null +++ b/devtools/client/inspector/shared/test/browser.ini @@ -0,0 +1,41 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + doc_author-sheet.html + doc_content_stylesheet.html + doc_content_stylesheet.xul + doc_content_stylesheet_imported.css + doc_content_stylesheet_imported2.css + doc_content_stylesheet_linked.css + doc_content_stylesheet_script.css + doc_content_stylesheet_xul.css + doc_frame_script.js + head.js + !/devtools/client/commandline/test/helpers.js + !/devtools/client/framework/test/shared-head.js + !/devtools/client/inspector/test/head.js + !/devtools/client/inspector/test/shared-head.js + !/devtools/client/shared/test/test-actor.js + !/devtools/client/shared/test/test-actor-registry.js + +[browser_styleinspector_context-menu-copy-color_01.js] +[browser_styleinspector_context-menu-copy-color_02.js] +subsuite = clipboard +[browser_styleinspector_context-menu-copy-urls.js] +subsuite = clipboard +[browser_styleinspector_csslogic-content-stylesheets.js] +skip-if = e10s && debug # Bug 1250058 (docshell leak when opening 2 toolboxes) +[browser_styleinspector_output-parser.js] +[browser_styleinspector_refresh_when_active.js] +[browser_styleinspector_tooltip-background-image.js] +[browser_styleinspector_tooltip-closes-on-new-selection.js] +skip-if = e10s # Bug 1111546 (e10s) +[browser_styleinspector_tooltip-longhand-fontfamily.js] +[browser_styleinspector_tooltip-multiple-background-images.js] +[browser_styleinspector_tooltip-shorthand-fontfamily.js] +[browser_styleinspector_tooltip-size.js] +[browser_styleinspector_transform-highlighter-01.js] +[browser_styleinspector_transform-highlighter-02.js] +[browser_styleinspector_transform-highlighter-03.js] +[browser_styleinspector_transform-highlighter-04.js] diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js new file mode 100644 index 000000000..5a27edf16 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js @@ -0,0 +1,118 @@ +/* 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 "Copy color" item of the context menu #1: Test _isColorPopup. + +const TEST_URI = ` + <div style="color:rgb(18, 58, 188);margin:0px;background:span[data-color];"> + Test "Copy color" context menu option + </div> +`; + +add_task(function* () { + // Test is slow on Linux EC2 instances - Bug 1137765 + requestLongerTimeout(2); + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector} = yield openInspector(); + yield testView("ruleview", inspector); + yield testView("computedview", inspector); +}); + +function* testView(viewId, inspector) { + info("Testing " + viewId); + + yield inspector.sidebar.select(viewId); + let view = inspector[viewId].view || inspector[viewId].computedView; + yield selectNode("div", inspector); + + testIsColorValueNode(view); + testIsColorPopupOnAllNodes(view); + yield clearCurrentNodeSelection(inspector); +} + +/** + * A function testing that isColorValueNode correctly detects nodes part of + * color values. + */ +function testIsColorValueNode(view) { + info("Testing that child nodes of color nodes are detected."); + let root = rootElement(view); + let colorNode = root.querySelector("span[data-color]"); + + ok(colorNode, "Color node found"); + for (let node of iterateNodes(colorNode)) { + ok(isColorValueNode(node), "Node is part of color value."); + } +} + +/** + * A function testing that _isColorPopup returns a correct value for all nodes + * in the view. + */ +function testIsColorPopupOnAllNodes(view) { + let root = rootElement(view); + for (let node of iterateNodes(root)) { + testIsColorPopupOnNode(view, node); + } +} + +/** + * Test result of _isColorPopup with given node. + * @param object view + * A CSSRuleView or CssComputedView instance. + * @param Node node + * A node to check. + */ +function testIsColorPopupOnNode(view, node) { + 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) ? "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) { + let container = (node.nodeType == node.TEXT_NODE) ? + node.parentElement : node; + + let isColorNode = el => el.dataset && "color" in el.dataset; + + while (!isColorNode(container)) { + container = container.parentNode; + if (!container) { + info("No color. Node is not part of color value."); + return false; + } + } + + info("Found a color. Node is part of color value."); + + return true; +} + +/** + * A generator that iterates recursively trough all child nodes of baseNode. + */ +function* iterateNodes(baseNode) { + yield baseNode; + + for (let child of baseNode.childNodes) { + yield* iterateNodes(child); + } +} + +/** + * Returns the root element for the given view, rule or computed. + */ +var rootElement = view => (view.element) ? view.element : view.styleDocument; diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js new file mode 100644 index 000000000..afae7a2b6 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js @@ -0,0 +1,99 @@ +/* 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 "Copy color" item of the context menu #2: Test that correct color is +// copied if the color changes. + +const TEST_URI = ` + <style type="text/css"> + div { + color: #123ABC; + } + </style> + <div>Testing the color picker tooltip!</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + + let {inspector, view} = yield openRuleView(); + + yield testCopyToClipboard(inspector, view); + yield testManualEdit(inspector, view); + yield testColorPickerEdit(inspector, view); +}); + +function* testCopyToClipboard(inspector, view) { + info("Testing that color is copied to clipboard"); + + yield selectNode("div", inspector); + + let element = getRuleViewProperty(view, "div", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + + let allMenuItems = openStyleContextMenuAndGetAllItems(view, element); + let menuitemCopyColor = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyColor")); + + ok(menuitemCopyColor.visible, "Copy color is visible"); + + yield waitForClipboardPromise(() => menuitemCopyColor.click(), + "#123ABC"); + + EventUtils.synthesizeKey("VK_ESCAPE", { }); +} + +function* testManualEdit(inspector, view) { + info("Testing manually edited colors"); + yield selectNode("div", inspector); + + let {valueSpan} = getRuleViewProperty(view, "div", "color"); + + let newColor = "#C9184E"; + let editor = yield focusEditableField(view, valueSpan); + + info("Typing new value"); + let input = editor.input; + let onBlur = once(input, "blur"); + EventUtils.sendString(newColor + ";", view.styleWindow); + yield onBlur; + yield wait(1); + + let colorValueElement = getRuleViewProperty(view, "div", "color") + .valueSpan.firstChild; + is(colorValueElement.dataset.color, newColor, "data-color was updated"); + + view.styleDocument.popupNode = colorValueElement; + + let contextMenu = view._contextmenu; + contextMenu._isColorPopup(); + is(contextMenu._colorToCopy, newColor, "_colorToCopy has the new value"); +} + +function* testColorPickerEdit(inspector, view) { + info("Testing colors edited via color picker"); + yield selectNode("div", inspector); + + let swatchElement = getRuleViewProperty(view, "div", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + + info("Opening the color picker"); + let picker = view.tooltips.colorPicker; + let onColorPickerReady = picker.once("ready"); + swatchElement.click(); + yield onColorPickerReady; + + let rgbaColor = [83, 183, 89, 1]; + let rgbaColorText = "rgba(83, 183, 89, 1)"; + yield simulateColorPickerChange(view, picker, rgbaColor); + + is(swatchElement.parentNode.dataset.color, rgbaColorText, + "data-color was updated"); + view.styleDocument.popupNode = swatchElement; + + let contextMenu = view._contextmenu; + contextMenu._isColorPopup(); + is(contextMenu._colorToCopy, rgbaColorText, "_colorToCopy has the new value"); +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js new file mode 100644 index 000000000..412137825 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* Tests both Copy URL and Copy Data URL context menu items */ + +const TEST_DATA_URI = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="; + +// Invalid URL still needs to be reachable otherwise getImageDataUrl will +// timeout. DevTools chrome:// URLs aren't content accessible, so use some +// random resource:// URL here. +const INVALID_IMAGE_URI = "resource://devtools/client/definitions.js"; +const ERROR_MESSAGE = STYLE_INSPECTOR_L10N.getStr("styleinspector.copyImageDataUrlError"); + +add_task(function* () { + const TEST_URI = `<style type="text/css"> + .valid-background { + background-image: url(${TEST_DATA_URI}); + } + .invalid-background { + background-image: url(${INVALID_IMAGE_URI}); + } + </style> + <div class="valid-background">Valid background image</div> + <div class="invalid-background">Invalid background image</div>`; + + yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_URI)); + + yield startTest(); +}); + +function* startTest() { + info("Opening rule view"); + let {inspector, view} = yield openRuleView(); + + info("Test valid background image URL in rule view"); + yield testCopyUrlToClipboard({view, inspector}, "data-uri", + ".valid-background", TEST_DATA_URI); + yield testCopyUrlToClipboard({view, inspector}, "url", + ".valid-background", TEST_DATA_URI); + + info("Test invalid background image URL in rue view"); + yield testCopyUrlToClipboard({view, inspector}, "data-uri", + ".invalid-background", ERROR_MESSAGE); + yield testCopyUrlToClipboard({view, inspector}, "url", + ".invalid-background", INVALID_IMAGE_URI); + + info("Opening computed view"); + view = selectComputedView(inspector); + + info("Test valid background image URL in computed view"); + yield testCopyUrlToClipboard({view, inspector}, "data-uri", + ".valid-background", TEST_DATA_URI); + yield testCopyUrlToClipboard({view, inspector}, "url", + ".valid-background", TEST_DATA_URI); + + info("Test invalid background image URL in computed view"); + yield testCopyUrlToClipboard({view, inspector}, "data-uri", + ".invalid-background", ERROR_MESSAGE); + yield testCopyUrlToClipboard({view, inspector}, "url", + ".invalid-background", INVALID_IMAGE_URI); +} + +function* testCopyUrlToClipboard({view, inspector}, type, selector, expected) { + info("Select node in inspector panel"); + yield selectNode(selector, inspector); + + info("Retrieve background-image link for selected node in current " + + "styleinspector view"); + let property = getBackgroundImageProperty(view, selector); + let imageLink = property.valueSpan.querySelector(".theme-link"); + ok(imageLink, "Background-image link element found"); + + info("Simulate right click on the background-image URL"); + let allMenuItems = openStyleContextMenuAndGetAllItems(view, imageLink); + let menuitemCopyUrl = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyUrl")); + let menuitemCopyImageDataUrl = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyImageDataUrl")); + + info("Context menu is displayed"); + ok(menuitemCopyUrl.visible, + "\"Copy URL\" menu entry is displayed"); + ok(menuitemCopyImageDataUrl.visible, + "\"Copy Image Data-URL\" menu entry is displayed"); + + if (type == "data-uri") { + info("Click Copy Data URI and wait for clipboard"); + yield waitForClipboardPromise(() => { + return menuitemCopyImageDataUrl.click(); + }, expected); + } else { + info("Click Copy URL and wait for clipboard"); + yield waitForClipboardPromise(() => { + return menuitemCopyUrl.click(); + }, expected); + } + + info("Hide context menu"); +} + +function getBackgroundImageProperty(view, selector) { + let isRuleView = view instanceof CssRuleView; + if (isRuleView) { + return getRuleViewProperty(view, selector, "background-image"); + } + return getComputedViewProperty(view, "background-image"); +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_csslogic-content-stylesheets.js b/devtools/client/inspector/shared/test/browser_styleinspector_csslogic-content-stylesheets.js new file mode 100644 index 000000000..421a2bb47 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_csslogic-content-stylesheets.js @@ -0,0 +1,82 @@ +/* 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 stylesheets on HMTL and XUL document + +// FIXME: this test opens the devtools for nothing, it should be changed into a +// devtools/server/tests/mochitest/test_css-logic-...something...html +// test + +const TEST_URI_HTML = TEST_URL_ROOT + "doc_content_stylesheet.html"; +const TEST_URI_AUTHOR = TEST_URL_ROOT + "doc_author-sheet.html"; +const TEST_URI_XUL = TEST_URL_ROOT + "doc_content_stylesheet.xul"; +const XUL_URI = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(TEST_URI_XUL, null, null); +var ssm = Components.classes["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); +const XUL_PRINCIPAL = ssm.createCodebasePrincipal(XUL_URI, {}); + +add_task(function* () { + requestLongerTimeout(2); + + info("Checking stylesheets on HTML document"); + yield addTab(TEST_URI_HTML); + + let {inspector, testActor} = yield openInspector(); + yield selectNode("#target", inspector); + + info("Checking stylesheets"); + yield checkSheets("#target", testActor); + + info("Checking authored stylesheets"); + yield addTab(TEST_URI_AUTHOR); + + ({inspector} = yield openInspector()); + yield selectNode("#target", inspector); + yield checkSheets("#target", testActor); + + info("Checking stylesheets on XUL document"); + info("Allowing XUL content"); + allowXUL(); + yield addTab(TEST_URI_XUL); + + ({inspector} = yield openInspector()); + yield selectNode("#target", inspector); + + yield checkSheets("#target", testActor); + info("Disallowing XUL content"); + disallowXUL(); +}); + +function allowXUL() { + Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager) + .addFromPrincipal(XUL_PRINCIPAL, "allowXULXBL", + Ci.nsIPermissionManager.ALLOW_ACTION); +} + +function disallowXUL() { + Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager) + .addFromPrincipal(XUL_PRINCIPAL, "allowXULXBL", + Ci.nsIPermissionManager.DENY_ACTION); +} + +function* checkSheets(targetSelector, testActor) { + let sheets = yield testActor.getStyleSheetsInfoForNode(targetSelector); + + for (let sheet of sheets) { + if (!sheet.href || + /doc_content_stylesheet_/.test(sheet.href) || + // For the "authored" case. + /^data:.*seagreen/.test(sheet.href)) { + ok(sheet.isContentSheet, + sheet.href + " identified as content stylesheet"); + } else { + ok(!sheet.isContentSheet, + sheet.href + " identified as non-content stylesheet"); + } + } +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_output-parser.js b/devtools/client/inspector/shared/test/browser_styleinspector_output-parser.js new file mode 100644 index 000000000..f1f846f5d --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_output-parser.js @@ -0,0 +1,341 @@ +/* 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 expected outputs of the output-parser's parseCssProperty function. + +// This is more of a unit test than a mochitest-browser test, but can't be +// tested with an xpcshell test as the output-parser requires the DOM to work. + +const {OutputParser} = require("devtools/client/shared/output-parser"); +const {initCssProperties, getCssProperties} = require("devtools/shared/fronts/css-properties"); + +const COLOR_CLASS = "color-class"; +const URL_CLASS = "url-class"; +const CUBIC_BEZIER_CLASS = "bezier-class"; +const ANGLE_CLASS = "angle-class"; + +const TEST_DATA = [ + { + name: "width", + value: "100%", + test: fragment => { + is(countAll(fragment), 0); + is(fragment.textContent, "100%"); + } + }, + { + name: "width", + value: "blue", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "content", + value: "'red url(test.png) repeat top left'", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "content", + value: "\"blue\"", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "margin-left", + value: "url(something.jpg)", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "background-color", + value: "transparent", + test: fragment => { + is(countAll(fragment), 2); + is(countColors(fragment), 1); + is(fragment.textContent, "transparent"); + } + }, + { + name: "color", + value: "red", + test: fragment => { + is(countColors(fragment), 1); + is(fragment.textContent, "red"); + } + }, + { + name: "color", + value: "#F06", + test: fragment => { + is(countColors(fragment), 1); + is(fragment.textContent, "#F06"); + } + }, + { + name: "border", + value: "80em dotted pink", + test: fragment => { + is(countAll(fragment), 2); + is(countColors(fragment), 1); + is(getColor(fragment), "pink"); + } + }, + { + name: "color", + value: "red !important", + test: fragment => { + is(countColors(fragment), 1); + is(fragment.textContent, "red !important"); + } + }, + { + name: "background", + value: "red url(test.png) repeat top left", + test: fragment => { + is(countColors(fragment), 1); + is(countUrls(fragment), 1); + is(getColor(fragment), "red"); + is(getUrl(fragment), "test.png"); + is(countAll(fragment), 3); + } + }, + { + name: "background", + value: "blue url(test.png) repeat top left !important", + test: fragment => { + is(countColors(fragment), 1); + is(countUrls(fragment), 1); + is(getColor(fragment), "blue"); + is(getUrl(fragment), "test.png"); + is(countAll(fragment), 3); + } + }, + { + name: "list-style-image", + value: "url(\"images/arrow.gif\")", + test: fragment => { + is(countAll(fragment), 1); + is(getUrl(fragment), "images/arrow.gif"); + } + }, + { + name: "list-style-image", + value: "url(\"images/arrow.gif\")!important", + test: fragment => { + is(countAll(fragment), 1); + is(getUrl(fragment), "images/arrow.gif"); + is(fragment.textContent, "url(\"images/arrow.gif\")!important"); + } + }, + { + name: "-moz-binding", + value: "url(http://somesite.com/path/to/binding.xml#someid)", + test: fragment => { + is(countAll(fragment), 1); + is(countUrls(fragment), 1); + is(getUrl(fragment), "http://somesite.com/path/to/binding.xml#someid"); + } + }, + { + name: "background", + value: "linear-gradient(to right, rgba(183,222,237,1) 0%, " + + "rgba(33,180,226,1) 30%, rgba(31,170,217,.5) 44%, " + + "#F06 75%, red 100%)", + test: fragment => { + is(countAll(fragment), 10); + let allSwatches = fragment.querySelectorAll("." + COLOR_CLASS); + is(allSwatches.length, 5); + is(allSwatches[0].textContent, "rgba(183,222,237,1)"); + is(allSwatches[1].textContent, "rgba(33,180,226,1)"); + is(allSwatches[2].textContent, "rgba(31,170,217,.5)"); + is(allSwatches[3].textContent, "#F06"); + is(allSwatches[4].textContent, "red"); + } + }, + { + name: "background", + value: "-moz-radial-gradient(center 45deg, circle closest-side, " + + "orange 0%, red 100%)", + test: fragment => { + is(countAll(fragment), 6); + let colorSwatches = fragment.querySelectorAll("." + COLOR_CLASS); + is(colorSwatches.length, 2); + is(colorSwatches[0].textContent, "orange"); + is(colorSwatches[1].textContent, "red"); + let angleSwatches = fragment.querySelectorAll("." + ANGLE_CLASS); + is(angleSwatches.length, 1); + is(angleSwatches[0].textContent, "45deg"); + } + }, + { + name: "background", + value: "white url(http://test.com/wow_such_image.png) no-repeat top left", + test: fragment => { + is(countAll(fragment), 3); + is(countUrls(fragment), 1); + is(countColors(fragment), 1); + } + }, + { + name: "background", + value: "url(\"http://test.com/wow_such_(oh-noes)image.png?testid=1&color=red#w00t\")", + test: fragment => { + is(countAll(fragment), 1); + is(getUrl(fragment), "http://test.com/wow_such_(oh-noes)image.png?testid=1&color=red#w00t"); + } + }, + { + name: "background-image", + value: "url(this-is-an-incredible-image.jpeg)", + test: fragment => { + is(countAll(fragment), 1); + is(getUrl(fragment), "this-is-an-incredible-image.jpeg"); + } + }, + { + name: "background", + value: "red url( \"http://wow.com/cool/../../../you're(doingit)wrong\" ) repeat center", + test: fragment => { + is(countAll(fragment), 3); + is(countColors(fragment), 1); + is(getUrl(fragment), "http://wow.com/cool/../../../you're(doingit)wrong"); + } + }, + { + name: "background-image", + value: "url(../../../look/at/this/folder/structure/../" + + "../red.blue.green.svg )", + test: fragment => { + is(countAll(fragment), 1); + is(getUrl(fragment), "../../../look/at/this/folder/structure/../" + + "../red.blue.green.svg"); + } + }, + { + name: "transition-timing-function", + value: "linear", + test: fragment => { + is(countCubicBeziers(fragment), 1); + is(getCubicBezier(fragment), "linear"); + } + }, + { + name: "animation-timing-function", + value: "ease-in-out", + test: fragment => { + is(countCubicBeziers(fragment), 1); + is(getCubicBezier(fragment), "ease-in-out"); + } + }, + { + name: "animation-timing-function", + value: "cubic-bezier(.1, 0.55, .9, -3.45)", + test: fragment => { + is(countCubicBeziers(fragment), 1); + is(getCubicBezier(fragment), "cubic-bezier(.1, 0.55, .9, -3.45)"); + } + }, + { + name: "animation", + value: "move 3s cubic-bezier(.1, 0.55, .9, -3.45)", + test: fragment => { + is(countCubicBeziers(fragment), 1); + is(getCubicBezier(fragment), "cubic-bezier(.1, 0.55, .9, -3.45)"); + } + }, + { + name: "transition", + value: "top 1s ease-in", + test: fragment => { + is(countCubicBeziers(fragment), 1); + is(getCubicBezier(fragment), "ease-in"); + } + }, + { + name: "transition", + value: "top 3s steps(4, end)", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "transition", + value: "top 3s step-start", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "transition", + value: "top 3s step-end", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "background", + value: "rgb(255, var(--g-value), 192)", + test: fragment => { + is(fragment.textContent, "rgb(255, var(--g-value), 192)"); + } + }, + { + name: "background", + value: "rgb(255, var(--g-value, 0), 192)", + test: fragment => { + is(fragment.textContent, "rgb(255, var(--g-value, 0), 192)"); + } + } +]; + +add_task(function* () { + // Mock the toolbox that initCssProperties expect so we get the fallback css properties. + let toolbox = {target: {client: {}, hasActor: () => false}}; + yield initCssProperties(toolbox); + let cssProperties = getCssProperties(toolbox); + + let parser = new OutputParser(document, cssProperties); + for (let i = 0; i < TEST_DATA.length; i++) { + let data = TEST_DATA[i]; + info("Output-parser test data " + i + ". {" + data.name + " : " + + data.value + ";}"); + data.test(parser.parseCssProperty(data.name, data.value, { + colorClass: COLOR_CLASS, + urlClass: URL_CLASS, + bezierClass: CUBIC_BEZIER_CLASS, + angleClass: ANGLE_CLASS, + defaultColorType: false + })); + } +}); + +function countAll(fragment) { + return fragment.querySelectorAll("*").length; +} +function countColors(fragment) { + return fragment.querySelectorAll("." + COLOR_CLASS).length; +} +function countUrls(fragment) { + return fragment.querySelectorAll("." + URL_CLASS).length; +} +function countCubicBeziers(fragment) { + return fragment.querySelectorAll("." + CUBIC_BEZIER_CLASS).length; +} +function getColor(fragment, index) { + return fragment.querySelectorAll("." + COLOR_CLASS)[index||0].textContent; +} +function getUrl(fragment, index) { + return fragment.querySelectorAll("." + URL_CLASS)[index||0].textContent; +} +function getCubicBezier(fragment, index) { + return fragment.querySelectorAll("." + CUBIC_BEZIER_CLASS)[index||0] + .textContent; +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_active.js b/devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_active.js new file mode 100644 index 000000000..942fe05e2 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_active.js @@ -0,0 +1,43 @@ +/* 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 style-inspector views only refresh when they are active. + +const TEST_URI = ` + <div id="one" style="color:red;">one</div> + <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"), "red", + "The rule-view shows the properties for test node one"); + + let cView = inspector.computedview.computedView; + 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"); + let onComputedViewReady = inspector.once("computed-view-refreshed"); + selectComputedView(inspector); + yield onComputedViewReady; + + ok(getComputedViewPropertyValue(cView, "color"), "#F00", + "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"), "red", + "The rule-view doesn't the properties for test node two"); +}); diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-background-image.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-background-image.js new file mode 100644 index 000000000..bd467b800 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-background-image.js @@ -0,0 +1,125 @@ +/* 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 background-image URLs have image preview tooltips in the rule-view +// and computed-view + +const TEST_URI = ` + <style type="text/css"> + body { + padding: 1em; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAADI5JREFUeNrsWwuQFNUVPf1m5z87szv7HWSWj8CigBFMEFZKiQsB1PgJwUAZg1HBpIQsKmokEhNjWUnFVPnDWBT+KolJYbRMoqUVq0yCClpqiX8sCchPWFwVlt2db7+X93pez7zu6Vn2NxsVWh8987p7pu+9555z7+tZjTGGY3kjOMa34w447oBjfKsY7i/UNM3Y8eFSAkD50Plgw03K5P9gvGv7U5ieeR3PszeREiPNX3/0DL4hjslzhm8THh+OITfXk3dhiv4GDtGPVzCaeJmPLYzuu5qJuWfuw2QTlcN1X9pwQU7LhdZ/ZAseD45cOh9hHvDkc/yAF/DNhdb5Mrr3PvBMaAYW8fMSIi2G497IMEK/YutGtAYr6+ej+nxu/NN8Ks3N7AR6HgcLz0Eg1Ljg1UcxZzi5qewIkMYLRweTr2Kzp+nmyXAd5pS3XQDd+N/4h4zgu9FI7brlXf90nMEnuwQxlvv+hosE3TuexmWeysmT4W+WxkMaLzf9Y8ATgjcUn7T9H1gqrpFq8eV1gMn6t16NhngjfoX6q4DUP032Rd4LJgpSLwJ1yzFqBG69eRkah0MVyo0Acfe+yy9AG4nMiYCkeM53KKFXncBLAXqEm+wCqZwaueq7WCmuLTcKSJmj737ol2hurA9eq9VdyiO8yWa3NNyog+SB5CZodSsQq/dfu34tJpYbBaTMzvVddDZu16q5smXf4G8zEvqm4cyaAmJPuTJk3oJWdS4WzcVtfMZbThSQckb/pYfRGgo3zNOqZnEHbJPGK4abaDCQIIsT8V/qTaBqHkLh6LzXH8XZQhbLhYKyyCC/WeHYcNdmvOgfe8skzbWL270/T3wf7tSx/lGCbTu8xlzzmCSWLc5iwmgikcCHi3Mga0Ry913vBFvQwg90l6M4ImWKfsWOp7DSWxmfpPlCFuPFfsNfKrCnPYpQKIRgqBK7D0SxYaNHwkEiJMtl0ReDp3Lc5D3PGoTo/sKngCl7a5chFqvBatKwjBd7WwqIlzB/78NcoUcp5VSgGxm+7b8eqQRGnHMO634epO4S1EZww09/iFg5UmGoESDuznP1xVhTUX1WWHPzjpd25wyH0hRxI3LGM75nxmuNEEUVpAN0XgxmPoKralakbQnWlIMQyVBD/w+3orkq4lvualjKyWwzt4MaxqspQHVhPOWG64bxYuhZXSFGWhipbSDVragOu5Y9eAsmDDUKyBA703vemVhHoueD6e9wAzJK1WfmN0Umk5GGM4kEMZcuIECqgjm0nldAqmbjwtm4VxZH5AvlADP6mx9Eqy9Q0+KqW8Ch+47FaMMYmnNGfY1iPMshoC6qFxme4wQ+0p+ARE6H3+9veWEDWgUhDhUKyFARn4jM5BNxT0XsMg7bfymGK1ov3wtjDfhL4w0HVGUVBEjDaaE+QNdrcNWch1PG4W6xrjBUXECGivg++Cva3JUT4iQUz3V2RsSVaKLwOuDT89A3HdBQoxhNC+fnVm74ual2EG893P6G+PuP4SfiO4cCBWQooL9qCWKNXPbcI37Aa/lnlZxXRt4RFONGwSDCPAHqOuqjWct1QiEMw5mChM5X4K47FyNqcd3aK9AwFH0CGYLoe1ctxk2eWi57rg5JfGp9rzC6ggCdFlAgHBDw5Yxlcg6G8SyHCjMlsgmDD9zhSeHlF+JnAgWDTQUy2NxfdwOao1UVV3pi3+bE97YSbWpLAbn6zefHNQkp1PMpIBwwvslKgIYTKM2nEpNzrGcH3FXTEal0L38kJ4uDQgEZbO4vnI173LXf5NHZaiUxtaCxyZuo/rK6LpUg54yg3zTWRAArvDcRIPZ6BqzrQ1REpmL+DNw32OKIDCb3X1qPVn8wNNMT4w2bvs+q4bAZrqBh2skaL3yyhhIIZ4i6oHkUK0RckcB8GigEyRIH4A6Mgc8fatl0/+BkkQxC9gIT4ljna1rIZW9rEdNbjJcNjsnoYj7LHWCUwpITzEgzRQKZ3XAFHbTzA3hrz8TEUUZxFBhoKpABQt/97p+w0hMZG68I8R6FtlsJT3FELndZntjM+VMnylKYq8GJI3UZaRMpquGSGFVOEfv0YZBMNzz+uvjbfzS6xQERIhlI9FcvQWNdFVb7x1zCb+QNK8vb9NsiifmI5hBgVoOCBC1sb0ab5RomqENxLO3eA1/0NDRU47q2RQNbRCUDIb7lF2CNL3ZGxEV4n08TVvZWYG4pZyV0zUdS45tyCBByOHWiyvZmxFXDCyRo1ge5+Sy0TA+8lWMiP/6O0S32exGV9Jf4fr8azdUR3zL/CZz4MtvzdX5uOYs6NDOmpkuj5Huh+7qUQSYl0ThHzw0YQzcGo6bhzEqoYq5rN3yRiYiG3Vfe2Ybm/qKA9NNZ3nNm4F7/yDkg9AN+U1mHiBcXP8zuDN76jj8hg1QyiWQigalj02BJPhK8I0zxijAjhp5zhlpLUDvS+BCy2HMAvvB4XDgL9/SXC0g/ou/5+6/xLX8w0uJrOIkXfPvyhY0F6gr7M8H0KWFYikcqAXakB+xwD9CdREBLoau7Gz3cAdSIdLFxFtJTCqRChSjnutvhDcREtzjz2Tswtz+yeNRFUeXZXtWux7C1fuoVcbd3J//ipDX3uZZDLGrwweS+UBLL5TDliVBnF8P7H+XI8aRRGsIBJg/Zlslt1+W+D1JWoSyi+kD9jfhs78t7mhZhSl+fLfY1Bdyv3I8V/qpY3B1McgN7ZFT5/vNO0I5DPLLdPBIJA8qc4h2I0QplYfDpJwHT+aj0246r5S8rToG8OjCle8wk4OLvvYGa+Ovr84uo2qBSwJS9G5egoZFLTfiEqWDtbwGfHgKOdPHcS+ai7XDzMPW/FJRLGGcxnBbK4YJC2K+h+T6Bdu5CqHqCWERd3bawb7JI+iJ735+LNaHaprBLLHBm08U3XxShEsdt+f3eTh3v7aC95Dct4RCWL5OZWh/oXBZThxAIxyOXLzBk8aiEWJID8rK3CpPOmeHaGpvCS+7EHv5FujVHUSJPLXvIFeHcNc+9xrB2gws9KZdxuLFax/WLM5gzzSm/lTXF/OdAcapyvjxPqxqHjr2v4ckX2bS2dRBrc5lSdpKjEJ9/9tdwX2WMd53ZQ2IVo3RES+UwVSpCPvYepNx4gmTGDUKIMQ4eduPnD7mx9xOn/KZKOlFbStjONxHTtR+BYAPmnoZ1Zp8wkBRwP/EL3u0F/C2hGl7vpz7vW37T3vP7if8wroKuoh8ribknX9BK5rcF+mo1qKaKyRPJTgTDjbzY8szcuLb3bpH00u35T47j7prRpwDJTxzyG0dHgxPp5bPG8VdkpfPbUg3SgoOo2mwVukb98D5EqpswZTTulCggTk4gpYhv0++wIhCJxr0+Hq1sondis0SE2oxQe3qWXwWyO4DSQg9gJ8Iiw1VFcGqXxet0N9xE4ygIxv/9W6wo9WyROEX/R+eiobYSq2vHTOR631Eiv2lRfh9dvxkumkXh92Qsx8XrAJ+7YGbWuhxOi/U+31NQmzyqNYG8N/3wfo6CRtRHcN01FzkvojohwLu0VVvDa56IS/xcj2b7nN+O+m0jqpE1wMPXZxAN9iCVThtDvH7gmiRGRpU8Lspv1Uhq4wIVdQoyuGSLNYPKUCS8+CzNURbzMmjK3i8u0U793lmuV0ef9nWQ5MGC/DiUqEUSaCtXna9RJEspZS1lrXINK/pcq+SpT50t98QKMq1FRmDfx3vxty102k0PM4ssEnvuz5+G26Ij4yDpz6z9fV8bkyIkqBFkhej0Ib+ZQ34XJK9AfozaiimqIoX3Jp3tiISrcfYpuN2+iFph/02P36PNC9fVcCnp6H9jYouKyfaWufz5Tp9tVxcUniw7IohZv4dZz81/ns67z3AYPrc2n0+Ix2q8k0PWjgBy88XaibnfK9A+5LdDY2Ivhy36fbT8Zv3Lb1U1qLqUxorXEEXIs0mjjrtxoTZWtdvigNs2sgPiujTv6DIZLld6b/V5742JZV3fUsUVFy5gdsNtKWFzUCEVbNepD1MkSMVbsb6SZm7jI3/zODtQKgUMsOw8wDZ63t5xcV1TnaEAxoc6wrqY+Fj+N4DsqOnhOIdicrQSm1MPYCPlIqHn5bbHg8/bj2D3QfZnCX3mpAICDZV8jH5kpbZqTD0W+DxaA74CWzLN2nd14OlL72J38Lf7+TjC7dadZFDoZJQPrtaIKL/G0L6ktptPZVJ8fMqHYPZOKYPMyQGadIJfDvdXwAFiZOTvDBPydf5vk4rWA+RfdhBlaF/yDDBRoMu9pfnSjv/p7DG+HXfAcQcc49v/BBgAcFAO4DmB2GQAAAAASUVORK5CYII=); + background-repeat: repeat-y; + background-position: right top; + } + .test-element { + font-family: verdana; + color: #333; + background: url(chrome://global/skin/icons/warning-64.png) no-repeat left center; + padding-left: 70px; + } + </style> + <div class="test-element">test element</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + info("Testing the background-image property on the body rule"); + yield testBodyRuleView(view); + + info("Selecting the test div node"); + yield selectNode(".test-element", inspector); + info("Testing the the background property on the .test-element rule"); + yield testDivRuleView(view); + + info("Testing that image preview tooltips show even when there are " + + "fields being edited"); + yield testTooltipAppearsEvenInEditMode(view); + + info("Switching over to the computed-view"); + let onComputedViewReady = inspector.once("computed-view-refreshed"); + view = selectComputedView(inspector); + yield onComputedViewReady; + + info("Testing that the background-image computed style has a tooltip too"); + yield testComputedView(view); +}); + +function* testBodyRuleView(view) { + info("Testing tooltips in the rule view"); + let panel = view.tooltips.previewTooltip.panel; + + // Check that the rule view has a tooltip and that a XUL panel has + // been created + ok(view.tooltips.previewTooltip, "Tooltip instance exists"); + ok(panel, "XUL panel exists"); + + // Get the background-image property inside the rule view + let {valueSpan} = getRuleViewProperty(view, "body", "background-image"); + let uriSpan = valueSpan.querySelector(".theme-link"); + + yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan); + + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src") + .indexOf("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHe") !== -1, + "The image URL seems fine"); +} + +function* testDivRuleView(view) { + let panel = view.tooltips.previewTooltip.panel; + + // Get the background property inside the rule view + let {valueSpan} = getRuleViewProperty(view, ".test-element", "background"); + let uriSpan = valueSpan.querySelector(".theme-link"); + + yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan); + + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src").startsWith("data:"), + "Tooltip contains a data-uri image as expected"); +} + +function* testTooltipAppearsEvenInEditMode(view) { + info("Switching to edit mode in the rule view"); + let editor = yield turnToEditMode(view); + + info("Now trying to show the preview tooltip"); + let {valueSpan} = getRuleViewProperty(view, ".test-element", "background"); + let uriSpan = valueSpan.querySelector(".theme-link"); + yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan); + + is(view.styleDocument.activeElement, editor.input, + "Tooltip was shown in edit mode, and inplace-editor still focused"); +} + +function turnToEditMode(ruleView) { + let brace = ruleView.styleDocument.querySelector(".ruleview-ruleclose"); + return focusEditableField(ruleView, brace); +} + +function* testComputedView(view) { + let tooltip = view.tooltips.previewTooltip; + ok(tooltip, "The computed-view has a tooltip defined"); + + let panel = tooltip.panel; + ok(panel, "The computed-view tooltip has a XUL panel"); + + let {valueSpan} = getComputedViewProperty(view, "background-image"); + let uriSpan = valueSpan.querySelector(".theme-link"); + + yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan); + + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + + ok(images[0].getAttribute("src").startsWith("data:"), + "Tooltip contains a data-uri in the computed-view too"); +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-closes-on-new-selection.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-closes-on-new-selection.js new file mode 100644 index 000000000..7f15d4fbe --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-closes-on-new-selection.js @@ -0,0 +1,73 @@ +/* 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 if a tooltip is visible when a new selection is made, it closes + +const TEST_URI = "<div class='one'>el 1</div><div class='two'>el 2</div>"; +const XHTML_NS = "http://www.w3.org/1999/xhtml"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode(".one", inspector); + + info("Testing rule view tooltip closes on new selection"); + yield testRuleView(view, inspector); + + info("Testing computed view tooltip closes on new selection"); + view = selectComputedView(inspector); + yield testComputedView(view, inspector); +}); + +function* testRuleView(ruleView, inspector) { + info("Showing the tooltip"); + + let tooltip = ruleView.tooltips.previewTooltip; + let tooltipContent = ruleView.styleDocument.createElementNS(XHTML_NS, "div"); + yield tooltip.setContent(tooltipContent, {width: 100, height: 30}); + + // Stop listening for mouse movements because it's not needed for this test, + // and causes intermittent failures on Linux. When this test runs in the suite + // sometimes a mouseleave event is dispatched at the start, which causes the + // tooltip to hide in the middle of being shown, which causes timeouts later. + tooltip.stopTogglingOnHover(); + + let onShown = tooltip.once("shown"); + tooltip.show(ruleView.styleDocument.firstElementChild); + yield onShown; + + info("Selecting a new node"); + let onHidden = tooltip.once("hidden"); + yield selectNode(".two", inspector); + yield onHidden; + + ok(true, "Rule view tooltip closed after a new node got selected"); +} + +function* testComputedView(computedView, inspector) { + info("Showing the tooltip"); + + let tooltip = computedView.tooltips.previewTooltip; + let tooltipContent = computedView.styleDocument.createElementNS(XHTML_NS, "div"); + yield tooltip.setContent(tooltipContent, {width: 100, height: 30}); + + // Stop listening for mouse movements because it's not needed for this test, + // and causes intermittent failures on Linux. When this test runs in the suite + // sometimes a mouseleave event is dispatched at the start, which causes the + // tooltip to hide in the middle of being shown, which causes timeouts later. + tooltip.stopTogglingOnHover(); + + let onShown = tooltip.once("shown"); + tooltip.show(computedView.styleDocument.firstElementChild); + yield onShown; + + info("Selecting a new node"); + let onHidden = tooltip.once("hidden"); + yield selectNode(".one", inspector); + yield onHidden; + + ok(true, "Computed view tooltip closed after a new node got selected"); +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-longhand-fontfamily.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-longhand-fontfamily.js new file mode 100644 index 000000000..6bce367ae --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-longhand-fontfamily.js @@ -0,0 +1,120 @@ +/* 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 the fontfamily tooltip on longhand properties + +const TEST_URI = ` + <style type="text/css"> + #testElement { + font-family: cursive; + color: #333; + padding-left: 70px; + } + </style> + <div id="testElement">test element</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testElement", inspector); + yield testRuleView(view, inspector.selection.nodeFront); + + info("Opening the computed view"); + let onComputedViewReady = inspector.once("computed-view-refreshed"); + view = selectComputedView(inspector); + yield onComputedViewReady; + + yield testComputedView(view, inspector.selection.nodeFront); + + yield testExpandedComputedViewProperty(view, inspector.selection.nodeFront); +}); + +function* testRuleView(ruleView, nodeFront) { + info("Testing font-family tooltips in the rule view"); + + let tooltip = ruleView.tooltips.previewTooltip; + let panel = tooltip.panel; + + // Check that the rule view has a tooltip and that a XUL panel has + // been created + ok(tooltip, "Tooltip instance exists"); + ok(panel, "XUL panel exists"); + + // Get the font family property inside the rule view + let {valueSpan} = getRuleViewProperty(ruleView, "#testElement", + "font-family"); + + // And verify that the tooltip gets shown on this property + yield assertHoverTooltipOn(tooltip, valueSpan); + + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src").startsWith("data:"), + "Tooltip contains a data-uri image as expected"); + + let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront); + is(images[0].getAttribute("src"), dataURL, + "Tooltip contains the correct data-uri image"); +} + +function* testComputedView(computedView, nodeFront) { + info("Testing font-family tooltips in the computed view"); + + let tooltip = computedView.tooltips.previewTooltip; + let panel = tooltip.panel; + let {valueSpan} = getComputedViewProperty(computedView, "font-family"); + + yield assertHoverTooltipOn(tooltip, valueSpan); + + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src").startsWith("data:"), + "Tooltip contains a data-uri image as expected"); + + let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront); + is(images[0].getAttribute("src"), dataURL, + "Tooltip contains the correct data-uri image"); +} + +function* testExpandedComputedViewProperty(computedView, nodeFront) { + info("Testing font-family tooltips in expanded properties of the " + + "computed view"); + + info("Expanding the font-family property to reveal matched selectors"); + let propertyView = getPropertyView(computedView, "font-family"); + propertyView.matchedExpanded = true; + yield propertyView.refreshMatchedSelectors(); + + let valueSpan = propertyView.matchedSelectorsContainer + .querySelector(".bestmatch .other-property-value"); + + let tooltip = computedView.tooltips.previewTooltip; + let panel = tooltip.panel; + + yield assertHoverTooltipOn(tooltip, valueSpan); + + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src").startsWith("data:"), + "Tooltip contains a data-uri image as expected"); + + let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront); + is(images[0].getAttribute("src"), dataURL, + "Tooltip contains the correct data-uri image"); +} + +function getPropertyView(computedView, name) { + let propertyView = null; + computedView.propertyViews.some(function (view) { + if (view.name == name) { + propertyView = view; + return true; + } + return false; + }); + return propertyView; +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-multiple-background-images.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-multiple-background-images.js new file mode 100644 index 000000000..60d747a45 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-multiple-background-images.js @@ -0,0 +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"; + +// Test for bug 1026921: Ensure the URL of hovered url() node is used instead +// of the first found from the declaration as there might be multiple urls. + +const YELLOW_DOT = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gYcDCwCr0o5ngAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAANSURBVAjXY/j/n6EeAAd9An7Z55GEAAAAAElFTkSuQmCC"; +const BLUE_DOT = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gYcDCwlCkCM9QAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAANSURBVAjXY2Bg+F8PAAKCAX/tPkrkAAAAAElFTkSuQmCC"; +const TEST_STYLE = `h1 {background: url(${YELLOW_DOT}), url(${BLUE_DOT});}`; +const TEST_URI = `<style>${TEST_STYLE}</style><h1>test element</h1>`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector} = yield openInspector(); + + yield testRuleViewUrls(inspector); + yield testComputedViewUrls(inspector); +}); + +function* testRuleViewUrls(inspector) { + info("Testing tooltips in the rule view"); + let view = selectRuleView(inspector); + yield selectNode("h1", inspector); + + let {valueSpan} = getRuleViewProperty(view, "h1", "background"); + yield performChecks(view, valueSpan); +} + +function* testComputedViewUrls(inspector) { + info("Testing tooltips in the computed view"); + + let onComputedViewReady = inspector.once("computed-view-refreshed"); + let view = selectComputedView(inspector); + yield onComputedViewReady; + + let {valueSpan} = getComputedViewProperty(view, "background-image"); + + yield performChecks(view, valueSpan); +} + +/** + * A helper that checks url() tooltips contain correct images + */ +function* performChecks(view, propertyValue) { + function checkTooltip(panel, imageSrc) { + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + is(images[0].getAttribute("src"), imageSrc, "The image URL is correct"); + } + + let links = propertyValue.querySelectorAll(".theme-link"); + let panel = view.tooltips.previewTooltip.panel; + + info("Checking first link tooltip"); + yield assertHoverTooltipOn(view.tooltips.previewTooltip, links[0]); + checkTooltip(panel, YELLOW_DOT); + + info("Checking second link tooltip"); + yield assertHoverTooltipOn(view.tooltips.previewTooltip, links[1]); + checkTooltip(panel, BLUE_DOT); +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-shorthand-fontfamily.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-shorthand-fontfamily.js new file mode 100644 index 000000000..bb851ec92 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-shorthand-fontfamily.js @@ -0,0 +1,58 @@ +/* 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 the fontfamily tooltip on shorthand properties + +const TEST_URI = ` + <style type="text/css"> + #testElement { + font: italic bold .8em/1.2 Arial; + } + </style> + <div id="testElement">test element</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + yield selectNode("#testElement", inspector); + yield testRuleView(view, inspector.selection.nodeFront); +}); + +function* testRuleView(ruleView, nodeFront) { + info("Testing font-family tooltips in the rule view"); + + let tooltip = ruleView.tooltips.previewTooltip; + let panel = tooltip.panel; + + // Check that the rule view has a tooltip and that a XUL panel has + // been created + ok(tooltip, "Tooltip instance exists"); + ok(panel, "XUL panel exists"); + + // Get the computed font family property inside the font rule view + let propertyList = ruleView.element + .querySelectorAll(".ruleview-propertylist"); + let fontExpander = propertyList[1].querySelectorAll(".ruleview-expander")[0]; + fontExpander.click(); + + let rule = getRuleViewRule(ruleView, "#testElement"); + let valueSpan = rule + .querySelector(".ruleview-computed .ruleview-propertyvalue"); + + // And verify that the tooltip gets shown on this property + yield assertHoverTooltipOn(tooltip, valueSpan); + + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src") + .startsWith("data:"), "Tooltip contains a data-uri image as expected"); + + let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront); + is(images[0].getAttribute("src"), dataURL, + "Tooltip contains the correct data-uri image"); +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-size.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-size.js new file mode 100644 index 000000000..b231fe1b1 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-size.js @@ -0,0 +1,86 @@ +/* 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"; + +// Checking tooltips dimensions, to make sure their big enough to display their +// content + +const TEST_URI = ` + <style type="text/css"> + div { + width: 300px;height: 300px;border-radius: 50%; + background: red url(chrome://global/skin/icons/warning-64.png); + } + </style> + <div></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 testImageDimension(view); + yield testPickerDimension(view); +}); + +function* testImageDimension(ruleView) { + info("Testing background-image tooltip dimensions"); + + let tooltip = ruleView.tooltips.previewTooltip; + let panel = tooltip.panel; + let {valueSpan} = getRuleViewProperty(ruleView, "div", "background"); + let uriSpan = valueSpan.querySelector(".theme-link"); + + // Make sure there is a hover tooltip for this property, this also will fill + // the tooltip with its content + yield assertHoverTooltipOn(tooltip, uriSpan); + + info("Showing the tooltip"); + let onShown = tooltip.once("shown"); + tooltip.show(uriSpan); + yield onShown; + + // Let's not test for a specific size, but instead let's make sure it's at + // least as big as the image + let imageRect = panel.querySelector("img").getBoundingClientRect(); + let panelRect = panel.getBoundingClientRect(); + + ok(panelRect.width >= imageRect.width, + "The panel is wide enough to show the image"); + ok(panelRect.height >= imageRect.height, + "The panel is high enough to show the image"); + + let onHidden = tooltip.once("hidden"); + tooltip.hide(); + yield onHidden; +} + +function* testPickerDimension(ruleView) { + info("Testing color-picker tooltip dimensions"); + + let {valueSpan} = getRuleViewProperty(ruleView, "div", "background"); + let swatch = valueSpan.querySelector(".ruleview-colorswatch"); + let cPicker = ruleView.tooltips.colorPicker; + + let onReady = cPicker.once("ready"); + swatch.click(); + yield onReady; + + // The colorpicker spectrum's iframe has a fixed width height, so let's + // make sure the tooltip is at least as big as that + let spectrumRect = cPicker.spectrum.element.getBoundingClientRect(); + let panelRect = cPicker.tooltip.container.getBoundingClientRect(); + + ok(panelRect.width >= spectrumRect.width, + "The panel is wide enough to show the picker"); + ok(panelRect.height >= spectrumRect.height, + "The panel is high enough to show the picker"); + + let onHidden = cPicker.tooltip.once("hidden"); + let onRuleViewChanged = ruleView.once("ruleview-changed"); + cPicker.hide(); + yield onHidden; + yield onRuleViewChanged; +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-01.js b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-01.js new file mode 100644 index 000000000..68a91ff95 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-01.js @@ -0,0 +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"; + +// Test that the css transform highlighter is created only when asked + +const TEST_URI = ` + <style type="text/css"> + body { + transform: skew(16deg); + } + </style> + Test the css transform highlighter +`; + +const TYPE = "CssTransformHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + let overlay = view.highlighters; + + ok(!overlay.highlighters[TYPE], "No highlighter exists in the rule-view"); + let h = yield overlay._getHighlighter(TYPE); + ok(overlay.highlighters[TYPE], + "The highlighter has been created in the rule-view"); + is(h, overlay.highlighters[TYPE], "The right highlighter has been created"); + let h2 = yield overlay._getHighlighter(TYPE); + is(h, h2, + "The same instance of highlighter is returned everytime in the rule-view"); + + let onComputedViewReady = inspector.once("computed-view-refreshed"); + let cView = selectComputedView(inspector); + yield onComputedViewReady; + overlay = cView.highlighters; + + ok(!overlay.highlighters[TYPE], "No highlighter exists in the computed-view"); + h = yield overlay._getHighlighter(TYPE); + ok(overlay.highlighters[TYPE], + "The highlighter has been created in the computed-view"); + is(h, overlay.highlighters[TYPE], "The right highlighter has been created"); + h2 = yield overlay._getHighlighter(TYPE); + is(h, h2, "The same instance of highlighter is returned everytime " + + "in the computed-view"); +}); diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-02.js b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-02.js new file mode 100644 index 000000000..a44a31422 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-02.js @@ -0,0 +1,57 @@ +/* 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 css transform highlighter is created when hovering over a +// transform property + +const TEST_URI = ` + <style type="text/css"> + body { + transform: skew(16deg); + color: yellow; + } + </style> + Test the css transform highlighter +`; + +var TYPE = "CssTransformHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let hs = view.highlighters; + + ok(!hs.highlighters[TYPE], "No highlighter exists in the rule-view (1)"); + + info("Faking a mousemove on a non-transform property"); + let {valueSpan} = getRuleViewProperty(view, "body", "color"); + hs._onMouseMove({target: valueSpan}); + ok(!hs.highlighters[TYPE], "No highlighter exists in the rule-view (2)"); + + info("Faking a mousemove on a transform property"); + ({valueSpan} = getRuleViewProperty(view, "body", "transform")); + let onHighlighterShown = hs.once("highlighter-shown"); + hs._onMouseMove({target: valueSpan}); + yield onHighlighterShown; + + let onComputedViewReady = inspector.once("computed-view-refreshed"); + let cView = selectComputedView(inspector); + yield onComputedViewReady; + hs = cView.highlighters; + + ok(!hs.highlighters[TYPE], "No highlighter exists in the computed-view (1)"); + + info("Faking a mousemove on a non-transform property"); + ({valueSpan} = getComputedViewProperty(cView, "color")); + hs._onMouseMove({target: valueSpan}); + ok(!hs.highlighters[TYPE], "No highlighter exists in the computed-view (2)"); + + info("Faking a mousemove on a transform property"); + ({valueSpan} = getComputedViewProperty(cView, "transform")); + onHighlighterShown = hs.once("highlighter-shown"); + hs._onMouseMove({target: valueSpan}); + yield onHighlighterShown; +}); diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-03.js b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-03.js new file mode 100644 index 000000000..1ecdf279e --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-03.js @@ -0,0 +1,103 @@ +/* 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 css transform highlighter is shown when hovering over transform +// properties + +// Note that in this test, we mock the highlighter front, merely testing the +// behavior of the style-inspector UI for now + +const TEST_URI = ` + <style type="text/css"> + html { + transform: scale(.9); + } + body { + transform: skew(16deg); + color: purple; + } + </style> + Test the css transform highlighter +`; + +const TYPE = "CssTransformHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + // Mock the highlighter front to get the reference of the NodeFront + let HighlighterFront = { + isShown: false, + nodeFront: null, + nbOfTimesShown: 0, + show: function (nodeFront) { + this.nodeFront = nodeFront; + this.isShown = true; + this.nbOfTimesShown ++; + return promise.resolve(true); + }, + hide: function () { + this.nodeFront = null; + this.isShown = false; + return promise.resolve(); + }, + finalize: function () {} + }; + + // Inject the mock highlighter in the rule-view + let hs = view.highlighters; + hs.highlighters[TYPE] = HighlighterFront; + + let {valueSpan} = getRuleViewProperty(view, "body", "transform"); + + info("Checking that the HighlighterFront's show/hide methods are called"); + let onHighlighterShown = hs.once("highlighter-shown"); + hs._onMouseMove({target: valueSpan}); + yield onHighlighterShown; + ok(HighlighterFront.isShown, "The highlighter is shown"); + let onHighlighterHidden = hs.once("highlighter-hidden"); + hs._onMouseOut(); + yield onHighlighterHidden; + ok(!HighlighterFront.isShown, "The highlighter is hidden"); + + info("Checking that hovering several times over the same property doesn't" + + " show the highlighter several times"); + let nb = HighlighterFront.nbOfTimesShown; + onHighlighterShown = hs.once("highlighter-shown"); + hs._onMouseMove({target: valueSpan}); + yield onHighlighterShown; + is(HighlighterFront.nbOfTimesShown, nb + 1, "The highlighter was shown once"); + hs._onMouseMove({target: valueSpan}); + hs._onMouseMove({target: valueSpan}); + is(HighlighterFront.nbOfTimesShown, nb + 1, + "The highlighter was shown once, after several mousemove"); + + info("Checking that the right NodeFront reference is passed"); + yield selectNode("html", inspector); + ({valueSpan} = getRuleViewProperty(view, "html", "transform")); + onHighlighterShown = hs.once("highlighter-shown"); + hs._onMouseMove({target: valueSpan}); + yield onHighlighterShown; + is(HighlighterFront.nodeFront.tagName, "HTML", + "The right NodeFront is passed to the highlighter (1)"); + + yield selectNode("body", inspector); + ({valueSpan} = getRuleViewProperty(view, "body", "transform")); + onHighlighterShown = hs.once("highlighter-shown"); + hs._onMouseMove({target: valueSpan}); + yield onHighlighterShown; + is(HighlighterFront.nodeFront.tagName, "BODY", + "The right NodeFront is passed to the highlighter (2)"); + + info("Checking that the highlighter gets hidden when hovering a " + + "non-transform property"); + ({valueSpan} = getRuleViewProperty(view, "body", "color")); + onHighlighterHidden = hs.once("highlighter-hidden"); + hs._onMouseMove({target: valueSpan}); + yield onHighlighterHidden; + ok(!HighlighterFront.isShown, "The highlighter is hidden"); +}); diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-04.js b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-04.js new file mode 100644 index 000000000..9d81e2649 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-04.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"; + +// Test that the css transform highlighter is shown only when hovering over a +// transform declaration that isn't overriden or disabled + +// Note that unlike the other browser_styleinspector_transform-highlighter-N.js +// tests, this one only tests the rule-view as only this view features disabled +// and overriden properties + +const TEST_URI = ` + <style type="text/css"> + div { + background: purple; + width:300px;height:300px; + transform: rotate(16deg); + } + .test { + transform: skew(25deg); + } + </style> + <div class="test"></div> +`; + +const TYPE = "CssTransformHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode(".test", inspector); + + let hs = view.highlighters; + + info("Faking a mousemove on the overriden property"); + let {valueSpan} = getRuleViewProperty(view, "div", "transform"); + hs._onMouseMove({target: valueSpan}); + ok(!hs.highlighters[TYPE], + "No highlighter was created for the overriden property"); + + info("Disabling the applied property"); + let classRuleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = classRuleEditor.rule.textProps[0].editor; + propEditor.enable.click(); + yield classRuleEditor.rule._applyingModifications; + + info("Faking a mousemove on the disabled property"); + ({valueSpan} = getRuleViewProperty(view, ".test", "transform")); + hs._onMouseMove({target: valueSpan}); + ok(!hs.highlighters[TYPE], + "No highlighter was created for the disabled property"); + + info("Faking a mousemove on the now unoverriden property"); + ({valueSpan} = getRuleViewProperty(view, "div", "transform")); + let onHighlighterShown = hs.once("highlighter-shown"); + hs._onMouseMove({target: valueSpan}); + yield onHighlighterShown; +}); diff --git a/devtools/client/inspector/shared/test/doc_author-sheet.html b/devtools/client/inspector/shared/test/doc_author-sheet.html new file mode 100644 index 000000000..d611bb387 --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_author-sheet.html @@ -0,0 +1,37 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>authored sheet test</title> + <style> + #target { + color: chartreuse; + } + </style> + <script> + "use strict"; + var gIOService = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + + var style = "data:text/css,div { background-color: seagreen; }"; + var uri = gIOService.newURI(style, null, null); + var windowUtils = SpecialPowers.wrap(window) + .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor) + .getInterface(SpecialPowers.Ci.nsIDOMWindowUtils); + windowUtils.loadSheet(uri, windowUtils.AUTHOR_SHEET); + </script> +</head> +<body> + <div id="target"> the ocean </div> + <input type=text placeholder=test></input> + <input type=color></input> + <input type=range></input> + <input type=number></input> + <progress></progress> + <blockquote type=cite> + <pre _moz_quote=true> + inspect <a href="foo">user agent</a> styles + </pre> + </blockquote> +</body> +</html> diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet.html b/devtools/client/inspector/shared/test/doc_content_stylesheet.html new file mode 100644 index 000000000..f9b52f78d --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_content_stylesheet.html @@ -0,0 +1,32 @@ +<html> +<head> + <title>test</title> + <link href="./doc_content_stylesheet_linked.css" rel="stylesheet" type="text/css"> + <script> + /* exported loadCSS */ + "use strict"; + // Load script.css + function loadCSS() { + let link = document.createElement("link"); + link.rel = "stylesheet"; + link.type = "text/css"; + link.href = "./doc_content_stylesheet_script.css"; + document.getElementsByTagName("head")[0].appendChild(link); + } + </script> + <style> + table { + border: 1px solid #000; + } + </style> +</head> +<body onload="loadCSS();"> + <table id="target"> + <tr> + <td> + <h3>Simple test</h3> + </td> + </tr> + </table> +</body> +</html> diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet.xul b/devtools/client/inspector/shared/test/doc_content_stylesheet.xul new file mode 100644 index 000000000..efd53815d --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_content_stylesheet.xul @@ -0,0 +1,9 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/xul.css" type="text/css"?> +<?xml-stylesheet href="./doc_content_stylesheet_xul.css" + type="text/css"?> +<!DOCTYPE window> +<window id="testwindow" xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <label id="target" value="Simple XUL document" /> +</window>
\ No newline at end of file diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet_imported.css b/devtools/client/inspector/shared/test/doc_content_stylesheet_imported.css new file mode 100644 index 000000000..ea1a3d986 --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_content_stylesheet_imported.css @@ -0,0 +1,5 @@ +@import url("./doc_content_stylesheet_imported2.css"); + +#target { + text-decoration: underline; +} diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet_imported2.css b/devtools/client/inspector/shared/test/doc_content_stylesheet_imported2.css new file mode 100644 index 000000000..77c73299e --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_content_stylesheet_imported2.css @@ -0,0 +1,3 @@ +#target { + text-decoration: underline; +} diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet_linked.css b/devtools/client/inspector/shared/test/doc_content_stylesheet_linked.css new file mode 100644 index 000000000..712ba78fb --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_content_stylesheet_linked.css @@ -0,0 +1,3 @@ +table { + border-collapse: collapse; +} diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet_script.css b/devtools/client/inspector/shared/test/doc_content_stylesheet_script.css new file mode 100644 index 000000000..5aa5e2c6c --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_content_stylesheet_script.css @@ -0,0 +1,5 @@ +@import url("./doc_content_stylesheet_imported.css"); + +table { + opacity: 1; +} diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet_xul.css b/devtools/client/inspector/shared/test/doc_content_stylesheet_xul.css new file mode 100644 index 000000000..a14ae7f6f --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_content_stylesheet_xul.css @@ -0,0 +1,3 @@ +#target { + font-size: 200px; +} diff --git a/devtools/client/inspector/shared/test/doc_frame_script.js b/devtools/client/inspector/shared/test/doc_frame_script.js new file mode 100644 index 000000000..aeb73a115 --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_frame_script.js @@ -0,0 +1,115 @@ +/* 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/ */ +/* globals addMessageListener, sendAsyncMessage */ + +"use strict"; + +// A helper frame-script for brower/devtools/styleinspector tests. +// +// Most listeners in the script expect "Test:"-namespaced messages from chrome, +// then execute code upon receiving, and immediately send back a message. +// This is so that chrome test code can execute code in content and wait for a +// response this way: +// let response = yield executeInContent(browser, "Test:MsgName", data, true); +// The response message should have the same name "Test:MsgName" +// +// Some listeners do not send a response message back. + +var {utils: Cu} = Components; +var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +var defer = require("devtools/shared/defer"); + +/** + * Get a value for a given property name in a css rule in a stylesheet, given + * their indexes + * @param {Object} data Expects a data object with the following properties + * - {Number} styleSheetIndex + * - {Number} ruleIndex + * - {String} name + * @return {String} The value, if found, null otherwise + */ +addMessageListener("Test:GetRulePropertyValue", function (msg) { + let {name, styleSheetIndex, ruleIndex} = msg.data; + let value = null; + + dumpn("Getting the value for property name " + name + " in sheet " + + styleSheetIndex + " and rule " + ruleIndex); + + let sheet = content.document.styleSheets[styleSheetIndex]; + if (sheet) { + let rule = sheet.cssRules[ruleIndex]; + if (rule) { + value = rule.style.getPropertyValue(name); + } + } + + sendAsyncMessage("Test:GetRulePropertyValue", value); +}); + +/** + * Get the property value from the computed style for an element. + * @param {Object} data Expects a data object with the following properties + * - {String} selector: The selector used to obtain the element. + * - {String} pseudo: pseudo id to query, or null. + * - {String} name: name of the property + * @return {String} The value, if found, null otherwise + */ +addMessageListener("Test:GetComputedStylePropertyValue", function (msg) { + let {selector, pseudo, name} = msg.data; + let doc = content.document; + + let element = doc.querySelector(selector); + let value = content.getComputedStyle(element, pseudo).getPropertyValue(name); + sendAsyncMessage("Test:GetComputedStylePropertyValue", value); +}); + +/** + * Wait the property value from the computed style for an element and + * compare it with the expected value + * @param {Object} data Expects a data object with the following properties + * - {String} selector: The selector used to obtain the element. + * - {String} pseudo: pseudo id to query, or null. + * - {String} name: name of the property + * - {String} expected: the expected value for property + */ +addMessageListener("Test:WaitForComputedStylePropertyValue", function (msg) { + let {selector, pseudo, name, expected} = msg.data; + let element = content.document.querySelector(selector); + waitForSuccess(() => { + let value = content.document.defaultView.getComputedStyle(element, pseudo) + .getPropertyValue(name); + + return value === expected; + }).then(() => { + sendAsyncMessage("Test:WaitForComputedStylePropertyValue"); + }); +}); + +var dumpn = msg => dump(msg + "\n"); + +/** + * Polls a given function waiting for it to return true. + * + * @param {Function} validatorFn A validator function that returns a boolean. + * This is called every few milliseconds to check if the result is true. When + * it is true, the promise resolves. + * @param {String} name Optional name of the test. This is used to generate + * the success and failure messages. + * @return a promise that resolves when the function returned true or rejects + * if the timeout is reached + */ +function waitForSuccess(validatorFn, name = "untitled") { + let def = defer(); + + function wait(fn) { + if (fn()) { + def.resolve(); + } else { + setTimeout(() => wait(fn), 200); + } + } + wait(validatorFn); + + return def.promise; +} diff --git a/devtools/client/inspector/shared/test/head.js b/devtools/client/inspector/shared/test/head.js new file mode 100644 index 000000000..bcc2ec2c7 --- /dev/null +++ b/devtools/client/inspector/shared/test/head.js @@ -0,0 +1,557 @@ +/* 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/ */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from ../../test/head.js */ +"use strict"; + +// Import the inspector's head.js first (which itself imports shared-head.js). +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js", + this); + +var {CssRuleView} = require("devtools/client/inspector/rules/rules"); +var {getInplaceEditorForSpan: inplaceEditor} = + require("devtools/client/shared/inplace-editor"); +const {getColor: getThemeColor} = require("devtools/client/shared/theme"); + +const TEST_URL_ROOT = + "http://example.com/browser/devtools/client/inspector/shared/test/"; +const TEST_URL_ROOT_SSL = + "https://example.com/browser/devtools/client/inspector/shared/test/"; +const ROOT_TEST_DIR = getRootDirectory(gTestPath); +const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js"; +const STYLE_INSPECTOR_L10N = + new LocalizationHelper("devtools/shared/locales/styleinspector.properties"); + +// Clean-up all prefs that might have been changed during a test run +// (safer here because if the test fails, then the pref is never reverted) +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.defaultColorUnit"); +}); + +/** + * The functions found below are here to ease test development and maintenance. + * Most of these functions are stateless and will require some form of context + * (the instance of the current toolbox, or inspector panel for instance). + * + * Most of these functions are async too and return promises. + * + * All tests should follow the following pattern: + * + * add_task(function*() { + * yield addTab(TEST_URI); + * let {toolbox, inspector} = yield openInspector(); + * inspector.sidebar.select(viewId); + * let view = inspector[viewId].view; + * yield selectNode("#test", inspector); + * yield someAsyncTestFunction(view); + * }); + * + * add_task is the way to define the testcase in the test file. It accepts + * a single generator-function argument. + * The generator function should yield any async call. + * + * There is no need to clean tabs up at the end of a test as this is done + * automatically. + * + * It is advised not to store any references on the global scope. There + * shouldn't be a need to anyway. Thanks to add_task, test steps, even + * though asynchronous, can be described in a nice flat way, and + * if/for/while/... control flow can be used as in sync code, making it + * possible to write the outline of the test case all in add_task, and delegate + * actual processing and assertions to other functions. + */ + +/* ********************************************* + * UTILS + * ********************************************* + * General test utilities. + * Add new tabs, open the toolbox and switch to the various panels, select + * nodes, get node references, ... + */ + +/** + * The rule-view tests rely on a frame-script to be injected in the content test + * page. So override the shared-head's addTab to load the frame script after the + * tab was added. + * FIXME: Refactor the rule-view tests to use the testActor instead of a frame + * script, so they can run on remote targets too. + */ +var _addTab = addTab; +addTab = function (url) { + return _addTab(url).then(tab => { + info("Loading the helper frame script " + FRAME_SCRIPT_URL); + let browser = tab.linkedBrowser; + browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false); + return tab; + }); +}; + +/** + * Wait for a content -> chrome message on the message manager (the window + * messagemanager is used). + * + * @param {String} name + * The message name + * @return {Promise} A promise that resolves to the response data when the + * message has been received + */ +function waitForContentMessage(name) { + info("Expecting message " + name + " from content"); + + let mm = gBrowser.selectedBrowser.messageManager; + + let def = defer(); + mm.addMessageListener(name, function onMessage(msg) { + mm.removeMessageListener(name, onMessage); + def.resolve(msg.data); + }); + return def.promise; +} + +/** + * Send an async message to the frame script (chrome -> content) and wait for a + * response message with the same name (content -> chrome). + * + * @param {String} name + * The message name. Should be one of the messages defined + * in doc_frame_script.js + * @param {Object} data + * Optional data to send along + * @param {Object} objects + * Optional CPOW objects to send along + * @param {Boolean} expectResponse + * If set to false, don't wait for a response with the same name + * from the content script. Defaults to true. + * @return {Promise} Resolves to the response data if a response is expected, + * immediately resolves otherwise + */ +function executeInContent(name, data = {}, objects = {}, + expectResponse = true) { + info("Sending message " + name + " to content"); + let mm = gBrowser.selectedBrowser.messageManager; + + mm.sendAsyncMessage(name, data, objects); + if (expectResponse) { + return waitForContentMessage(name); + } + + return promise.resolve(); +} + +/** + * Send an async message to the frame script and get back the requested + * computed style property. + * + * @param {String} selector + * The selector used to obtain the element. + * @param {String} pseudo + * pseudo id to query, or null. + * @param {String} name + * name of the property. + */ +function* getComputedStyleProperty(selector, pseudo, propName) { + return yield executeInContent("Test:GetComputedStylePropertyValue", + {selector, + pseudo, + name: propName}); +} + +/** + * Send an async message to the frame script and wait until the requested + * computed style property has the expected value. + * + * @param {String} selector + * The selector used to obtain the element. + * @param {String} pseudo + * pseudo id to query, or null. + * @param {String} prop + * name of the property. + * @param {String} expected + * expected value of property + * @param {String} name + * the name used in test message + */ +function* waitForComputedStyleProperty(selector, pseudo, name, expected) { + return yield executeInContent("Test:WaitForComputedStylePropertyValue", + {selector, + pseudo, + expected, + name}); +} + +/** + * Given an inplace editable element, click to switch it to edit mode, wait for + * focus + * + * @return a promise that resolves to the inplace-editor element when ready + */ +var focusEditableField = Task.async(function* (ruleView, editable, xOffset = 1, + yOffset = 1, options = {}) { + let onFocus = once(editable.parentNode, "focus", true); + info("Clicking on editable field to turn to edit mode"); + EventUtils.synthesizeMouse(editable, xOffset, yOffset, options, + editable.ownerDocument.defaultView); + yield onFocus; + + info("Editable field gained focus, returning the input field now"); + let onEdit = inplaceEditor(editable.ownerDocument.activeElement); + + return onEdit; +}); + +/** + * Polls a given function waiting for it to return true. + * + * @param {Function} validatorFn + * A validator function that returns a boolean. + * This is called every few milliseconds to check if the result is true. + * When it is true, the promise resolves. + * @param {String} name + * Optional name of the test. This is used to generate + * the success and failure messages. + * @return a promise that resolves when the function returned true or rejects + * if the timeout is reached + */ +function waitForSuccess(validatorFn, name = "untitled") { + let def = defer(); + + function wait(validator) { + if (validator()) { + ok(true, "Validator function " + name + " returned true"); + def.resolve(); + } else { + setTimeout(() => wait(validator), 200); + } + } + wait(validatorFn); + + return def.promise; +} + +/** + * Get the dataURL for the font family tooltip. + * + * @param {String} font + * The font family value. + * @param {object} nodeFront + * The NodeActor that will used to retrieve the dataURL for the + * font family tooltip contents. + */ +var getFontFamilyDataURL = Task.async(function* (font, nodeFront) { + let fillStyle = getThemeColor("body-color"); + + let {data} = yield nodeFront.getFontFamilyDataURL(font, fillStyle); + let dataURL = yield data.string(); + return dataURL; +}); + +/* ********************************************* + * RULE-VIEW + * ********************************************* + * Rule-view related test utility functions + * This object contains functions to get rules, get properties, ... + */ + +/** + * Get the DOMNode for a css rule in the rule-view that corresponds to the given + * selector + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view for which the rule + * object is wanted + * @return {DOMNode} + */ +function getRuleViewRule(view, selectorText) { + let rule; + for (let r of view.styleDocument.querySelectorAll(".ruleview-rule")) { + let selector = r.querySelector(".ruleview-selectorcontainer, " + + ".ruleview-selector-matched"); + if (selector && selector.textContent === selectorText) { + rule = r; + break; + } + } + + return rule; +} + +/** + * Get references to the name and value span nodes corresponding to a given + * selector and property name in the rule-view + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view to look for the property in + * @param {String} propertyName + * The name of the property + * @return {Object} An object like {nameSpan: DOMNode, valueSpan: DOMNode} + */ +function getRuleViewProperty(view, selectorText, propertyName) { + let prop; + + let rule = getRuleViewRule(view, selectorText); + if (rule) { + // Look for the propertyName in that rule element + for (let p of rule.querySelectorAll(".ruleview-property")) { + let nameSpan = p.querySelector(".ruleview-propertyname"); + let valueSpan = p.querySelector(".ruleview-propertyvalue"); + + if (nameSpan.textContent === propertyName) { + prop = {nameSpan: nameSpan, valueSpan: valueSpan}; + break; + } + } + } + return prop; +} + +/** + * Get the text value of the property corresponding to a given selector and name + * in the rule-view + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view to look for the property in + * @param {String} propertyName + * The name of the property + * @return {String} The property value + */ +function getRuleViewPropertyValue(view, selectorText, propertyName) { + return getRuleViewProperty(view, selectorText, propertyName) + .valueSpan.textContent; +} + +/** + * Get a reference to the selector DOM element corresponding to a given selector + * in the rule-view + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view to look for + * @return {DOMNode} The selector DOM element + */ +function getRuleViewSelector(view, selectorText) { + let rule = getRuleViewRule(view, selectorText); + return rule.querySelector(".ruleview-selector, .ruleview-selector-matched"); +} + +/** + * Get a reference to the selectorhighlighter icon DOM element corresponding to + * a given selector in the rule-view + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view to look for + * @return {DOMNode} The selectorhighlighter icon DOM element + */ +function getRuleViewSelectorHighlighterIcon(view, selectorText) { + let rule = getRuleViewRule(view, selectorText); + return rule.querySelector(".ruleview-selectorhighlighter"); +} + +/** + * Simulate a color change in a given color picker tooltip, and optionally wait + * for a given element in the page to have its style changed as a result + * + * @param {RuleView} ruleView + * The related rule view instance + * @param {SwatchColorPickerTooltip} colorPicker + * @param {Array} newRgba + * The new color to be set [r, g, b, a] + * @param {Object} expectedChange + * Optional object that needs the following props: + * - {DOMNode} element The element in the page that will have its + * style changed. + * - {String} name The style name that will be changed + * - {String} value The expected style value + * The style will be checked like so: getComputedStyle(element)[name] === value + */ +var simulateColorPickerChange = Task.async(function* (ruleView, colorPicker, + newRgba, expectedChange) { + let onRuleViewChanged = ruleView.once("ruleview-changed"); + info("Getting the spectrum colorpicker object"); + let spectrum = yield colorPicker.spectrum; + info("Setting the new color"); + spectrum.rgb = newRgba; + info("Applying the change"); + spectrum.updateUI(); + spectrum.onChange(); + info("Waiting for rule-view to update"); + yield onRuleViewChanged; + + if (expectedChange) { + info("Waiting for the style to be applied on the page"); + yield waitForSuccess(() => { + let {element, name, value} = expectedChange; + return content.getComputedStyle(element)[name] === value; + }, "Color picker change applied on the page"); + } +}); + +/** + * Get a rule-link from the rule-view given its index + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {Number} index + * The index of the link to get + * @return {DOMNode} The link if any at this index + */ +function getRuleViewLinkByIndex(view, index) { + let links = view.styleDocument.querySelectorAll(".ruleview-rule-source"); + return links[index]; +} + +/** + * Get rule-link text from the rule-view given its index + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {Number} index + * The index of the link to get + * @return {String} The string at this index + */ +function getRuleViewLinkTextByIndex(view, index) { + let link = getRuleViewLinkByIndex(view, index); + return link.querySelector(".ruleview-rule-source-label").textContent; +} + +/** + * Click on a rule-view's close brace to focus a new property name editor + * + * @param {RuleEditor} ruleEditor + * An instance of RuleEditor that will receive the new property + * @return a promise that resolves to the newly created editor when ready and + * focused + */ +var focusNewRuleViewProperty = Task.async(function* (ruleEditor) { + info("Clicking on a close ruleEditor brace to start editing a new property"); + ruleEditor.closeBrace.scrollIntoView(); + let editor = yield focusEditableField(ruleEditor.ruleView, + ruleEditor.closeBrace); + + is(inplaceEditor(ruleEditor.newPropSpan), editor, + "Focused editor is the new property editor."); + + return editor; +}); + +/** + * Create a new property name in the rule-view, focusing a new property editor + * by clicking on the close brace, and then entering the given text. + * Keep in mind that the rule-view knows how to handle strings with multiple + * properties, so the input text may be like: "p1:v1;p2:v2;p3:v3". + * + * @param {RuleEditor} ruleEditor + * The instance of RuleEditor that will receive the new property(ies) + * @param {String} inputValue + * The text to be entered in the new property name field + * @return a promise that resolves when the new property name has been entered + * and once the value field is focused + */ +var createNewRuleViewProperty = Task.async(function* (ruleEditor, inputValue) { + info("Creating a new property editor"); + let editor = yield focusNewRuleViewProperty(ruleEditor); + + info("Entering the value " + inputValue); + editor.input.value = inputValue; + + info("Submitting the new value and waiting for value field focus"); + let onFocus = once(ruleEditor.element, "focus", true); + EventUtils.synthesizeKey("VK_RETURN", {}, + ruleEditor.element.ownerDocument.defaultView); + yield onFocus; +}); + +/** + * Set the search value for the rule-view filter styles search box. + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} searchValue + * The filter search value + * @return a promise that resolves when the rule-view is filtered for the + * search term + */ +var setSearchFilter = Task.async(function* (view, searchValue) { + info("Setting filter text to \"" + searchValue + "\""); + let win = view.styleWindow; + let searchField = view.searchField; + searchField.focus(); + synthesizeKeys(searchValue, win); + yield view.inspector.once("ruleview-filtered"); +}); + +/* ********************************************* + * COMPUTED-VIEW + * ********************************************* + * Computed-view related utility functions. + * Allows to get properties, links, expand properties, ... + */ + +/** + * Get references to the name and value span nodes corresponding to a given + * property name in the computed-view + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {String} name + * The name of the property to retrieve + * @return an object {nameSpan, valueSpan} + */ +function getComputedViewProperty(view, name) { + let prop; + for (let property of view.styleDocument.querySelectorAll(".property-view")) { + let nameSpan = property.querySelector(".property-name"); + let valueSpan = property.querySelector(".property-value"); + + if (nameSpan.textContent === name) { + prop = {nameSpan: nameSpan, valueSpan: valueSpan}; + break; + } + } + return prop; +} + +/** + * Get the text value of the property corresponding to a given name in the + * computed-view + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {String} name + * The name of the property to retrieve + * @return {String} The property value + */ +function getComputedViewPropertyValue(view, name, propertyName) { + return getComputedViewProperty(view, name, propertyName) + .valueSpan.textContent; +} + +/** + * Open the style editor context menu and return all of it's items in a flat array + * @param {CssRuleView} view + * The instance of the rule-view panel + * @return An array of MenuItems + */ +function openStyleContextMenuAndGetAllItems(view, target) { + let menu = view._contextmenu._openMenu({target: target}); + + // Flatten all menu items into a single array to make searching through it easier + let allItems = [].concat.apply([], menu.items.map(function addItem(item) { + if (item.submenu) { + return addItem(item.submenu.items); + } + return item; + })); + + return allItems; +} diff --git a/devtools/client/inspector/shared/tooltips-overlay.js b/devtools/client/inspector/shared/tooltips-overlay.js new file mode 100644 index 000000000..8a02d7e3d --- /dev/null +++ b/devtools/client/inspector/shared/tooltips-overlay.js @@ -0,0 +1,319 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * The tooltip overlays are tooltips that appear when hovering over property values and + * editor tooltips that appear when clicking swatch based editors. + */ + +const { Task } = require("devtools/shared/task"); +const Services = require("Services"); +const { + VIEW_NODE_VALUE_TYPE, + VIEW_NODE_IMAGE_URL_TYPE, +} = require("devtools/client/inspector/shared/node-types"); +const { getColor } = require("devtools/client/shared/theme"); +const { getCssProperties } = require("devtools/shared/fronts/css-properties"); +const CssDocsTooltip = require("devtools/client/shared/widgets/tooltip/CssDocsTooltip"); +const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip"); +const { + getImageDimensions, + setImageTooltip, + setBrokenImageTooltip, +} = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper"); +const SwatchColorPickerTooltip = require("devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip"); +const SwatchCubicBezierTooltip = require("devtools/client/shared/widgets/tooltip/SwatchCubicBezierTooltip"); +const SwatchFilterTooltip = require("devtools/client/shared/widgets/tooltip/SwatchFilterTooltip"); + +const PREF_IMAGE_TOOLTIP_SIZE = "devtools.inspector.imagePreviewTooltipSize"; + +// Types of existing tooltips +const TOOLTIP_IMAGE_TYPE = "image"; +const TOOLTIP_FONTFAMILY_TYPE = "font-family"; + +/** + * Manages all tooltips in the style-inspector. + * + * @param {CssRuleView|CssComputedView} view + * Either the rule-view or computed-view panel + */ +function TooltipsOverlay(view) { + this.view = view; + + let {CssRuleView} = require("devtools/client/inspector/rules/rules"); + this.isRuleView = view instanceof CssRuleView; + this._cssProperties = getCssProperties(this.view.inspector.toolbox); + + this._onNewSelection = this._onNewSelection.bind(this); + this.view.inspector.selection.on("new-node-front", this._onNewSelection); +} + +TooltipsOverlay.prototype = { + get isEditing() { + return this.colorPicker.tooltip.isVisible() || + this.colorPicker.eyedropperOpen || + this.cubicBezier.tooltip.isVisible() || + this.filterEditor.tooltip.isVisible(); + }, + + /** + * Add the tooltips overlay to the view. This will start tracking mouse + * movements and display tooltips when needed + */ + addToView: function () { + if (this._isStarted || this._isDestroyed) { + return; + } + + let { toolbox } = this.view.inspector; + + // Initializing the different tooltips that are used in the inspector. + // These tooltips are attached to the toolbox document if they require a popup panel. + // Otherwise, it is attached to the inspector panel document if it is an inline + // editor. + this.previewTooltip = new HTMLTooltip(toolbox.doc, { + type: "arrow", + useXulWrapper: true + }); + this.previewTooltip.startTogglingOnHover(this.view.element, + this._onPreviewTooltipTargetHover.bind(this)); + + // MDN CSS help tooltip + this.cssDocs = new CssDocsTooltip(toolbox.doc); + + if (this.isRuleView) { + // Color picker tooltip + this.colorPicker = new SwatchColorPickerTooltip(toolbox.doc, this.view.inspector); + // Cubic bezier tooltip + this.cubicBezier = new SwatchCubicBezierTooltip(toolbox.doc); + // Filter editor tooltip + this.filterEditor = new SwatchFilterTooltip(toolbox.doc, + this._cssProperties.getValidityChecker(this.view.inspector.panelDoc)); + } + + this._isStarted = true; + }, + + /** + * Remove the tooltips overlay from the view. This will stop tracking mouse + * movements and displaying tooltips + */ + removeFromView: function () { + if (!this._isStarted || this._isDestroyed) { + return; + } + + this.previewTooltip.stopTogglingOnHover(this.view.element); + this.previewTooltip.destroy(); + + if (this.colorPicker) { + this.colorPicker.destroy(); + } + + if (this.cubicBezier) { + this.cubicBezier.destroy(); + } + + if (this.cssDocs) { + this.cssDocs.destroy(); + } + + if (this.filterEditor) { + this.filterEditor.destroy(); + } + + this._isStarted = false; + }, + + /** + * Given a hovered node info, find out which type of tooltip should be shown, + * if any + * + * @param {Object} nodeInfo + * @return {String} The tooltip type to be shown, or null + */ + _getTooltipType: function ({type, value: prop}) { + let tooltipType = null; + let inspector = this.view.inspector; + + // Image preview tooltip + if (type === VIEW_NODE_IMAGE_URL_TYPE && + inspector.hasUrlToImageDataResolver) { + tooltipType = TOOLTIP_IMAGE_TYPE; + } + + // Font preview tooltip + if (type === VIEW_NODE_VALUE_TYPE && prop.property === "font-family") { + let value = prop.value.toLowerCase(); + if (value !== "inherit" && value !== "unset" && value !== "initial") { + tooltipType = TOOLTIP_FONTFAMILY_TYPE; + } + } + + return tooltipType; + }, + + /** + * Executed by the tooltip when the pointer hovers over an element of the + * view. Used to decide whether the tooltip should be shown or not and to + * actually put content in it. + * Checks if the hovered target is a css value we support tooltips for. + * + * @param {DOMNode} target The currently hovered node + * @return {Promise} + */ + _onPreviewTooltipTargetHover: Task.async(function* (target) { + let nodeInfo = this.view.getNodeInfo(target); + if (!nodeInfo) { + // The hovered node isn't something we care about + return false; + } + + let type = this._getTooltipType(nodeInfo); + if (!type) { + // There is no tooltip type defined for the hovered node + return false; + } + + if (this.isRuleView && this.colorPicker.tooltip.isVisible()) { + this.colorPicker.revert(); + this.colorPicker.hide(); + } + + if (this.isRuleView && this.cubicBezier.tooltip.isVisible()) { + this.cubicBezier.revert(); + this.cubicBezier.hide(); + } + + if (this.isRuleView && this.cssDocs.tooltip.isVisible()) { + this.cssDocs.hide(); + } + + if (this.isRuleView && this.filterEditor.tooltip.isVisible()) { + this.filterEditor.revert(); + this.filterEdtior.hide(); + } + + let inspector = this.view.inspector; + + if (type === TOOLTIP_IMAGE_TYPE) { + try { + yield this._setImagePreviewTooltip(nodeInfo.value.url); + } catch (e) { + yield setBrokenImageTooltip(this.previewTooltip, this.view.inspector.panelDoc); + } + return true; + } + + if (type === TOOLTIP_FONTFAMILY_TYPE) { + let font = nodeInfo.value.value; + let nodeFront = inspector.selection.nodeFront; + yield this._setFontPreviewTooltip(font, nodeFront); + return true; + } + + return false; + }), + + /** + * Set the content of the preview tooltip to display an image preview. The image URL can + * be relative, a call will be made to the debuggee to retrieve the image content as an + * imageData URI. + * + * @param {String} imageUrl + * The image url value (may be relative or absolute). + * @return {Promise} A promise that resolves when the preview tooltip content is ready + */ + _setImagePreviewTooltip: Task.async(function* (imageUrl) { + let doc = this.view.inspector.panelDoc; + let maxDim = Services.prefs.getIntPref(PREF_IMAGE_TOOLTIP_SIZE); + + let naturalWidth, naturalHeight; + if (imageUrl.startsWith("data:")) { + // If the imageUrl already is a data-url, save ourselves a round-trip + let size = yield getImageDimensions(doc, imageUrl); + naturalWidth = size.naturalWidth; + naturalHeight = size.naturalHeight; + } else { + let inspectorFront = this.view.inspector.inspector; + let {data, size} = yield inspectorFront.getImageDataFromURL(imageUrl, maxDim); + imageUrl = yield data.string(); + naturalWidth = size.naturalWidth; + naturalHeight = size.naturalHeight; + } + + yield setImageTooltip(this.previewTooltip, doc, imageUrl, + {maxDim, naturalWidth, naturalHeight}); + }), + + /** + * Set the content of the preview tooltip to display a font family preview. + * + * @param {String} font + * The font family value. + * @param {object} nodeFront + * The NodeActor that will used to retrieve the dataURL for the font + * family tooltip contents. + * @return {Promise} A promise that resolves when the preview tooltip content is ready + */ + _setFontPreviewTooltip: Task.async(function* (font, nodeFront) { + if (!font || !nodeFront || typeof nodeFront.getFontFamilyDataURL !== "function") { + throw new Error("Unable to create font preview tooltip content."); + } + + font = font.replace(/"/g, "'"); + font = font.replace("!important", ""); + font = font.trim(); + + let fillStyle = getColor("body-color"); + let {data, size: maxDim} = yield nodeFront.getFontFamilyDataURL(font, fillStyle); + + let imageUrl = yield data.string(); + let doc = this.view.inspector.panelDoc; + let {naturalWidth, naturalHeight} = yield getImageDimensions(doc, imageUrl); + + yield setImageTooltip(this.previewTooltip, doc, imageUrl, + {hideDimensionLabel: true, maxDim, naturalWidth, naturalHeight}); + }), + + _onNewSelection: function () { + if (this.previewTooltip) { + this.previewTooltip.hide(); + } + + if (this.colorPicker) { + this.colorPicker.hide(); + } + + if (this.cubicBezier) { + this.cubicBezier.hide(); + } + + if (this.cssDocs) { + this.cssDocs.hide(); + } + + if (this.filterEditor) { + this.filterEditor.hide(); + } + }, + + /** + * Destroy this overlay instance, removing it from the view + */ + destroy: function () { + this.removeFromView(); + + this.view.inspector.selection.off("new-node-front", this._onNewSelection); + this.view = null; + + this._isDestroyed = true; + } +}; + +module.exports = TooltipsOverlay; diff --git a/devtools/client/inspector/shared/utils.js b/devtools/client/inspector/shared/utils.js new file mode 100644 index 000000000..60dda914c --- /dev/null +++ b/devtools/client/inspector/shared/utils.js @@ -0,0 +1,161 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {parseDeclarations} = require("devtools/shared/css/parsing-utils"); +const promise = require("promise"); +const {getCSSLexer} = require("devtools/shared/css/lexer"); +const {KeyCodes} = require("devtools/client/shared/keycodes"); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; + +/** + * Create a child element with a set of attributes. + * + * @param {Element} parent + * The parent node. + * @param {string} tagName + * The tag name. + * @param {object} attributes + * A set of attributes to set on the node. + */ +function createChild(parent, tagName, attributes = {}) { + let elt = parent.ownerDocument.createElementNS(HTML_NS, tagName); + for (let attr in attributes) { + if (attributes.hasOwnProperty(attr)) { + if (attr === "textContent") { + elt.textContent = attributes[attr]; + } else if (attr === "child") { + elt.appendChild(attributes[attr]); + } else { + elt.setAttribute(attr, attributes[attr]); + } + } + } + parent.appendChild(elt); + return elt; +} + +exports.createChild = createChild; + +/** + * Append a text node to an element. + * + * @param {Element} parent + * The parent node. + * @param {string} text + * The text content for the text node. + */ +function appendText(parent, text) { + parent.appendChild(parent.ownerDocument.createTextNode(text)); +} + +exports.appendText = appendText; + +/** + * Called when a character is typed in a value editor. This decides + * whether to advance or not, first by checking to see if ";" was + * typed, and then by lexing the input and seeing whether the ";" + * would be a terminator at this point. + * + * @param {number} keyCode + * Key code to be checked. + * @param {string} aValue + * Current text editor value. + * @param {number} insertionPoint + * The index of the insertion point. + * @return {Boolean} True if the focus should advance; false if + * the character should be inserted. + */ +function advanceValidate(keyCode, value, insertionPoint) { + // Only ";" has special handling here. + if (keyCode !== KeyCodes.DOM_VK_SEMICOLON) { + return false; + } + + // Insert the character provisionally and see what happens. If we + // end up with a ";" symbol token, then the semicolon terminates the + // value. Otherwise it's been inserted in some spot where it has a + // valid meaning, like a comment or string. + value = value.slice(0, insertionPoint) + ";" + value.slice(insertionPoint); + let lexer = getCSSLexer(value); + while (true) { + let token = lexer.nextToken(); + if (token.endOffset > insertionPoint) { + if (token.tokenType === "symbol" && token.text === ";") { + // The ";" is a terminator. + return true; + } + // The ";" is not a terminator in this context. + break; + } + } + return false; +} + +exports.advanceValidate = advanceValidate; + +/** + * Create a throttling function wrapper to regulate its frequency. + * + * @param {Function} func + * The function to throttle + * @param {number} wait + * The throttling period + * @param {Object} scope + * The scope to use for func + * @return {Function} The throttled function + */ +function throttle(func, wait, scope) { + let timer = null; + + return function () { + if (timer) { + clearTimeout(timer); + } + + let args = arguments; + timer = setTimeout(function () { + timer = null; + func.apply(scope, args); + }, wait); + }; +} + +exports.throttle = throttle; + +/** + * Event handler that causes a blur on the target if the input has + * multiple CSS properties as the value. + */ +function blurOnMultipleProperties(cssProperties) { + return (e) => { + setTimeout(() => { + let props = parseDeclarations(cssProperties.isKnown, e.target.value); + if (props.length > 1) { + e.target.blur(); + } + }, 0); + }; +} + +exports.blurOnMultipleProperties = blurOnMultipleProperties; + +/** + * Log the provided error to the console and return a rejected Promise for + * this error. + * + * @param {Error} error + * The error to log + * @return {Promise} A rejected promise + */ +function promiseWarn(error) { + console.error(error); + return promise.reject(error); +} + +exports.promiseWarn = promiseWarn; diff --git a/devtools/client/inspector/test/.eslintrc.js b/devtools/client/inspector/test/.eslintrc.js new file mode 100644 index 000000000..8d15a76d9 --- /dev/null +++ b/devtools/client/inspector/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../.eslintrc.mochitests.js" +}; diff --git a/devtools/client/inspector/test/browser.ini b/devtools/client/inspector/test/browser.ini new file mode 100644 index 000000000..65ad71c0c --- /dev/null +++ b/devtools/client/inspector/test/browser.ini @@ -0,0 +1,172 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + doc_inspector_add_node.html + doc_inspector_breadcrumbs.html + doc_inspector_breadcrumbs_visibility.html + doc_inspector_csp.html + doc_inspector_csp.html^headers^ + doc_inspector_delete-selected-node-01.html + doc_inspector_delete-selected-node-02.html + doc_inspector_embed.html + doc_inspector_gcli-inspect-command.html + doc_inspector_highlight_after_transition.html + doc_inspector_highlighter-comments.html + doc_inspector_highlighter-geometry_01.html + doc_inspector_highlighter-geometry_02.html + doc_inspector_highlighter_csstransform.html + doc_inspector_highlighter_dom.html + doc_inspector_highlighter_inline.html + doc_inspector_highlighter.html + doc_inspector_highlighter_rect.html + doc_inspector_highlighter_rect_iframe.html + doc_inspector_highlighter_xbl.xul + doc_inspector_infobar_01.html + doc_inspector_infobar_02.html + doc_inspector_infobar_03.html + doc_inspector_infobar_textnode.html + doc_inspector_long-divs.html + doc_inspector_menu.html + doc_inspector_outerhtml.html + doc_inspector_remove-iframe-during-load.html + doc_inspector_search.html + doc_inspector_search-reserved.html + doc_inspector_search-suggestions.html + doc_inspector_search-svg.html + doc_inspector_select-last-selected-01.html + doc_inspector_select-last-selected-02.html + doc_inspector_svg.svg + head.js + shared-head.js + !/devtools/client/commandline/test/helpers.js + !/devtools/client/framework/test/shared-head.js + !/devtools/client/shared/test/test-actor.js + !/devtools/client/shared/test/test-actor-registry.js + +[browser_inspector_addNode_01.js] +[browser_inspector_addNode_02.js] +[browser_inspector_addNode_03.js] +[browser_inspector_addSidebarTab.js] +[browser_inspector_breadcrumbs.js] +[browser_inspector_breadcrumbs_highlight_hover.js] +[browser_inspector_breadcrumbs_keybinding.js] +[browser_inspector_breadcrumbs_keyboard_trap.js] +skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences +[browser_inspector_breadcrumbs_mutations.js] +[browser_inspector_breadcrumbs_namespaced.js] +[browser_inspector_breadcrumbs_visibility.js] +[browser_inspector_delete-selected-node-01.js] +[browser_inspector_delete-selected-node-02.js] +[browser_inspector_delete-selected-node-03.js] +[browser_inspector_destroy-after-navigation.js] +[browser_inspector_destroy-before-ready.js] +[browser_inspector_expand-collapse.js] +[browser_inspector_gcli-inspect-command.js] +[browser_inspector_highlighter-01.js] +[browser_inspector_highlighter-02.js] +[browser_inspector_highlighter-03.js] +[browser_inspector_highlighter-04.js] +[browser_inspector_highlighter-by-type.js] +[browser_inspector_highlighter-cancel.js] +[browser_inspector_highlighter-comments.js] +[browser_inspector_highlighter-cssgrid_01.js] +[browser_inspector_highlighter-csstransform_01.js] +[browser_inspector_highlighter-csstransform_02.js] +[browser_inspector_highlighter-embed.js] +[browser_inspector_highlighter-eyedropper-clipboard.js] +subsuite = clipboard +[browser_inspector_highlighter-eyedropper-csp.js] +[browser_inspector_highlighter-eyedropper-events.js] +[browser_inspector_highlighter-eyedropper-label.js] +[browser_inspector_highlighter-eyedropper-show-hide.js] +[browser_inspector_highlighter-eyedropper-xul.js] +[browser_inspector_highlighter-geometry_01.js] +[browser_inspector_highlighter-geometry_02.js] +[browser_inspector_highlighter-geometry_03.js] +[browser_inspector_highlighter-geometry_04.js] +[browser_inspector_highlighter-geometry_05.js] +[browser_inspector_highlighter-geometry_06.js] +[browser_inspector_highlighter-hover_01.js] +[browser_inspector_highlighter-hover_02.js] +[browser_inspector_highlighter-hover_03.js] +[browser_inspector_highlighter-iframes_01.js] +[browser_inspector_highlighter-iframes_02.js] +[browser_inspector_highlighter-inline.js] +[browser_inspector_highlighter-keybinding_01.js] +[browser_inspector_highlighter-keybinding_02.js] +[browser_inspector_highlighter-keybinding_03.js] +[browser_inspector_highlighter-keybinding_04.js] +[browser_inspector_highlighter-measure_01.js] +[browser_inspector_highlighter-measure_02.js] +[browser_inspector_highlighter-options.js] +[browser_inspector_highlighter-preview.js] +[browser_inspector_highlighter-rect_01.js] +[browser_inspector_highlighter-rect_02.js] +[browser_inspector_highlighter-rulers_01.js] +[browser_inspector_highlighter-rulers_02.js] +[browser_inspector_highlighter-selector_01.js] +[browser_inspector_highlighter-selector_02.js] +[browser_inspector_highlighter-xbl.js] +[browser_inspector_highlighter-zoom.js] +[browser_inspector_iframe-navigation.js] +[browser_inspector_infobar_01.js] +[browser_inspector_infobar_02.js] +[browser_inspector_infobar_03.js] +[browser_inspector_infobar_textnode.js] +[browser_inspector_initialization.js] +skip-if = (e10s && debug) # Bug 1250058 - Docshell leak on debug e10s +[browser_inspector_inspect-object-element.js] +[browser_inspector_invalidate.js] +[browser_inspector_keyboard-shortcuts-copy-outerhtml.js] +subsuite = clipboard +[browser_inspector_keyboard-shortcuts.js] +[browser_inspector_menu-01-sensitivity.js] +subsuite = clipboard +[browser_inspector_menu-02-copy-items.js] +subsuite = clipboard +[browser_inspector_menu-03-paste-items.js] +subsuite = clipboard +[browser_inspector_menu-03-paste-items-svg.js] +subsuite = clipboard +[browser_inspector_menu-04-use-in-console.js] +[browser_inspector_menu-05-attribute-items.js] +[browser_inspector_menu-06-other.js] +[browser_inspector_navigation.js] +[browser_inspector_navigate_to_errors.js] +[browser_inspector_open_on_neterror.js] +[browser_inspector_pane-toggle-01.js] +[browser_inspector_pane-toggle-02.js] +[browser_inspector_pane-toggle-03.js] +[browser_inspector_pane-toggle-05.js] +skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard +[browser_inspector_picker-stop-on-destroy.js] +[browser_inspector_picker-stop-on-tool-change.js] +[browser_inspector_portrait_mode.js] +[browser_inspector_pseudoclass-lock.js] +[browser_inspector_pseudoclass-menu.js] +[browser_inspector_reload-01.js] +[browser_inspector_reload-02.js] +[browser_inspector_remove-iframe-during-load.js] +[browser_inspector_search-01.js] +[browser_inspector_search-02.js] +[browser_inspector_search-03.js] +[browser_inspector_search-04.js] +[browser_inspector_search-05.js] +[browser_inspector_search-06.js] +[browser_inspector_search-07.js] +[browser_inspector_search-08.js] +[browser_inspector_search-clear.js] +[browser_inspector_search-filter_context-menu.js] +subsuite = clipboard +[browser_inspector_search_keyboard_trap.js] +[browser_inspector_search-label.js] +[browser_inspector_search-reserved.js] +[browser_inspector_search-selection.js] +[browser_inspector_search-sidebar.js] +[browser_inspector_select-docshell.js] +[browser_inspector_select-last-selected.js] +[browser_inspector_search-navigation.js] +[browser_inspector_sidebarstate.js] +[browser_inspector_switch-to-inspector-on-pick.js] +[browser_inspector_textbox-menu.js] diff --git a/devtools/client/inspector/test/browser_inspector_addNode_01.js b/devtools/client/inspector/test/browser_inspector_addNode_01.js new file mode 100644 index 000000000..f90cb6c5c --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_addNode_01.js @@ -0,0 +1,22 @@ +/* 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 the add node button and context menu items are present in the UI. + +const TEST_URL = "data:text/html;charset=utf-8,<h1>Add node</h1>"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + let {panelDoc} = inspector; + + let allMenuItems = openContextMenuAndGetAllItems(inspector); + let menuItem = allMenuItems.find(item => item.id === "node-menu-add"); + ok(menuItem, "The item is in the menu"); + + let toolbarButton = + panelDoc.querySelector("#inspector-toolbar #inspector-element-add-button"); + ok(toolbarButton, "The add button is in the toolbar"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_addNode_02.js b/devtools/client/inspector/test/browser_inspector_addNode_02.js new file mode 100644 index 000000000..2421f9df3 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_addNode_02.js @@ -0,0 +1,63 @@ +/* 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 the add node button and context menu items have the right state +// depending on the current selection. + +const TEST_URL = URL_ROOT + "doc_inspector_add_node.html"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + info("Select the DOCTYPE element"); + let {nodes} = yield inspector.walker.children(inspector.walker.rootNode); + yield selectNode(nodes[0], inspector); + assertState(false, inspector, + "The button and item are disabled on DOCTYPE"); + + info("Select the ::before pseudo-element"); + let body = yield getNodeFront("body", inspector); + ({nodes} = yield inspector.walker.children(body)); + yield selectNode(nodes[0], inspector); + assertState(false, inspector, + "The button and item are disabled on a pseudo-element"); + + info("Select the svg element"); + yield selectNode("svg", inspector); + assertState(false, inspector, + "The button and item are disabled on a SVG element"); + + info("Select the div#foo element"); + yield selectNode("#foo", inspector); + assertState(true, inspector, + "The button and item are enabled on a DIV element"); + + info("Select the documentElement element (html)"); + yield selectNode("html", inspector); + assertState(false, inspector, + "The button and item are disabled on the documentElement"); + + info("Select the iframe element"); + yield selectNode("iframe", inspector); + assertState(false, inspector, + "The button and item are disabled on an IFRAME element"); +}); + +function assertState(isEnabled, inspector, desc) { + let doc = inspector.panelDoc; + let btn = doc.querySelector("#inspector-element-add-button"); + + // Force an update of the context menu to make sure menu items are updated + // according to the current selection. This normally happens when the menu is + // opened, but for the sake of this test's simplicity, we directly call the + // private update function instead. + let allMenuItems = openContextMenuAndGetAllItems(inspector); + let menuItem = allMenuItems.find(item => item.id === "node-menu-add"); + ok(menuItem, "The item is in the menu"); + is(!menuItem.disabled, isEnabled, desc); + + is(!btn.hasAttribute("disabled"), isEnabled, desc); +} diff --git a/devtools/client/inspector/test/browser_inspector_addNode_03.js b/devtools/client/inspector/test/browser_inspector_addNode_03.js new file mode 100644 index 000000000..38a8369ec --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_addNode_03.js @@ -0,0 +1,84 @@ +/* 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 nodes does work as expected: the parent gets expanded, the +// new node gets selected. + +const TEST_URL = URL_ROOT + "doc_inspector_add_node.html"; +const PARENT_TREE_LEVEL = 3; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + info("Adding in element that has no children and is collapsed"); + let parentNode = yield getNodeFront("#foo", inspector); + yield selectNode(parentNode, inspector); + yield testAddNode(parentNode, inspector); + + info("Adding in element with children but that has not been expanded yet"); + parentNode = yield getNodeFront("#bar", inspector); + yield selectNode(parentNode, inspector); + yield testAddNode(parentNode, inspector); + + info("Adding in element with children that has been expanded then collapsed"); + // Select again #bar and collapse it. + parentNode = yield getNodeFront("#bar", inspector); + yield selectNode(parentNode, inspector); + collapseNode(parentNode, inspector); + yield testAddNode(parentNode, inspector); + + info("Adding in element with children that is expanded"); + parentNode = yield getNodeFront("#bar", inspector); + yield selectNode(parentNode, inspector); + yield testAddNode(parentNode, inspector); +}); + +function* testAddNode(parentNode, inspector) { + let btn = inspector.panelDoc.querySelector("#inspector-element-add-button"); + let markupWindow = inspector.markup.win; + let parentContainer = inspector.markup.getContainer(parentNode); + + is(parentContainer.tagLine.getAttribute("aria-level"), PARENT_TREE_LEVEL, + "Parent level should be up to date."); + + info("Clicking 'add node' and expecting a markup mutation and focus event"); + let onMutation = inspector.once("markupmutation"); + btn.click(); + let mutations = yield onMutation; + + info("Expecting an inspector-updated event right after the mutation event " + + "to wait for the new node selection"); + yield inspector.once("inspector-updated"); + + is(mutations.length, 1, "There is one mutation only"); + is(mutations[0].added.length, 1, "There is one new node only"); + + let newNode = mutations[0].added[0]; + + is(newNode, inspector.selection.nodeFront, + "The new node is selected"); + + ok(parentContainer.expanded, "The parent node is now expanded"); + + is(inspector.selection.nodeFront.parentNode(), parentNode, + "The new node is inside the right parent"); + + let focusedElement = markupWindow.document.activeElement; + let focusedContainer = focusedElement.container; + let selectedContainer = inspector.markup._selectedContainer; + is(selectedContainer.tagLine.getAttribute("aria-level"), + PARENT_TREE_LEVEL + 1, "Added container level should be up to date."); + is(selectedContainer.node, inspector.selection.nodeFront, + "The right container is selected in the markup-view"); + ok(selectedContainer.selected, "Selected container is set to selected"); + is(focusedContainer.toString(), "[root container]", + "Root container is focused"); +} + +function collapseNode(node, inspector) { + let container = inspector.markup.getContainer(node); + container.setExpanded(false); +} diff --git a/devtools/client/inspector/test/browser_inspector_addSidebarTab.js b/devtools/client/inspector/test/browser_inspector_addSidebarTab.js new file mode 100644 index 000000000..77dc2632e --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_addSidebarTab.js @@ -0,0 +1,62 @@ +/* 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"; + +const TEST_URI = "data:text/html;charset=UTF-8," + + "<h1>browser_inspector_addtabbar.js</h1>"; + +const CONTENT_TEXT = "Hello World!"; + +/** + * Verify InspectorPanel.addSidebarTab() API that can be consumed + * by DevTools extensions as well as DevTools code base. + */ +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URI); + + const React = inspector.React; + const { div } = React.DOM; + + info("Adding custom panel."); + + // Define custom side-panel. + let tabPanel = React.createFactory(React.createClass({ + displayName: "myTabPanel", + render: function () { + return ( + div({className: "my-tab-panel"}, + CONTENT_TEXT + ) + ); + } + })); + + // Append custom panel (tab) into the Inspector panel and + // make sure it's selected by default (the last arg = true). + inspector.addSidebarTab("myPanel", "My Panel", tabPanel, true); + is(inspector.sidebar.getCurrentTabID(), "myPanel", + "My Panel is selected by default"); + + // Define another custom side-panel. + tabPanel = React.createFactory(React.createClass({ + displayName: "myTabPanel2", + render: function () { + return ( + div({className: "my-tab-panel2"}, + "Another Content" + ) + ); + } + })); + + // Append second panel, but don't select it by default. + inspector.addSidebarTab("myPanel", "My Panel", tabPanel, false); + is(inspector.sidebar.getCurrentTabID(), "myPanel", + "My Panel is selected by default"); + + // Check the the panel content is properly rendered. + let tabPanelNode = inspector.panelDoc.querySelector(".my-tab-panel"); + is(tabPanelNode.textContent, CONTENT_TEXT, + "Side panel content has been rendered."); +}); diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs.js new file mode 100644 index 000000000..e5befff9e --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs.js @@ -0,0 +1,132 @@ +/* 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 breadcrumbs widget content is correct. + +const TEST_URI = URL_ROOT + "doc_inspector_breadcrumbs.html"; +const NODES = [ + {selector: "#i1111", ids: "i1 i11 i111 i1111", nodeName: "div", + title: "div#i1111"}, + {selector: "#i22", ids: "i2 i22", nodeName: "div", + title: "div#i22"}, + {selector: "#i2111", ids: "i2 i21 i211 i2111", nodeName: "div", + title: "div#i2111"}, + {selector: "#i21", ids: "i2 i21 i211 i2111", nodeName: "div", + title: "div#i21"}, + {selector: "#i22211", ids: "i2 i22 i222 i2221 i22211", nodeName: "div", + title: "div#i22211"}, + {selector: "#i22", ids: "i2 i22 i222 i2221 i22211", nodeName: "div", + title: "div#i22"}, + {selector: "#i3", ids: "i3", nodeName: "article", + title: "article#i3"}, + {selector: "clipPath", ids: "vector clip", nodeName: "clipPath", + title: "clipPath#clip"}, +]; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URI); + let breadcrumbs = inspector.panelDoc.getElementById("inspector-breadcrumbs"); + let container = breadcrumbs.querySelector(".html-arrowscrollbox-inner"); + + for (let node of NODES) { + info("Testing node " + node.selector); + + info("Selecting node and waiting for breadcrumbs to update"); + let breadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + yield selectNode(node.selector, inspector); + yield breadcrumbsUpdated; + + info("Performing checks for node " + node.selector); + let buttonsLabelIds = node.ids.split(" "); + + // html > body > … + is(container.childNodes.length, buttonsLabelIds.length + 2, + "Node " + node.selector + ": Items count"); + + for (let i = 2; i < container.childNodes.length; i++) { + let expectedId = "#" + buttonsLabelIds[i - 2]; + let button = container.childNodes[i]; + let labelId = button.querySelector(".breadcrumbs-widget-item-id"); + is(labelId.textContent, expectedId, + "Node " + node.selector + ": button " + i + " matches"); + } + + let checkedButton = container.querySelector("button[checked]"); + let labelId = checkedButton.querySelector(".breadcrumbs-widget-item-id"); + let id = inspector.selection.nodeFront.id; + is(labelId.textContent, "#" + id, + "Node " + node.selector + ": selection matches"); + + let labelTag = checkedButton.querySelector(".breadcrumbs-widget-item-tag"); + is(labelTag.textContent, node.nodeName, + "Node " + node.selector + " has the expected tag name"); + + is(checkedButton.getAttribute("title"), node.title, + "Node " + node.selector + " has the expected tooltip"); + } + + yield testPseudoElements(inspector, container); + yield testComments(inspector, container); +}); + +function* testPseudoElements(inspector, container) { + info("Checking for pseudo elements"); + + let pseudoParent = yield getNodeFront("#pseudo-container", inspector); + let children = yield inspector.walker.children(pseudoParent); + is(children.nodes.length, 2, "Pseudo children returned from walker"); + + let beforeElement = children.nodes[0]; + let breadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + yield selectNode(beforeElement, inspector); + yield breadcrumbsUpdated; + is(container.childNodes[3].textContent, "::before", + "::before shows up in breadcrumb"); + + let afterElement = children.nodes[1]; + breadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + yield selectNode(afterElement, inspector); + yield breadcrumbsUpdated; + is(container.childNodes[3].textContent, "::after", + "::before shows up in breadcrumb"); +} + +function* testComments(inspector, container) { + info("Checking for comment elements"); + + let breadcrumbs = inspector.breadcrumbs; + let checkedButtonIndex = 2; + let button = container.childNodes[checkedButtonIndex]; + + let onBreadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + button.click(); + yield onBreadcrumbsUpdated; + + is(breadcrumbs.currentIndex, checkedButtonIndex, "New button is selected"); + ok(breadcrumbs.outer.hasAttribute("aria-activedescendant"), + "Active descendant must be set"); + + let comment = [...inspector.markup._containers].find(([node]) => + node.nodeType === Ci.nsIDOMNode.COMMENT_NODE)[0]; + + let onInspectorUpdated = inspector.once("inspector-updated"); + inspector.selection.setNodeFront(comment); + yield onInspectorUpdated; + + is(breadcrumbs.currentIndex, -1, + "When comment is selected no breadcrumb should be checked"); + ok(!breadcrumbs.outer.hasAttribute("aria-activedescendant"), + "Active descendant must not be set"); + + onInspectorUpdated = inspector.once("inspector-updated"); + onBreadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + button.click(); + yield Promise.all([onInspectorUpdated, onBreadcrumbsUpdated]); + + is(breadcrumbs.currentIndex, checkedButtonIndex, + "Same button is selected again"); + ok(breadcrumbs.outer.hasAttribute("aria-activedescendant"), + "Active descendant must be set again"); +} diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_highlight_hover.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_highlight_hover.js new file mode 100644 index 000000000..6714ea35e --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_highlight_hover.js @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that hovering over nodes on the breadcrumb buttons in the inspector +// shows the highlighter over those nodes +add_task(function* () { + info("Loading the test document and opening the inspector"); + let {toolbox, inspector, testActor} = yield openInspectorForURL( + "data:text/html;charset=utf-8,<h1>foo</h1><span>bar</span>"); + info("Selecting the test node"); + yield selectNode("span", inspector); + let bcButtons = inspector.breadcrumbs.container; + + let onNodeHighlighted = toolbox.once("node-highlight"); + let button = bcButtons.childNodes[1]; + EventUtils.synthesizeMouseAtCenter(button, {type: "mousemove"}, + button.ownerDocument.defaultView); + yield onNodeHighlighted; + + let isVisible = yield testActor.isHighlighting(); + ok(isVisible, "The highlighter is shown on a markup container hover"); + + ok((yield testActor.assertHighlightedNode("body")), + "The highlighter highlights the right node"); + + let onNodeUnhighlighted = toolbox.once("node-unhighlight"); + // move outside of the breadcrumb trail to trigger unhighlight + EventUtils.synthesizeMouseAtCenter(inspector.addNodeButton, + {type: "mousemove"}, + inspector.addNodeButton.ownerDocument.defaultView); + yield onNodeUnhighlighted; + + onNodeHighlighted = toolbox.once("node-highlight"); + button = bcButtons.childNodes[2]; + EventUtils.synthesizeMouseAtCenter(button, {type: "mousemove"}, + button.ownerDocument.defaultView); + yield onNodeHighlighted; + + isVisible = yield testActor.isHighlighting(); + ok(isVisible, "The highlighter is shown on a markup container hover"); + + ok((yield testActor.assertHighlightedNode("span")), + "The highlighter highlights the right node"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_keybinding.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keybinding.js new file mode 100644 index 000000000..8e72a8bab --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keybinding.js @@ -0,0 +1,71 @@ +/* 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 breadcrumbs keybindings work. + +const TEST_URI = URL_ROOT + "doc_inspector_breadcrumbs.html"; +const TEST_DATA = [{ + desc: "Pressing left should select the parent <body>", + key: "VK_LEFT", + newSelection: "body" +}, { + desc: "Pressing left again should select the parent <html>", + key: "VK_LEFT", + newSelection: "html" +}, { + desc: "Pressing left again should stay on <html>, it's the first element", + key: "VK_LEFT", + newSelection: "html" +}, { + desc: "Pressing right should go to <body>", + key: "VK_RIGHT", + newSelection: "body" +}, { + desc: "Pressing right again should go to #i2", + key: "VK_RIGHT", + newSelection: "#i2" +}, { + desc: "Pressing right again should stay on #i2, it's the last element", + key: "VK_RIGHT", + newSelection: "#i2" +}]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URI); + + info("Selecting the test node"); + yield selectNode("#i2", inspector); + + info("Clicking on the corresponding breadcrumbs node to focus it"); + let container = inspector.panelDoc.getElementById("inspector-breadcrumbs"); + + let button = container.querySelector("button[checked]"); + button.click(); + + let currentSelection = "#id2"; + for (let {desc, key, newSelection} of TEST_DATA) { + info(desc); + + // If the selection will change, wait for the breadcrumb to update, + // otherwise continue. + let onUpdated = null; + if (newSelection !== currentSelection) { + info("Expecting a new node to be selected"); + onUpdated = inspector.once("breadcrumbs-updated"); + } + + EventUtils.synthesizeKey(key, {}); + yield onUpdated; + + let newNodeFront = yield getNodeFront(newSelection, inspector); + is(newNodeFront, inspector.selection.nodeFront, + "The current selection is correct"); + is(container.getAttribute("aria-activedescendant"), + container.querySelector("button[checked]").id, + "aria-activedescendant is set correctly"); + + currentSelection = newSelection; + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js new file mode 100644 index 000000000..16c70650b --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js @@ -0,0 +1,83 @@ +/* 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 ability to tab to and away from breadcrumbs using keyboard. + +const TEST_URL = URL_ROOT + "doc_inspector_breadcrumbs.html"; + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * focused {Boolean} flag, indicating if breadcrumbs contain focus + * key {String} key event's key + * options {?Object} optional event data such as shiftKey, etc + * } + */ +const TEST_DATA = [ + { + desc: "Move the focus away from breadcrumbs to a next focusable element", + focused: false, + key: "VK_TAB", + options: { } + }, + { + desc: "Move the focus back to the breadcrumbs", + focused: true, + key: "VK_TAB", + options: { shiftKey: true } + }, + { + desc: "Move the focus back away from breadcrumbs to a previous focusable " + + "element", + focused: false, + key: "VK_TAB", + options: { shiftKey: true } + }, + { + desc: "Move the focus back to the breadcrumbs", + focused: true, + key: "VK_TAB", + options: { } + } +]; + +add_task(function* () { + let { toolbox, inspector } = yield openInspectorForURL(TEST_URL); + let doc = inspector.panelDoc; + let {breadcrumbs} = inspector; + + yield selectNode("#i2", inspector); + + info("Clicking on the corresponding breadcrumbs node to focus it"); + let container = doc.getElementById("inspector-breadcrumbs"); + + let button = container.querySelector("button[checked]"); + let onHighlight = toolbox.once("node-highlight"); + button.click(); + yield onHighlight; + + // Ensure a breadcrumb is focused. + is(doc.activeElement, container, "Focus is on selected breadcrumb"); + is(container.getAttribute("aria-activedescendant"), button.id, + "aria-activedescendant is set correctly"); + + for (let { desc, focused, key, options } of TEST_DATA) { + info(desc); + + EventUtils.synthesizeKey(key, options); + // Wait until the keyPromise promise resolves. + yield breadcrumbs.keyPromise; + + if (focused) { + is(doc.activeElement, container, "Focus is on selected breadcrumb"); + } else { + ok(!containsFocus(doc, container), "Focus is outside of breadcrumbs"); + } + is(container.getAttribute("aria-activedescendant"), button.id, + "aria-activedescendant is set correctly"); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_mutations.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_mutations.js new file mode 100644 index 000000000..100ee275a --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_mutations.js @@ -0,0 +1,212 @@ +/* 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 breadcrumbs widget refreshes correctly when there are markup +// mutations (and that it doesn't refresh when those mutations don't change its +// output). + +const TEST_URI = URL_ROOT + "doc_inspector_breadcrumbs.html"; + +// Each item in the TEST_DATA array is a test case that should contain the +// following properties: +// - desc {String} A description of this test case (will be logged). +// - setup {Function*} A generator function (can yield promises) that sets up +// the test case. Useful for selecting a node before starting the test. +// - run {Function*} A generator function (can yield promises) that runs the +// actual test case, i.e, mutates the content DOM to cause the breadcrumbs +// to refresh, or not. +// - shouldRefresh {Boolean} Once the `run` function has completed, and the test +// has detected that the page has changed, this boolean instructs the test to +// verify if the breadcrumbs has refreshed or not. +// - output {Array} A list of strings for the text that should be found in each +// button after the test has run. +const TEST_DATA = [{ + desc: "Adding a child at the end of the chain shouldn't change anything", + setup: function* (inspector) { + yield selectNode("#i1111", inspector); + }, + run: function* ({walker, selection}) { + yield walker.setInnerHTML(selection.nodeFront, "<b>test</b>"); + }, + shouldRefresh: false, + output: ["html", "body", "article#i1", "div#i11", "div#i111", "div#i1111"] +}, { + desc: "Updating an ID to an displayed element should refresh", + setup: function* () {}, + run: function* ({walker}) { + let node = yield walker.querySelector(walker.rootNode, "#i1"); + yield node.modifyAttributes([{ + attributeName: "id", + newValue: "i1-changed" + }]); + }, + shouldRefresh: true, + output: ["html", "body", "article#i1-changed", "div#i11", "div#i111", + "div#i1111"] +}, { + desc: "Updating an class to a displayed element should refresh", + setup: function* () {}, + run: function* ({walker}) { + let node = yield walker.querySelector(walker.rootNode, "body"); + yield node.modifyAttributes([{ + attributeName: "class", + newValue: "test-class" + }]); + }, + shouldRefresh: true, + output: ["html", "body.test-class", "article#i1-changed", "div#i11", + "div#i111", "div#i1111"] +}, { + desc: "Updating a non id/class attribute to a displayed element should not " + + "refresh", + setup: function* () {}, + run: function* ({walker}) { + let node = yield walker.querySelector(walker.rootNode, "#i11"); + yield node.modifyAttributes([{ + attributeName: "name", + newValue: "value" + }]); + }, + shouldRefresh: false, + output: ["html", "body.test-class", "article#i1-changed", "div#i11", + "div#i111", "div#i1111"] +}, { + desc: "Moving a child in an element that's not displayed should not refresh", + setup: function* () {}, + run: function* ({walker}) { + // Re-append #i1211 as a last child of #i2. + let parent = yield walker.querySelector(walker.rootNode, "#i2"); + let child = yield walker.querySelector(walker.rootNode, "#i211"); + yield walker.insertBefore(child, parent); + }, + shouldRefresh: false, + output: ["html", "body.test-class", "article#i1-changed", "div#i11", + "div#i111", "div#i1111"] +}, { + desc: "Moving an undisplayed child in a displayed element should not refresh", + setup: function* () {}, + run: function* ({walker}) { + // Re-append #i2 in body (move it to the end). + let parent = yield walker.querySelector(walker.rootNode, "body"); + let child = yield walker.querySelector(walker.rootNode, "#i2"); + yield walker.insertBefore(child, parent); + }, + shouldRefresh: false, + output: ["html", "body.test-class", "article#i1-changed", "div#i11", + "div#i111", "div#i1111"] +}, { + desc: "Updating attributes on an element that's not displayed should not " + + "refresh", + setup: function* () {}, + run: function* ({walker}) { + let node = yield walker.querySelector(walker.rootNode, "#i2"); + yield node.modifyAttributes([{ + attributeName: "id", + newValue: "i2-changed" + }, { + attributeName: "class", + newValue: "test-class" + }]); + }, + shouldRefresh: false, + output: ["html", "body.test-class", "article#i1-changed", "div#i11", + "div#i111", "div#i1111"] +}, { + desc: "Removing the currently selected node should refresh", + setup: function* (inspector) { + yield selectNode("#i2-changed", inspector); + }, + run: function* ({walker, selection}) { + yield walker.removeNode(selection.nodeFront); + }, + shouldRefresh: true, + output: ["html", "body.test-class"] +}, { + desc: "Changing the class of the currently selected node should refresh", + setup: function* () {}, + run: function* ({selection}) { + yield selection.nodeFront.modifyAttributes([{ + attributeName: "class", + newValue: "test-class-changed" + }]); + }, + shouldRefresh: true, + output: ["html", "body.test-class-changed"] +}, { + desc: "Changing the id of the currently selected node should refresh", + setup: function* () {}, + run: function* ({selection}) { + yield selection.nodeFront.modifyAttributes([{ + attributeName: "id", + newValue: "new-id" + }]); + }, + shouldRefresh: true, + output: ["html", "body#new-id.test-class-changed"] +}]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URI); + let breadcrumbs = inspector.panelDoc.getElementById("inspector-breadcrumbs"); + let container = breadcrumbs.querySelector(".html-arrowscrollbox-inner"); + let win = container.ownerDocument.defaultView; + + for (let {desc, setup, run, shouldRefresh, output} of TEST_DATA) { + info("Running test case: " + desc); + + info("Listen to markupmutation events from the inspector to know when a " + + "test case has completed"); + let onContentMutation = inspector.once("markupmutation"); + + info("Running setup"); + yield setup(inspector); + + info("Listen to mutations on the breadcrumbs container"); + let hasBreadcrumbsMutated = false; + let observer = new win.MutationObserver(mutations => { + // Only consider childList changes or tooltiptext/checked attributes + // changes. The rest may be mutations caused by the overflowing arrowbox. + for (let {type, attributeName} of mutations) { + let isChildList = type === "childList"; + let isAttributes = type === "attributes" && + (attributeName === "checked" || + attributeName === "tooltiptext"); + if (isChildList || isAttributes) { + hasBreadcrumbsMutated = true; + break; + } + } + }); + observer.observe(container, { + attributes: true, + childList: true, + subtree: true + }); + + info("Running the test case"); + yield run(inspector); + + info("Wait until the page has mutated"); + yield onContentMutation; + + if (shouldRefresh) { + info("The breadcrumbs is expected to refresh, so wait for it"); + yield inspector.once("inspector-updated"); + } else { + ok(!inspector._updateProgress, + "The breadcrumbs widget is not currently updating"); + } + + is(shouldRefresh, hasBreadcrumbsMutated, "Has the breadcrumbs refreshed?"); + observer.disconnect(); + + info("Check the output of the breadcrumbs widget"); + is(container.childNodes.length, output.length, "Correct number of buttons"); + for (let i = 0; i < container.childNodes.length; i++) { + is(output[i], container.childNodes[i].textContent, + "Text content for button " + i + " is correct"); + } + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_namespaced.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_namespaced.js new file mode 100644 index 000000000..0b14ef1b0 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_namespaced.js @@ -0,0 +1,55 @@ +/* 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 breadcrumbs widget content for namespaced elements is correct. + +const XHTML = ` + <!DOCTYPE html> + <html xmlns="http://www.w3.org/1999/xhtml" + xmlns:svg="http://www.w3.org/2000/svg"> + <body> + <svg:svg width="100" height="100"> + <svg:clipPath id="clip"> + <svg:rect id="rectangle" x="0" y="0" width="10" height="5"></svg:rect> + </svg:clipPath> + <svg:circle cx="0" cy="0" r="5"></svg:circle> + </svg:svg> + </body> + </html> +`; + +const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML); + +const NODES = [ + {selector: "clipPath", nodes: ["svg:svg", "svg:clipPath"], + nodeName: "svg:clipPath", title: "svg:clipPath#clip"}, + {selector: "circle", nodes: ["svg:svg", "svg:circle"], + nodeName: "svg:circle", title: "svg:circle"}, +]; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URI); + let container = inspector.panelDoc.getElementById("inspector-breadcrumbs"); + + for (let node of NODES) { + info("Testing node " + node.selector); + + info("Selecting node and waiting for breadcrumbs to update"); + let breadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + yield selectNode(node.selector, inspector); + yield breadcrumbsUpdated; + + info("Performing checks for node " + node.selector); + + let checkedButton = container.querySelector("button[checked]"); + + let labelTag = checkedButton.querySelector(".breadcrumbs-widget-item-tag"); + is(labelTag.textContent, node.nodeName, + "Node " + node.selector + " has the expected tag name"); + + is(checkedButton.getAttribute("title"), node.title, + "Node " + node.selector + " has the expected tooltip"); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_breadcrumbs_visibility.js b/devtools/client/inspector/test/browser_inspector_breadcrumbs_visibility.js new file mode 100644 index 000000000..caee745c9 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_visibility.js @@ -0,0 +1,110 @@ +/* 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 start and end buttons on the breadcrumb trail bring the right +// crumbs into the visible area, for both LTR and RTL + +let { Toolbox } = require("devtools/client/framework/toolbox"); + +const TEST_URI = URL_ROOT + "doc_inspector_breadcrumbs_visibility.html"; +const NODE_ONE = "div#aVeryLongIdToExceedTheBreadcrumbTruncationLimit"; +const NODE_TWO = "div#anotherVeryLongIdToExceedTheBreadcrumbTruncationLimit"; +const NODE_THREE = "div#aThirdVeryLongIdToExceedTheTruncationLimit"; +const NODE_FOUR = "div#aFourthOneToExceedTheTruncationLimit"; +const NODE_FIVE = "div#aFifthOneToExceedTheTruncationLimit"; +const NODE_SIX = "div#aSixthOneToExceedTheTruncationLimit"; +const NODE_SEVEN = "div#aSeventhOneToExceedTheTruncationLimit"; + +const NODES = [ + { action: "start", title: NODE_SIX }, + { action: "start", title: NODE_FIVE }, + { action: "start", title: NODE_FOUR }, + { action: "start", title: NODE_THREE }, + { action: "start", title: NODE_TWO }, + { action: "start", title: NODE_ONE }, + { action: "end", title: NODE_TWO }, + { action: "end", title: NODE_THREE }, + { action: "end", title: NODE_FOUR }, + { action: "end", title: NODE_FIVE }, + { action: "end", title: NODE_SIX } +]; + +add_task(function* () { + // This test needs specific initial size of the sidebar. + yield pushPref("devtools.toolsidebar-width.inspector", 350); + yield pushPref("devtools.toolsidebar-height.inspector", 150); + + let { inspector, toolbox } = yield openInspectorForURL(TEST_URI); + + // No way to wait for scrolling to end (Bug 1172171) + // Rather than wait a max time; limit test to instant scroll behavior + inspector.breadcrumbs.arrowScrollBox.scrollBehavior = "instant"; + + yield toolbox.switchHost(Toolbox.HostType.WINDOW); + let hostWindow = toolbox.win.parent; + let originalWidth = hostWindow.outerWidth; + let originalHeight = hostWindow.outerHeight; + hostWindow.resizeTo(640, 300); + + info("Testing transitions ltr"); + yield pushPref("intl.uidirection.en-US", "ltr"); + yield testBreadcrumbTransitions(hostWindow, inspector); + + info("Testing transitions rtl"); + yield pushPref("intl.uidirection.en-US", "rtl"); + yield testBreadcrumbTransitions(hostWindow, inspector); + + hostWindow.resizeTo(originalWidth, originalHeight); +}); + +function* testBreadcrumbTransitions(hostWindow, inspector) { + let breadcrumbs = inspector.panelDoc.getElementById("inspector-breadcrumbs"); + let startBtn = breadcrumbs.querySelector(".scrollbutton-up"); + let endBtn = breadcrumbs.querySelector(".scrollbutton-down"); + let container = breadcrumbs.querySelector(".html-arrowscrollbox-inner"); + let breadcrumbsUpdated = inspector.once("breadcrumbs-updated"); + + info("Selecting initial node"); + yield selectNode(NODE_SEVEN, inspector); + + // So just need to wait for a duration + yield breadcrumbsUpdated; + let initialCrumb = container.querySelector("button[checked]"); + is(isElementInViewport(hostWindow, initialCrumb), true, + "initial element was visible"); + + for (let node of NODES) { + info("Checking for visibility of crumb " + node.title); + if (node.action === "end") { + info("Simulating click of end button"); + EventUtils.synthesizeMouseAtCenter(endBtn, {}, inspector.panelWin); + } else if (node.action === "start") { + info("Simulating click of start button"); + EventUtils.synthesizeMouseAtCenter(startBtn, {}, inspector.panelWin); + } + + yield breadcrumbsUpdated; + let selector = "button[title=\"" + node.title + "\"]"; + let relevantCrumb = container.querySelector(selector); + is(isElementInViewport(hostWindow, relevantCrumb), true, + node.title + " crumb is visible"); + } +} + +function isElementInViewport(window, el) { + let rect = el.getBoundingClientRect(); + + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= window.innerHeight && + rect.right <= window.innerWidth + ); +} + +registerCleanupFunction(function () { + // Restore the host type for other tests. + Services.prefs.clearUserPref("devtools.toolbox.host"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_delete-selected-node-01.js b/devtools/client/inspector/test/browser_inspector_delete-selected-node-01.js new file mode 100644 index 000000000..3b5049e25 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_delete-selected-node-01.js @@ -0,0 +1,24 @@ +/* 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 to ensure inspector handles deletion of selected node correctly. + +const TEST_URL = URL_ROOT + "doc_inspector_delete-selected-node-01.html"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + let span = yield getNodeFrontInFrame("span", "iframe", inspector); + yield selectNode(span, inspector); + + info("Removing selected <span> element."); + let parentNode = span.parentNode(); + yield inspector.walker.removeNode(span); + + // Wait for the inspector to process the mutation + yield inspector.once("inspector-updated"); + is(inspector.selection.nodeFront, parentNode, + "Parent node of selected <span> got selected."); +}); diff --git a/devtools/client/inspector/test/browser_inspector_delete-selected-node-02.js b/devtools/client/inspector/test/browser_inspector_delete-selected-node-02.js new file mode 100644 index 000000000..fbd008a89 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_delete-selected-node-02.js @@ -0,0 +1,154 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// Test that when nodes are being deleted in the page, the current selection +// and therefore the markup view, css rule view, computed view, font view, +// box model view, and breadcrumbs, reset accordingly to show the right node + +const TEST_PAGE = URL_ROOT + + "doc_inspector_delete-selected-node-02.html"; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_PAGE); + + yield testManuallyDeleteSelectedNode(); + yield testAutomaticallyDeleteSelectedNode(); + yield testDeleteSelectedNodeContainerFrame(); + yield testDeleteWithNonElementNode(); + + function* testManuallyDeleteSelectedNode() { + info("Selecting a node, deleting it via context menu and checking that " + + "its parent node is selected and breadcrumbs are updated."); + + yield deleteNodeWithContextMenu("#deleteManually"); + + info("Performing checks."); + yield assertNodeSelectedAndPanelsUpdated("#selectedAfterDelete", + "li#selectedAfterDelete"); + } + + function* testAutomaticallyDeleteSelectedNode() { + info("Selecting a node, deleting it via javascript and checking that " + + "its parent node is selected and breadcrumbs are updated."); + + let div = yield getNodeFront("#deleteAutomatically", inspector); + yield selectNode(div, inspector); + + info("Deleting selected node via javascript."); + yield inspector.walker.removeNode(div); + + info("Waiting for inspector to update."); + yield inspector.once("inspector-updated"); + + info("Inspector updated, performing checks."); + yield assertNodeSelectedAndPanelsUpdated("#deleteChildren", + "ul#deleteChildren"); + } + + function* testDeleteSelectedNodeContainerFrame() { + info("Selecting a node inside iframe, deleting the iframe via javascript " + + "and checking the parent node of the iframe is selected and " + + "breadcrumbs are updated."); + + info("Selecting an element inside iframe."); + let iframe = yield getNodeFront("#deleteIframe", inspector); + let div = yield getNodeFrontInFrame("#deleteInIframe", iframe, inspector); + yield selectNode(div, inspector); + + info("Deleting selected node via javascript."); + yield inspector.walker.removeNode(iframe); + + info("Waiting for inspector to update."); + yield inspector.once("inspector-updated"); + + info("Inspector updated, performing checks."); + yield assertNodeSelectedAndPanelsUpdated("body", "body"); + } + + function* testDeleteWithNonElementNode() { + info("Selecting a node, deleting it via context menu and checking that " + + "its parent node is selected and breadcrumbs are updated " + + "when the node is followed by a non-element node"); + + yield deleteNodeWithContextMenu("#deleteWithNonElement"); + + let expectedCrumbs = ["html", "body", "div#deleteToMakeSingleTextNode"]; + yield assertNodeSelectedAndCrumbsUpdated(expectedCrumbs, + Node.TEXT_NODE); + + // Delete node with key, as cannot delete text node with + // context menu at this time. + inspector.markup._frame.focus(); + EventUtils.synthesizeKey("VK_DELETE", {}); + yield inspector.once("inspector-updated"); + + expectedCrumbs = ["html", "body", "div#deleteToMakeSingleTextNode"]; + yield assertNodeSelectedAndCrumbsUpdated(expectedCrumbs, + Node.ELEMENT_NODE); + } + + function* deleteNodeWithContextMenu(selector) { + yield selectNode(selector, inspector); + let nodeToBeDeleted = inspector.selection.nodeFront; + + info("Getting the node container in the markup view."); + let container = yield getContainerForSelector(selector, inspector); + + let allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: container.tagLine, + }); + let menuItem = allMenuItems.find(item => item.id === "node-menu-delete"); + + info("Clicking 'Delete Node' in the context menu."); + is(menuItem.disabled, false, "delete menu item is enabled"); + menuItem.click(); + + // close the open context menu + EventUtils.synthesizeKey("VK_ESCAPE", {}); + + info("Waiting for inspector to update."); + yield inspector.once("inspector-updated"); + + // Since the mutations are sent asynchronously from the server, the + // inspector-updated event triggered by the deletion might happen before + // the mutation is received and the element is removed from the + // breadcrumbs. See bug 1284125. + if (inspector.breadcrumbs.indexOf(nodeToBeDeleted) > -1) { + info("Crumbs haven't seen deletion. Waiting for breadcrumbs-updated."); + yield inspector.once("breadcrumbs-updated"); + } + + return menuItem; + } + + function* assertNodeSelectedAndCrumbsUpdated(expectedCrumbs, + expectedNodeType) { + info("Performing checks"); + let actualNodeType = inspector.selection.nodeFront.nodeType; + is(actualNodeType, expectedNodeType, "The node has the right type"); + + let breadcrumbs = inspector.panelDoc.querySelectorAll( + "#inspector-breadcrumbs .html-arrowscrollbox-inner > *"); + is(breadcrumbs.length, expectedCrumbs.length, + "Have the correct number of breadcrumbs"); + for (let i = 0; i < breadcrumbs.length; i++) { + is(breadcrumbs[i].textContent, expectedCrumbs[i], + "Text content for button " + i + " is correct"); + } + } + + function* assertNodeSelectedAndPanelsUpdated(selector, crumbLabel) { + let nodeFront = yield getNodeFront(selector, inspector); + is(inspector.selection.nodeFront, nodeFront, "The right node is selected"); + + let breadcrumbs = inspector.panelDoc.querySelector( + "#inspector-breadcrumbs .html-arrowscrollbox-inner"); + is(breadcrumbs.querySelector("button[checked=true]").textContent, + crumbLabel, + "The right breadcrumb is selected"); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_delete-selected-node-03.js b/devtools/client/inspector/test/browser_inspector_delete-selected-node-03.js new file mode 100644 index 000000000..21057cdb6 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_delete-selected-node-03.js @@ -0,0 +1,27 @@ +/* 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 to ensure inspector can handle destruction of selected node inside an +// iframe. + +const TEST_URL = URL_ROOT + "doc_inspector_delete-selected-node-01.html"; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URL); + + let iframe = yield getNodeFront("iframe", inspector); + let node = yield getNodeFrontInFrame("span", iframe, inspector); + yield selectNode(node, inspector); + + info("Removing iframe."); + yield inspector.walker.removeNode(iframe); + yield inspector.selection.once("detached-front"); + + let body = yield getNodeFront("body", inspector); + + is(inspector.selection.nodeFront, body, "Selection is now the body node"); + + yield inspector.once("inspector-updated"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_destroy-after-navigation.js b/devtools/client/inspector/test/browser_inspector_destroy-after-navigation.js new file mode 100644 index 000000000..5fcd5538b --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_destroy-after-navigation.js @@ -0,0 +1,24 @@ +/* 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"; + +// Testing that closing the inspector after navigating to a page doesn't fail. + +const URL_1 = "data:text/plain;charset=UTF-8,abcde"; +const URL_2 = "data:text/plain;charset=UTF-8,12345"; + +add_task(function* () { + let { inspector, toolbox } = yield openInspectorForURL(URL_1); + + yield navigateTo(inspector, URL_2); + + info("Destroying toolbox"); + try { + yield toolbox.destroy(); + ok(true, "Toolbox destroyed"); + } catch (e) { + ok(false, "An exception occured while destroying toolbox"); + console.error(e); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_destroy-before-ready.js b/devtools/client/inspector/test/browser_inspector_destroy-before-ready.js new file mode 100644 index 000000000..ac8ad5d37 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_destroy-before-ready.js @@ -0,0 +1,26 @@ +/* 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 switching to the inspector panel and not waiting for it to be fully +// loaded doesn't fail the test with unhandled rejected promises. + +add_task(function* () { + // At least one assertion is needed to avoid failing the test, but really, + // what we're interested in is just having the test pass when switching to the + // inspector. + ok(true); + + yield addTab("data:text/html;charset=utf-8,test inspector destroy"); + + info("Open the toolbox on the debugger panel"); + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = yield gDevTools.showToolbox(target, "jsdebugger"); + + info("Switch to the inspector panel and immediately end the test"); + let onInspectorSelected = toolbox.once("inspector-selected"); + toolbox.selectTool("inspector"); + yield onInspectorSelected; +}); diff --git a/devtools/client/inspector/test/browser_inspector_expand-collapse.js b/devtools/client/inspector/test/browser_inspector_expand-collapse.js new file mode 100644 index 000000000..3b1dcb6b2 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_expand-collapse.js @@ -0,0 +1,64 @@ +/* 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"; + +// Tests that context menu items exapnd all and collapse are shown properly. + +const TEST_URL = "data:text/html;charset=utf-8," + + "<div id='parent-node'><div id='child-node'></div></div>"; + +add_task(function* () { + // Test is often exceeding time-out threshold, similar to Bug 1137765 + requestLongerTimeout(2); + + let {inspector} = yield openInspectorForURL(TEST_URL); + + info("Selecting the parent node"); + + let front = yield getNodeFrontForSelector("#parent-node", inspector); + + yield selectNode(front, inspector); + + info("Simulating context menu click on the selected node container."); + let allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: getContainerForNodeFront(front, inspector).tagLine, + }); + let nodeMenuCollapseElement = + allMenuItems.find(item => item.id === "node-menu-collapse"); + let nodeMenuExpandElement = + allMenuItems.find(item => item.id === "node-menu-expand"); + + ok(nodeMenuCollapseElement.disabled, "Collapse option is disabled"); + ok(!nodeMenuExpandElement.disabled, "ExpandAll option is enabled"); + + info("Testing whether expansion works properly"); + nodeMenuExpandElement.click(); + + info("Waiting for expansion to occur"); + yield waitForMultipleChildrenUpdates(inspector); + let markUpContainer = getContainerForNodeFront(front, inspector); + ok(markUpContainer.expanded, "node has been successfully expanded"); + + // reselecting node after expansion + yield selectNode(front, inspector); + + info("Testing whether collapse works properly"); + info("Simulating context menu click on the selected node container."); + allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: getContainerForNodeFront(front, inspector).tagLine, + }); + nodeMenuCollapseElement = + allMenuItems.find(item => item.id === "node-menu-collapse"); + nodeMenuExpandElement = + allMenuItems.find(item => item.id === "node-menu-expand"); + + ok(!nodeMenuCollapseElement.disabled, "Collapse option is enabled"); + ok(!nodeMenuExpandElement.disabled, "ExpandAll option is enabled"); + nodeMenuCollapseElement.click(); + + info("Waiting for collapse to occur"); + yield waitForMultipleChildrenUpdates(inspector); + ok(!markUpContainer.expanded, "node has been successfully collapsed"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_gcli-inspect-command.js b/devtools/client/inspector/test/browser_inspector_gcli-inspect-command.js new file mode 100644 index 000000000..dca8167c4 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_gcli-inspect-command.js @@ -0,0 +1,118 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint key-spacing: 0 */ +"use strict"; + +// Testing that the gcli 'inspect' command works as it should. + +const TEST_URI = URL_ROOT + "doc_inspector_gcli-inspect-command.html"; + +add_task(function* () { + return helpers.addTabWithToolbar(TEST_URI, Task.async(function* (options) { + let {inspector} = yield openInspector(); + + let checkSelection = Task.async(function* (selector) { + let node = yield getNodeFront(selector, inspector); + is(inspector.selection.nodeFront, node, "the current selection is correct"); + }); + + yield helpers.audit(options, [ + { + setup: "inspect", + check: { + input: "inspect", + hints: " <selector>", + markup: "VVVVVVV", + status: "ERROR", + args: { + selector: { + message: "Value required for \u2018selector\u2019." + }, + } + }, + }, + { + setup: "inspect div", + check: { + input: "inspect div", + hints: "", + markup: "VVVVVVVVVVV", + status: "VALID", + args: { + selector: { message: "" }, + } + }, + exec: {}, + post: () => checkSelection("div"), + }, + { + setup: "inspect .someclass", + check: { + input: "inspect .someclass", + hints: "", + markup: "VVVVVVVVVVVVVVVVVV", + status: "VALID", + args: { + selector: { message: "" }, + } + }, + exec: {}, + post: () => checkSelection(".someclass"), + }, + { + setup: "inspect #someid", + check: { + input: "inspect #someid", + hints: "", + markup: "VVVVVVVVVVVVVVV", + status: "VALID", + args: { + selector: { message: "" }, + } + }, + exec: {}, + post: () => checkSelection("#someid"), + }, + { + setup: "inspect button[disabled]", + check: { + input: "inspect button[disabled]", + hints: "", + markup: "VVVVVVVVVVVVVVVVVVVVVVVV", + status: "VALID", + args: { + selector: { message: "" }, + } + }, + exec: {}, + post: () => checkSelection("button[disabled]"), + }, + { + setup: "inspect p>strong", + check: { + input: "inspect p>strong", + hints: "", + markup: "VVVVVVVVVVVVVVVV", + status: "VALID", + args: { + selector: { message: "" }, + } + }, + exec: {}, + post: () => checkSelection("p>strong"), + }, + { + setup: "inspect :root", + check: { + input: "inspect :root", + hints: "", + markup: "VVVVVVVVVVVVV", + status: "VALID" + }, + exec: {}, + post: () => checkSelection(":root"), + }, + ]); + })); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-01.js b/devtools/client/inspector/test/browser_inspector_highlighter-01.js new file mode 100644 index 000000000..946b8c3c8 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-01.js @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that hovering over nodes in the markup-view shows the highlighter over +// those nodes +add_task(function* () { + info("Loading the test document and opening the inspector"); + let {toolbox, inspector, testActor} = yield openInspectorForURL( + "data:text/html;charset=utf-8,<h1>foo</h1><span>bar</span>"); + + let isVisible = yield testActor.isHighlighting(toolbox); + ok(!isVisible, "The highlighter is hidden by default"); + + info("Selecting the test node"); + yield selectNode("span", inspector); + let container = yield getContainerForSelector("h1", inspector); + + let onHighlighterReady = toolbox.once("highlighter-ready"); + EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousemove"}, + inspector.markup.doc.defaultView); + yield onHighlighterReady; + + isVisible = yield testActor.isHighlighting(); + ok(isVisible, "The highlighter is shown on a markup container hover"); + + ok((yield testActor.assertHighlightedNode("h1")), + "The highlighter highlights the right node"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-02.js b/devtools/client/inspector/test/browser_inspector_highlighter-02.js new file mode 100644 index 000000000..37eb9389e --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-02.js @@ -0,0 +1,39 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that the highlighter is correctly displayed over a variety of elements + +const TEST_URI = URL_ROOT + "doc_inspector_highlighter.html"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URI); + + info("Selecting the simple, non-transformed DIV"); + yield selectAndHighlightNode("#simple-div", inspector); + + let isVisible = yield testActor.isHighlighting(); + ok(isVisible, "The highlighter is shown"); + ok((yield testActor.assertHighlightedNode("#simple-div")), + "The highlighter's outline corresponds to the simple div"); + yield testActor.isNodeCorrectlyHighlighted("#simple-div", is, "non-zoomed"); + + info("Selecting the rotated DIV"); + yield selectAndHighlightNode("#rotated-div", inspector); + + isVisible = yield testActor.isHighlighting(); + ok(isVisible, "The highlighter is shown"); + yield testActor.isNodeCorrectlyHighlighted("#rotated-div", is, "rotated"); + + info("Selecting the zero width height DIV"); + yield selectAndHighlightNode("#widthHeightZero-div", inspector); + + isVisible = yield testActor.isHighlighting(); + ok(isVisible, "The highlighter is shown"); + yield testActor.isNodeCorrectlyHighlighted("#widthHeightZero-div", is, + "zero width height"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-03.js b/devtools/client/inspector/test/browser_inspector_highlighter-03.js new file mode 100644 index 000000000..344b5c6c8 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-03.js @@ -0,0 +1,70 @@ +/* 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 iframes are correctly highlighted. + +const IFRAME_SRC = "<style>" + + "body {" + + "margin:0;" + + "height:100%;" + + "background-color:red" + + "}" + + "</style><body>hello from iframe</body>"; + +const DOCUMENT_SRC = "<style>" + + "iframe {" + + "height:200px;" + + "border: 11px solid black;" + + "padding: 13px;" + + "}" + + "body,iframe {" + + "margin:0" + + "}" + + "</style>" + + "<body>" + + "<iframe src='data:text/html;charset=utf-8," + IFRAME_SRC + "'></iframe>" + + "</body>"; + +const TEST_URI = "data:text/html;charset=utf-8," + DOCUMENT_SRC; + +add_task(function* () { + let { inspector, toolbox, testActor } = yield openInspectorForURL(TEST_URI); + + info("Waiting for box mode to show."); + let body = yield getNodeFront("body", inspector); + yield inspector.highlighter.showBoxModel(body); + + info("Waiting for element picker to become active."); + yield startPicker(toolbox); + + info("Moving mouse over iframe padding."); + yield moveMouseOver("iframe", 1, 1); + + info("Performing checks"); + yield testActor.isNodeCorrectlyHighlighted("iframe", is); + + info("Scrolling the document"); + yield testActor.setProperty("iframe", "style", "margin-bottom: 2000px"); + yield testActor.eval("window.scrollBy(0, 40);"); + + // target the body within the iframe + let iframeBodySelector = ["iframe", "body"]; + + info("Moving mouse over iframe body"); + yield moveMouseOver("iframe", 40, 40); + + ok((yield testActor.assertHighlightedNode(iframeBodySelector)), + "highlighter shows the right node"); + yield testActor.isNodeCorrectlyHighlighted(iframeBodySelector, is); + + info("Waiting for the element picker to deactivate."); + yield inspector.toolbox.highlighterUtils.stopPicker(); + + function moveMouseOver(selector, x, y) { + info("Waiting for element " + selector + " to be highlighted"); + testActor.synthesizeMouse({selector, x, y, options: {type: "mousemove"}}); + return inspector.toolbox.once("picker-node-hovered"); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-04.js b/devtools/client/inspector/test/browser_inspector_highlighter-04.js new file mode 100644 index 000000000..d87f20e94 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-04.js @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Check that various highlighter elements exist. + +const TEST_URL = "data:text/html;charset=utf-8,<div>test</div>"; + +// IDs of all highlighter elements that we expect to find in the canvasFrame. +const ELEMENTS = ["box-model-root", + "box-model-elements", + "box-model-margin", + "box-model-border", + "box-model-padding", + "box-model-content", + "box-model-guide-top", + "box-model-guide-right", + "box-model-guide-bottom", + "box-model-guide-left", + "box-model-infobar-container", + "box-model-infobar-tagname", + "box-model-infobar-id", + "box-model-infobar-classes", + "box-model-infobar-pseudo-classes", + "box-model-infobar-dimensions"]; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + info("Show the box-model highlighter"); + let divFront = yield getNodeFront("div", inspector); + yield inspector.highlighter.showBoxModel(divFront); + + for (let id of ELEMENTS) { + let foundId = yield testActor.getHighlighterNodeAttribute(id, "id"); + is(foundId, id, "Element " + id + " found"); + } + + info("Hide the box-model highlighter"); + yield inspector.highlighter.hideBoxModel(); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-by-type.js b/devtools/client/inspector/test/browser_inspector_highlighter-by-type.js new file mode 100644 index 000000000..485d9db0e --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-by-type.js @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Check that custom highlighters can be retrieved by type and that they expose +// the expected API. + +const TEST_URL = "data:text/html;charset=utf-8,custom highlighters"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + yield onlyOneInstanceOfMainHighlighter(inspector); + yield manyInstancesOfCustomHighlighters(inspector); + yield showHideMethodsAreAvailable(inspector); + yield unknownHighlighterTypeShouldntBeAccepted(inspector); +}); + +function* onlyOneInstanceOfMainHighlighter({inspector}) { + info("Check that the inspector always sends back the same main highlighter"); + + let h1 = yield inspector.getHighlighter(false); + let h2 = yield inspector.getHighlighter(false); + is(h1, h2, "The same highlighter front was returned"); + + is(h1.typeName, "highlighter", "The right front type was returned"); +} + +function* manyInstancesOfCustomHighlighters({inspector}) { + let h1 = yield inspector.getHighlighterByType("BoxModelHighlighter"); + let h2 = yield inspector.getHighlighterByType("BoxModelHighlighter"); + ok(h1 !== h2, "getHighlighterByType returns new instances every time (1)"); + + let h3 = yield inspector.getHighlighterByType("CssTransformHighlighter"); + let h4 = yield inspector.getHighlighterByType("CssTransformHighlighter"); + ok(h3 !== h4, "getHighlighterByType returns new instances every time (2)"); + ok(h3 !== h1 && h3 !== h2, + "getHighlighterByType returns new instances every time (3)"); + ok(h4 !== h1 && h4 !== h2, + "getHighlighterByType returns new instances every time (4)"); + + yield h1.finalize(); + yield h2.finalize(); + yield h3.finalize(); + yield h4.finalize(); +} + +function* showHideMethodsAreAvailable({inspector}) { + let h1 = yield inspector.getHighlighterByType("BoxModelHighlighter"); + let h2 = yield inspector.getHighlighterByType("CssTransformHighlighter"); + + ok("show" in h1, "Show method is present on the front API"); + ok("show" in h2, "Show method is present on the front API"); + ok("hide" in h1, "Hide method is present on the front API"); + ok("hide" in h2, "Hide method is present on the front API"); + + yield h1.finalize(); + yield h2.finalize(); +} + +function* unknownHighlighterTypeShouldntBeAccepted({inspector}) { + let h = yield inspector.getHighlighterByType("whatever"); + ok(!h, "No highlighter was returned for the invalid type"); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cancel.js b/devtools/client/inspector/test/browser_inspector_highlighter-cancel.js new file mode 100644 index 000000000..f1022bb50 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cancel.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that canceling the element picker zooms back on the focused element. Bug 1224304. + +const TEST_URL = URL_ROOT + "doc_inspector_long-divs.html"; + +add_task(function* () { + let {inspector, toolbox, testActor} = yield openInspectorForURL(TEST_URL); + + yield selectAndHighlightNode("#focus-here", inspector); + ok((yield testActor.assertHighlightedNode("#focus-here")), + "The highlighter focuses on div#focus-here"); + ok(isSelectedMarkupNodeInView(), + "The currently selected node is on the screen."); + + // Start the picker but skip focusing manually focusing on the target, let the element + // picker do the focusing. + yield startPicker(toolbox, true); + yield moveMouseOver("#zoom-here"); + ok(!isSelectedMarkupNodeInView(), + "The currently selected node is off the screen."); + + yield cancelPickerByShortcut(); + ok(isSelectedMarkupNodeInView(), + "The currently selected node is focused back on the screen."); + + function cancelPickerByShortcut() { + info("Key pressed. Waiting for picker to be canceled."); + testActor.synthesizeKey({key: "VK_ESCAPE", options: {}}); + return inspector.toolbox.once("picker-canceled"); + } + + function moveMouseOver(selector) { + info(`Waiting for element ${selector} to be hovered in the markup view`); + testActor.synthesizeMouse({ + options: {type: "mousemove"}, + center: true, + selector: selector + }); + return inspector.markup.once("showcontainerhovered"); + } + + function isSelectedMarkupNodeInView() { + const selectedNodeContainer = inspector.markup._selectedContainer.elt; + const bounds = selectedNodeContainer.getBoundingClientRect(); + return bounds.top > 0 && bounds.bottom > 0; + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-comments.js b/devtools/client/inspector/test/browser_inspector_highlighter-comments.js new file mode 100644 index 000000000..104395227 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-comments.js @@ -0,0 +1,105 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// +// Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejection should be fixed. +// +thisTestLeaksUncaughtRejectionsAndShouldBeFixed("false"); + +// Test that hovering over the markup-view's containers doesn't always show the +// highlighter, depending on the type of node hovered over. + +const TEST_PAGE = URL_ROOT + + "doc_inspector_highlighter-comments.html"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_PAGE); + let markupView = inspector.markup; + yield selectNode("p", inspector); + + info("Hovering over #id1 and waiting for highlighter to appear."); + yield hoverElement("#id1"); + yield assertHighlighterShownOn("#id1"); + + info("Hovering over comment node and ensuring highlighter doesn't appear."); + yield hoverComment(); + yield assertHighlighterHidden(); + + info("Hovering over #id1 again and waiting for highlighter to appear."); + yield hoverElement("#id1"); + yield assertHighlighterShownOn("#id1"); + + info("Hovering over #id2 and waiting for highlighter to appear."); + yield hoverElement("#id2"); + yield assertHighlighterShownOn("#id2"); + + info("Hovering over <script> and ensuring highlighter doesn't appear."); + yield hoverElement("script"); + yield assertHighlighterHidden(); + + info("Hovering over #id3 and waiting for highlighter to appear."); + yield hoverElement("#id3"); + yield assertHighlighterShownOn("#id3"); + + info("Hovering over hidden #id4 and ensuring highlighter doesn't appear."); + yield hoverElement("#id4"); + yield assertHighlighterHidden(); + + info("Hovering over a text node and waiting for highlighter to appear."); + yield hoverTextNode("Visible text node"); + yield assertHighlighterShownOnTextNode("body", 14); + + function hoverContainer(container) { + let promise = inspector.toolbox.once("node-highlight"); + + EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"}, + markupView.doc.defaultView); + + return promise; + } + + function* hoverElement(selector) { + info(`Hovering node ${selector} in the markup view`); + let container = yield getContainerForSelector(selector, inspector); + return hoverContainer(container); + } + + function hoverComment() { + info("Hovering the comment node in the markup view"); + for (let [node, container] of markupView._containers) { + if (node.nodeType === Ci.nsIDOMNode.COMMENT_NODE) { + return hoverContainer(container); + } + } + return null; + } + + function hoverTextNode(text) { + info(`Hovering the text node "${text}" in the markup view`); + let container = [...markupView._containers].filter(([nodeFront]) => { + return nodeFront.nodeType === Ci.nsIDOMNode.TEXT_NODE && + nodeFront._form.nodeValue.trim() === text.trim(); + })[0][1]; + return hoverContainer(container); + } + + function* assertHighlighterShownOn(selector) { + ok((yield testActor.assertHighlightedNode(selector)), + "Highlighter is shown on the right node: " + selector); + } + + function* assertHighlighterShownOnTextNode(parentSelector, childNodeIndex) { + ok((yield testActor.assertHighlightedTextNode(parentSelector, childNodeIndex)), + "Highlighter is shown on the right text node"); + } + + function* assertHighlighterHidden() { + let isVisible = yield testActor.isHighlighting(); + ok(!isVisible, "Highlighter is hidden"); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_01.js new file mode 100644 index 000000000..ef21b88c9 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_01.js @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test the creation of the canvas highlighter element of the css grid highlighter. + +const TEST_URL = ` + <style type='text/css'> + #grid { + display: grid; + } + #cell1 { + grid-column: 1; + grid-row: 1; + } + #cell2 { + grid-column: 2; + grid-row: 1; + } + #cell3 { + grid-column: 1; + grid-row: 2; + } + #cell4 { + grid-column: 2; + grid-row: 2; + } + </style> + <div id="grid"> + <div id="cell1">cell1</div> + <div id="cell2">cell2</div> + <div id="cell3">cell3</div> + <div id="cell4">cell4</div> + </div> +`; + +const HIGHLIGHTER_TYPE = "CssGridHighlighter"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URL)); + let front = inspector.inspector; + let highlighter = yield front.getHighlighterByType(HIGHLIGHTER_TYPE); + + yield isHiddenByDefault(testActor, highlighter); + yield isVisibleWhenShown(testActor, inspector, highlighter); + + yield highlighter.finalize(); +}); + +function* isHiddenByDefault(testActor, highlighterFront) { + info("Checking that the highlighter is hidden by default"); + + let hidden = yield testActor.getHighlighterNodeAttribute( + "css-grid-canvas", "hidden", highlighterFront); + ok(hidden, "The highlighter is hidden by default"); +} + +function* isVisibleWhenShown(testActor, inspector, highlighterFront) { + info("Asking to show the highlighter on the test node"); + + let node = yield getNodeFront("#grid", inspector); + yield highlighterFront.show(node); + + let hidden = yield testActor.getHighlighterNodeAttribute( + "css-grid-canvas", "hidden", highlighterFront); + ok(!hidden, "The highlighter is visible"); + + info("Hiding the highlighter"); + yield highlighterFront.hide(); + + hidden = yield testActor.getHighlighterNodeAttribute( + "css-grid-canvas", "hidden", highlighterFront); + ok(hidden, "The highlighter is hidden"); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_01.js new file mode 100644 index 000000000..f30d1b590 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_01.js @@ -0,0 +1,152 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test the creation of the SVG highlighter elements of the css transform +// highlighter. + +const TEST_URL = ` + <div id="transformed" + style="border:1px solid red;width:100px;height:100px;transform:skew(13deg);"> + </div> + <div id="untransformed" + style="border:1px solid blue;width:100px;height:100px;"> + </div> + <span id="inline" + style="transform:rotate(90deg);">this is an inline transformed element + </span> +`; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURI(TEST_URL)); + let front = inspector.inspector; + + let highlighter = yield front.getHighlighterByType("CssTransformHighlighter"); + + yield isHiddenByDefault(testActor, highlighter); + yield has2PolygonsAnd4Lines(testActor, highlighter); + yield isNotShownForUntransformed(testActor, inspector, highlighter); + yield isNotShownForInline(testActor, inspector, highlighter); + yield isVisibleWhenShown(testActor, inspector, highlighter); + yield linesLinkThePolygons(testActor, inspector, highlighter); + + yield highlighter.finalize(); +}); + +function* isHiddenByDefault(testActor, highlighterFront) { + info("Checking that the highlighter is hidden by default"); + + let hidden = yield testActor.getHighlighterNodeAttribute( + "css-transform-elements", "hidden", highlighterFront); + ok(hidden, "The highlighter is hidden by default"); +} + +function* has2PolygonsAnd4Lines(testActor, highlighterFront) { + info("Checking that the highlighter is made up of 4 lines and 2 polygons"); + + let value = yield testActor.getHighlighterNodeAttribute( + "css-transform-untransformed", "class", highlighterFront); + is(value, "css-transform-untransformed", "The untransformed polygon exists"); + + value = yield testActor.getHighlighterNodeAttribute( + "css-transform-transformed", "class", highlighterFront); + is(value, "css-transform-transformed", "The transformed polygon exists"); + + for (let nb of ["1", "2", "3", "4"]) { + value = yield testActor.getHighlighterNodeAttribute( + "css-transform-line" + nb, "class", highlighterFront); + is(value, "css-transform-line", "The line " + nb + " exists"); + } +} + +function* isNotShownForUntransformed(testActor, inspector, highlighterFront) { + info("Asking to show the highlighter on the untransformed test node"); + + let node = yield getNodeFront("#untransformed", inspector); + yield highlighterFront.show(node); + + let hidden = yield testActor.getHighlighterNodeAttribute( + "css-transform-elements", "hidden", highlighterFront); + ok(hidden, "The highlighter is still hidden"); +} + +function* isNotShownForInline(testActor, inspector, highlighterFront) { + info("Asking to show the highlighter on the inline test node"); + + let node = yield getNodeFront("#inline", inspector); + yield highlighterFront.show(node); + + let hidden = yield testActor.getHighlighterNodeAttribute( + "css-transform-elements", "hidden", highlighterFront); + ok(hidden, "The highlighter is still hidden"); +} + +function* isVisibleWhenShown(testActor, inspector, highlighterFront) { + info("Asking to show the highlighter on the test node"); + + let node = yield getNodeFront("#transformed", inspector); + yield highlighterFront.show(node); + + let hidden = yield testActor.getHighlighterNodeAttribute( + "css-transform-elements", "hidden", highlighterFront); + ok(!hidden, "The highlighter is visible"); + + info("Hiding the highlighter"); + yield highlighterFront.hide(); + + hidden = yield testActor.getHighlighterNodeAttribute( + "css-transform-elements", "hidden", highlighterFront); + ok(hidden, "The highlighter is hidden"); +} + +function* linesLinkThePolygons(testActor, inspector, highlighterFront) { + info("Showing the highlighter on the transformed node"); + + let node = yield getNodeFront("#transformed", inspector); + yield highlighterFront.show(node); + + info("Checking that the 4 lines do link the 2 shape's corners"); + + let lines = []; + for (let nb of ["1", "2", "3", "4"]) { + let x1 = yield testActor.getHighlighterNodeAttribute( + "css-transform-line" + nb, "x1", highlighterFront); + let y1 = yield testActor.getHighlighterNodeAttribute( + "css-transform-line" + nb, "y1", highlighterFront); + let x2 = yield testActor.getHighlighterNodeAttribute( + "css-transform-line" + nb, "x2", highlighterFront); + let y2 = yield testActor.getHighlighterNodeAttribute( + "css-transform-line" + nb, "y2", highlighterFront); + lines.push({x1, y1, x2, y2}); + } + + let points1 = yield testActor.getHighlighterNodeAttribute( + "css-transform-untransformed", "points", highlighterFront); + points1 = points1.split(" "); + + let points2 = yield testActor.getHighlighterNodeAttribute( + "css-transform-transformed", "points", highlighterFront); + points2 = points2.split(" "); + + for (let i = 0; i < lines.length; i++) { + info("Checking line nb " + i); + let line = lines[i]; + + let p1 = points1[i].split(","); + is(p1[0], line.x1, + "line " + i + "'s first point matches the untransformed x coordinate"); + is(p1[1], line.y1, + "line " + i + "'s first point matches the untransformed y coordinate"); + + let p2 = points2[i].split(","); + is(p2[0], line.x2, + "line " + i + "'s first point matches the transformed x coordinate"); + is(p2[1], line.y2, + "line " + i + "'s first point matches the transformed y coordinate"); + } + + yield highlighterFront.hide(); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_02.js new file mode 100644 index 000000000..52e3b0146 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-csstransform_02.js @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* +Bug 1014547 - CSS transforms highlighter +Test that the highlighter elements created have the right size and coordinates. + +Note that instead of hard-coding values here, the assertions are made by +comparing with the result of getAdjustedQuads. + +There's a separate test for checking that getAdjustedQuads actually returns +sensible values +(devtools/client/shared/test/browser_layoutHelpers-getBoxQuads.js), +so the present test doesn't care about that, it just verifies that the css +transform highlighter applies those values correctly to the SVG elements +*/ + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_csstransform.html"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + let front = inspector.inspector; + + let highlighter = yield front.getHighlighterByType("CssTransformHighlighter"); + + let nodeFront = yield getNodeFront("#test-node", inspector); + + info("Displaying the transform highlighter on test node"); + yield highlighter.show(nodeFront); + + let data = yield testActor.getAllAdjustedQuads("#test-node"); + let [expected] = data.border; + + let points = yield testActor.getHighlighterNodeAttribute( + "css-transform-transformed", "points", highlighter); + let polygonPoints = points.split(" ").map(p => { + return { + x: +p.substring(0, p.indexOf(",")), + y: +p.substring(p.indexOf(",") + 1) + }; + }); + + for (let i = 1; i < 5; i++) { + is(polygonPoints[i - 1].x, expected["p" + i].x, + "p" + i + " x coordinate is correct"); + is(polygonPoints[i - 1].y, expected["p" + i].y, + "p" + i + " y coordinate is correct"); + } + + info("Hiding the transform highlighter"); + yield highlighter.hide(); + yield highlighter.finalize(); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-embed.js b/devtools/client/inspector/test/browser_inspector_highlighter-embed.js new file mode 100644 index 000000000..23cd4332a --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-embed.js @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that the highlighter can go inside <embed> elements + +const TEST_URL = URL_ROOT + "doc_inspector_embed.html"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + info("Get a node inside the <embed> element and select/highlight it"); + let body = yield getEmbeddedBody(inspector); + yield selectAndHighlightNode(body, inspector); + + let selectedNode = inspector.selection.nodeFront; + is(selectedNode.tagName.toLowerCase(), "body", "The selected node is <body>"); + ok(selectedNode.baseURI.endsWith("doc_inspector_menu.html"), + "The selected node is the <body> node inside the <embed> element"); +}); + +function* getEmbeddedBody({walker}) { + let embed = yield walker.querySelector(walker.rootNode, "embed"); + let {nodes} = yield walker.children(embed); + let contentDoc = nodes[0]; + let body = yield walker.querySelector(contentDoc, "body"); + return body; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js new file mode 100644 index 000000000..2d91f81a7 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js @@ -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/. */ +"use strict"; + +// Test that the eyedropper can copy colors to the clipboard + +const HIGHLIGHTER_TYPE = "EyeDropper"; +const ID = "eye-dropper-"; +const TEST_URI = "data:text/html;charset=utf-8,<style>html{background:red}</style>"; + +add_task(function* () { + let helper = yield openInspectorForURL(TEST_URI) + .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE)); + helper.prefix = ID; + + let {show, finalize, + waitForElementAttributeSet, waitForElementAttributeRemoved} = helper; + + info("Show the eyedropper with the copyOnSelect option"); + yield show("html", {copyOnSelect: true}); + + info("Make sure to wait until the eyedropper is done taking a screenshot of the page"); + yield waitForElementAttributeSet("root", "drawn", helper); + + yield waitForClipboardPromise(() => { + info("Activate the eyedropper so the background color is copied"); + EventUtils.synthesizeKey("VK_RETURN", {}); + }, "#FF0000"); + + ok(true, "The clipboard contains the right value"); + + yield waitForElementAttributeRemoved("root", "drawn", helper); + yield waitForElementAttributeSet("root", "hidden", helper); + ok(true, "The eyedropper is now hidden"); + + finalize(); +}); + diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-csp.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-csp.js new file mode 100644 index 000000000..0cd425b56 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-csp.js @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// Test that the eyedropper opens correctly even when the page defines CSP headers. + +const HIGHLIGHTER_TYPE = "EyeDropper"; +const ID = "eye-dropper-"; +const TEST_URI = URL_ROOT + "doc_inspector_csp.html"; + +add_task(function* () { + let helper = yield openInspectorForURL(TEST_URI) + .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE)); + helper.prefix = ID; + let {show, hide, finalize, isElementHidden, waitForElementAttributeSet} = helper; + + info("Try to display the eyedropper"); + yield show("html"); + + let hidden = yield isElementHidden("root"); + ok(!hidden, "The eyedropper is now shown"); + + info("Wait until the eyedropper is done taking a screenshot of the page"); + yield waitForElementAttributeSet("root", "drawn", helper); + ok(true, "The image data was retrieved successfully from the window"); + + yield hide(); + finalize(); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-events.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-events.js new file mode 100644 index 000000000..49543b5ce --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-events.js @@ -0,0 +1,141 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// Test the eyedropper mouse and keyboard handling. + +const HIGHLIGHTER_TYPE = "EyeDropper"; +const ID = "eye-dropper-"; +const TEST_URI = ` +<style> + html{width:100%;height:100%;} +</style> +<body>eye-dropper test</body>`; + +const MOVE_EVENTS_DATA = [ + {type: "mouse", x: 200, y: 100, expected: {x: 200, y: 100}}, + {type: "mouse", x: 100, y: 200, expected: {x: 100, y: 200}}, + {type: "keyboard", key: "VK_LEFT", expected: {x: 99, y: 200}}, + {type: "keyboard", key: "VK_LEFT", shift: true, expected: {x: 89, y: 200}}, + {type: "keyboard", key: "VK_RIGHT", expected: {x: 90, y: 200}}, + {type: "keyboard", key: "VK_RIGHT", shift: true, expected: {x: 100, y: 200}}, + {type: "keyboard", key: "VK_DOWN", expected: {x: 100, y: 201}}, + {type: "keyboard", key: "VK_DOWN", shift: true, expected: {x: 100, y: 211}}, + {type: "keyboard", key: "VK_UP", expected: {x: 100, y: 210}}, + {type: "keyboard", key: "VK_UP", shift: true, expected: {x: 100, y: 200}}, + // Mouse initialization for left and top snapping + {type: "mouse", x: 7, y: 7, expected: {x: 7, y: 7}}, + // Left Snapping + {type: "keyboard", key: "VK_LEFT", shift: true, expected: {x: 0, y: 7}, + desc: "Left Snapping to x=0"}, + // Top Snapping + {type: "keyboard", key: "VK_UP", shift: true, expected: {x: 0, y: 0}, + desc: "Top Snapping to y=0"}, + // Mouse initialization for right snapping + { + type: "mouse", + x: (width, height) => width - 5, + y: 0, + expected: { + x: (width, height) => width - 5, + y: 0 + } + }, + // Right snapping + { + type: "keyboard", + key: "VK_RIGHT", + shift: true, + expected: { + x: (width, height) => width, + y: 0 + }, + desc: "Right snapping to x=max window width available" + }, + // Mouse initialization for bottom snapping + { + type: "mouse", + x: 0, + y: (width, height) => height - 5, + expected: { + x: 0, + y: (width, height) => height - 5 + } + }, + // Bottom snapping + { + type: "keyboard", + key: "VK_DOWN", + shift: true, + expected: { + x: 0, + y: (width, height) => height + }, + desc: "Bottom snapping to y=max window height available" + }, +]; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)({inspector, testActor}); + + helper.prefix = ID; + + yield helper.show("html"); + yield respondsToMoveEvents(helper, testActor); + yield respondsToReturnAndEscape(helper); + + helper.finalize(); +}); + +function* respondsToMoveEvents(helper, testActor) { + info("Checking that the eyedropper responds to events from the mouse and keyboard"); + let {mouse} = helper; + let {width, height} = yield testActor.getBoundingClientRect("html"); + + for (let {type, x, y, key, shift, expected, desc} of MOVE_EVENTS_DATA) { + x = typeof x === "function" ? x(width, height) : x; + y = typeof y === "function" ? y(width, height) : y; + expected.x = typeof expected.x === "function" ? + expected.x(width, height) : expected.x; + expected.y = typeof expected.y === "function" ? + expected.y(width, height) : expected.y; + + if (typeof desc === "undefined") { + info(`Simulating a ${type} event to move to ${expected.x} ${expected.y}`); + } else { + info(`Simulating ${type} event: ${desc}`); + } + + if (type === "mouse") { + yield mouse.move(x, y); + } else if (type === "keyboard") { + let options = shift ? {shiftKey: true} : {}; + yield EventUtils.synthesizeKey(key, options); + } + yield checkPosition(expected, helper); + } +} + +function* checkPosition({x, y}, {getElementAttribute}) { + let style = yield getElementAttribute("root", "style"); + is(style, `top:${y}px;left:${x}px;`, + `The eyedropper is at the expected ${x} ${y} position`); +} + +function* respondsToReturnAndEscape({isElementHidden, show}) { + info("Simulating return to select the color and hide the eyedropper"); + + yield EventUtils.synthesizeKey("VK_RETURN", {}); + let hidden = yield isElementHidden("root"); + ok(hidden, "The eyedropper has been hidden"); + + info("Showing the eyedropper again and simulating escape to hide it"); + + yield show("html"); + yield EventUtils.synthesizeKey("VK_ESCAPE", {}); + hidden = yield isElementHidden("root"); + ok(hidden, "The eyedropper has been hidden again"); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-label.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-label.js new file mode 100644 index 000000000..02750761b --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-label.js @@ -0,0 +1,115 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// Test the position of the eyedropper label. +// It should move around when the eyedropper is close to the edges of the viewport so as +// to always stay visible. + +const HIGHLIGHTER_TYPE = "EyeDropper"; +const ID = "eye-dropper-"; + +const HTML = ` +<style> +html, body {height: 100%; margin: 0;} +body {background: linear-gradient(red, gold); display: flex; justify-content: center; + align-items: center;} +</style> +Eyedropper label position test +`; +const TEST_PAGE = "data:text/html;charset=utf-8," + encodeURI(HTML); + +const TEST_DATA = [{ + desc: "Move the mouse to the center of the screen", + getCoordinates: (width, height) => { + return {x: width / 2, y: height / 2}; + }, + expectedPositions: {top: false, right: false, left: false} +}, { + desc: "Move the mouse to the center left", + getCoordinates: (width, height) => { + return {x: 0, y: height / 2}; + }, + expectedPositions: {top: false, right: true, left: false} +}, { + desc: "Move the mouse to the center right", + getCoordinates: (width, height) => { + return {x: width, y: height / 2}; + }, + expectedPositions: {top: false, right: false, left: true} +}, { + desc: "Move the mouse to the bottom center", + getCoordinates: (width, height) => { + return {x: width / 2, y: height}; + }, + expectedPositions: {top: true, right: false, left: false} +}, { + desc: "Move the mouse to the bottom left", + getCoordinates: (width, height) => { + return {x: 0, y: height}; + }, + expectedPositions: {top: true, right: true, left: false} +}, { + desc: "Move the mouse to the bottom right", + getCoordinates: (width, height) => { + return {x: width, y: height}; + }, + expectedPositions: {top: true, right: false, left: true} +}, { + desc: "Move the mouse to the top left", + getCoordinates: (width, height) => { + return {x: 0, y: 0}; + }, + expectedPositions: {top: false, right: true, left: false} +}, { + desc: "Move the mouse to the top right", + getCoordinates: (width, height) => { + return {x: width, y: 0}; + }, + expectedPositions: {top: false, right: false, left: true} +}]; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_PAGE); + let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)({inspector, testActor}); + helper.prefix = ID; + + let {mouse, show, hide, finalize} = helper; + let {width, height} = yield testActor.getBoundingClientRect("html"); + + // This test fails in non-e10s windows if we use width and height. For some reasons, the + // mouse events can't be dispatched/handled properly when we try to move the eyedropper + // to the far right and/or bottom of the screen. So just removing 10px from each side + // fixes it. + width -= 10; + height -= 10; + + info("Show the eyedropper on the page"); + yield show("html"); + + info("Move the eyedropper around and check that the label appears at the right place"); + for (let {desc, getCoordinates, expectedPositions} of TEST_DATA) { + info(desc); + let {x, y} = getCoordinates(width, height); + info(`Moving the mouse to ${x} ${y}`); + yield mouse.move(x, y); + yield checkLabelPositionAttributes(helper, expectedPositions); + } + + info("Hide the eyedropper"); + yield hide(); + finalize(); +}); + +function* checkLabelPositionAttributes(helper, positions) { + for (let position in positions) { + is((yield hasAttribute(helper, position)), positions[position], + `The label was ${positions[position] ? "" : "not "}moved to the ${position}`); + } +} + +function* hasAttribute({getElementAttribute}, name) { + let value = yield getElementAttribute("root", name); + return value !== null; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-show-hide.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-show-hide.js new file mode 100644 index 000000000..86f2ae83d --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-show-hide.js @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// Test the basic structure of the eye-dropper highlighter. + +const HIGHLIGHTER_TYPE = "EyeDropper"; +const ID = "eye-dropper-"; + +add_task(function* () { + let helper = yield openInspectorForURL("data:text/html;charset=utf-8,eye-dropper test") + .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE)); + helper.prefix = ID; + + yield isInitiallyHidden(helper); + yield canBeShownAndHidden(helper); + + helper.finalize(); +}); + +function* isInitiallyHidden({isElementHidden}) { + info("Checking that the eyedropper is hidden by default"); + + let hidden = yield isElementHidden("root"); + ok(hidden, "The eyedropper is hidden by default"); +} + +function* canBeShownAndHidden({show, hide, isElementHidden, getElementAttribute}) { + info("Asking to show and hide the highlighter actually works"); + + yield show("html"); + let hidden = yield isElementHidden("root"); + ok(!hidden, "The eyedropper is now shown"); + + let style = yield getElementAttribute("root", "style"); + is(style, "top:100px;left:100px;", "The eyedropper is correctly positioned"); + + yield hide(); + hidden = yield isElementHidden("root"); + ok(hidden, "The eyedropper is now hidden again"); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-xul.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-xul.js new file mode 100644 index 000000000..7c44e7275 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-xul.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the eyedropper icons in the toolbar and in the color picker aren't displayed +// when the page isn't an HTML one. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_xbl.xul"; +const TEST_URL_2 = + "data:text/html;charset=utf-8,<h1 style='color:red'>HTML test page</h1>"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + info("Check the inspector toolbar"); + let button = inspector.panelDoc.querySelector("#inspector-eyedropper-toggle"); + ok(isDisabled(button), "The button is hidden in the toolbar"); + + info("Check the color picker"); + yield selectNode("#scale", inspector); + + // Find the color swatch in the rule-view. + let ruleView = inspector.ruleview.view; + let ruleViewDocument = ruleView.styleDocument; + let swatchEl = ruleViewDocument.querySelector(".ruleview-colorswatch"); + + info("Open the color picker"); + let cPicker = ruleView.tooltips.colorPicker; + let onColorPickerReady = cPicker.once("ready"); + swatchEl.click(); + yield onColorPickerReady; + + button = cPicker.tooltip.doc.querySelector("#eyedropper-button"); + ok(isDisabled(button), "The button is disabled in the color picker"); + + info("Navigate to a HTML document"); + yield navigateTo(inspector, TEST_URL_2); + + info("Check the inspector toolbar in HTML document"); + button = inspector.panelDoc.querySelector("#inspector-eyedropper-toggle"); + ok(!isDisabled(button), "The button is enabled in the toolbar"); + + info("Check the color picker in HTML document"); + // Find the color swatch in the rule-view. + yield selectNode("h1", inspector); + + ruleView = inspector.ruleview.view; + ruleViewDocument = ruleView.styleDocument; + swatchEl = ruleViewDocument.querySelector(".ruleview-colorswatch"); + + info("Open the color picker in HTML document"); + cPicker = ruleView.tooltips.colorPicker; + onColorPickerReady = cPicker.once("ready"); + swatchEl.click(); + yield onColorPickerReady; + + button = cPicker.tooltip.doc.querySelector("#eyedropper-button"); + ok(!isDisabled(button), "The button is enabled in the color picker"); +}); + +function isDisabled(button) { + return button.disabled; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_01.js new file mode 100644 index 000000000..28a20998c --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_01.js @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test the creation of the geometry highlighter elements. + +const TEST_URL = `data:text/html;charset=utf-8, + <span id='inline'></span> + <div id='positioned' style=' + background:yellow; + position:absolute; + left:5rem; + top:30px; + right:300px; + bottom:10em;'></div> + <div id='sized' style=' + background:red; + width:5em; + height:50%;'></div>`; + +const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter"; + +const ID = "geometry-editor-"; +const SIDES = ["left", "right", "top", "bottom"]; + +add_task(function* () { + let helper = yield openInspectorForURL(TEST_URL) + .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE)); + + let { finalize } = helper; + + helper.prefix = ID; + + yield hasArrowsAndLabelsAndHandlers(helper); + yield isHiddenForNonPositionedNonSizedElement(helper); + yield sideArrowsAreDisplayedForPositionedNode(helper); + + finalize(); +}); + +function* hasArrowsAndLabelsAndHandlers({getElementAttribute}) { + info("Checking that the highlighter has the expected arrows and labels"); + + for (let name of [...SIDES]) { + let value = yield getElementAttribute("arrow-" + name, "class"); + is(value, ID + "arrow " + name, "The " + name + " arrow exists"); + + value = yield getElementAttribute("label-text-" + name, "class"); + is(value, ID + "label-text", "The " + name + " label exists"); + + value = yield getElementAttribute("handler-" + name, "class"); + is(value, ID + "handler-" + name, "The " + name + " handler exists"); + } +} + +function* isHiddenForNonPositionedNonSizedElement( + {show, hide, isElementHidden}) { + info("Asking to show the highlighter on an inline, non p ositioned element"); + + yield show("#inline"); + + for (let name of [...SIDES]) { + let hidden = yield isElementHidden("arrow-" + name); + ok(hidden, "The " + name + " arrow is hidden"); + + hidden = yield isElementHidden("handler-" + name); + ok(hidden, "The " + name + " handler is hidden"); + } +} + +function* sideArrowsAreDisplayedForPositionedNode( + {show, hide, isElementHidden}) { + info("Asking to show the highlighter on the positioned node"); + + yield show("#positioned"); + + for (let name of SIDES) { + let hidden = yield isElementHidden("arrow-" + name); + ok(!hidden, "The " + name + " arrow is visible for the positioned node"); + + hidden = yield isElementHidden("handler-" + name); + ok(!hidden, "The " + name + " handler is visible for the positioned node"); + } + + info("Hiding the highlighter"); + yield hide(); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_02.js new file mode 100644 index 000000000..e0681c6f9 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_02.js @@ -0,0 +1,116 @@ +/* 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/. */ + +/* Globals defined in: devtools/client/inspector/test/head.js */ + +"use strict"; + +// Test that the geometry highlighter labels are correct. + +const TEST_URL = `data:text/html;charset=utf-8, + <div id='positioned' style=' + background:yellow; + position:absolute; + left:5rem; + top:30px; + right:300px; + bottom:10em;'></div> + <div id='positioned2' style=' + background:blue; + position:absolute; + right:10%; + top:5vmin;'>test element</div> + <div id='relative' style=' + background:green; + position:relative; + top:10px; + left:20px; + bottom:30px; + right:40px; + width:100px; + height:100px;'></div> + <div id='relative2' style=' + background:grey; + position:relative; + top:0;bottom:-50px; + height:3em;'>relative</div>`; + +const ID = "geometry-editor-"; +const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter"; + +const POSITIONED_ELEMENT_TESTS = [{ + selector: "#positioned", + expectedLabels: [ + {side: "left", visible: true, label: "5rem"}, + {side: "top", visible: true, label: "30px"}, + {side: "right", visible: true, label: "300px"}, + {side: "bottom", visible: true, label: "10em"} + ] +}, { + selector: "#positioned2", + expectedLabels: [ + {side: "left", visible: false}, + {side: "top", visible: true, label: "5vmin"}, + {side: "right", visible: true, label: "10%"}, + {side: "bottom", visible: false} + ] +}, { + selector: "#relative", + expectedLabels: [ + {side: "left", visible: true, label: "20px"}, + {side: "top", visible: true, label: "10px"}, + {side: "right", visible: false}, + {side: "bottom", visible: false} + ] +}, { + selector: "#relative2", + expectedLabels: [ + {side: "left", visible: false}, + {side: "top", visible: true, label: "0px"}, + {side: "right", visible: false}, + {side: "bottom", visible: false} + ] +}]; + +add_task(function* () { + let helper = yield openInspectorForURL(TEST_URL) + .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE)); + + helper.prefix = ID; + + let { finalize } = helper; + + yield positionLabelsAreCorrect(helper); + + yield finalize(); +}); + +function* positionLabelsAreCorrect( + {show, hide, isElementHidden, getElementTextContent} +) { + info("Highlight nodes and check position labels"); + + for (let {selector, expectedLabels} of POSITIONED_ELEMENT_TESTS) { + info("Testing node " + selector); + + yield show(selector); + + for (let {side, visible, label} of expectedLabels) { + let id = "label-" + side; + + let hidden = yield isElementHidden(id); + if (visible) { + ok(!hidden, "The " + side + " label is visible"); + + let value = yield getElementTextContent(id); + is(value, label, "The " + side + " label textcontent is correct"); + } else { + ok(hidden, "The " + side + " label is hidden"); + } + } + + info("Hiding the highlighter"); + yield hide(); + } +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_03.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_03.js new file mode 100644 index 000000000..0fa7bb96b --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_03.js @@ -0,0 +1,61 @@ +/* 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/. */ + +/* Globals defined in: devtools/client/inspector/test/head.js */ + +"use strict"; + +// Test that the right arrows/labels are shown even when the css properties are +// in several different css rules. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_01.html"; +const ID = "geometry-editor-"; +const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter"; +const PROPS = ["left", "right", "top", "bottom"]; + +add_task(function* () { + let helper = yield openInspectorForURL(TEST_URL) + .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE)); + + helper.prefix = ID; + + let { finalize } = helper; + + yield checkArrowsLabelsAndHandlers( + "#node2", ["top", "left", "bottom", "right"], + helper); + + yield checkArrowsLabelsAndHandlers("#node3", ["top", "left"], helper); + + yield finalize(); +}); + +function* checkArrowsLabelsAndHandlers(selector, expectedProperties, + {show, hide, isElementHidden} +) { + info("Getting node " + selector + " from the page"); + + yield show(selector); + + for (let name of expectedProperties) { + let hidden = (yield isElementHidden("arrow-" + name)) && + (yield isElementHidden("handler-" + name)); + ok(!hidden, + "The " + name + " label/arrow & handler is visible for node " + selector); + } + + // Testing that the other arrows are hidden + for (let name of PROPS) { + if (expectedProperties.indexOf(name) !== -1) { + continue; + } + let hidden = (yield isElementHidden("arrow-" + name)) && + (yield isElementHidden("handler-" + name)); + ok(hidden, + "The " + name + " arrow & handler is hidden for node " + selector); + } + + info("Hiding the highlighter"); + yield hide(); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_04.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_04.js new file mode 100644 index 000000000..7f198f6e3 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_04.js @@ -0,0 +1,85 @@ +/* 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/. */ + + /* Globals defined in: devtools/client/inspector/test/head.js */ + +"use strict"; + +// Test that the arrows and handlers are positioned correctly and have the right +// size. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_01.html"; +const ID = "geometry-editor-"; +const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter"; + +const handlerMap = { + "top": {"cx": "x2", "cy": "y2"}, + "bottom": {"cx": "x2", "cy": "y2"}, + "left": {"cx": "x2", "cy": "y2"}, + "right": {"cx": "x2", "cy": "y2"} +}; + +add_task(function* () { + let helper = yield openInspectorForURL(TEST_URL) + .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE)); + + helper.prefix = ID; + + let { hide, finalize } = helper; + + yield checkArrowsAndHandlers(helper, ".absolute-all-4", { + "top": {x1: 506, y1: 51, x2: 506, y2: 61}, + "bottom": {x1: 506, y1: 451, x2: 506, y2: 251}, + "left": {x1: 401, y1: 156, x2: 411, y2: 156}, + "right": {x1: 901, y1: 156, x2: 601, y2: 156} + }); + + yield checkArrowsAndHandlers(helper, ".relative", { + "top": {x1: 901, y1: 51, x2: 901, y2: 91}, + "left": {x1: 401, y1: 97, x2: 651, y2: 97} + }); + + yield checkArrowsAndHandlers(helper, ".fixed", { + "top": {x1: 25, y1: 0, x2: 25, y2: 400}, + "left": {x1: 0, y1: 425, x2: 0, y2: 425} + }); + + info("Hiding the highlighter"); + yield hide(); + yield finalize(); +}); + +function* checkArrowsAndHandlers(helper, selector, arrows) { + info("Highlighting the test node " + selector); + + yield helper.show(selector); + + for (let side in arrows) { + yield checkArrowAndHandler(helper, side, arrows[side]); + } +} + +function* checkArrowAndHandler({getElementAttribute}, name, expectedCoords) { + info("Checking " + name + "arrow and handler coordinates are correct"); + + let handlerX = yield getElementAttribute("handler-" + name, "cx"); + let handlerY = yield getElementAttribute("handler-" + name, "cy"); + + let expectedHandlerX = yield getElementAttribute("arrow-" + name, + handlerMap[name].cx); + let expectedHandlerY = yield getElementAttribute("arrow-" + name, + handlerMap[name].cy); + + is(handlerX, expectedHandlerX, + "coordinate X for handler " + name + " is correct."); + is(handlerY, expectedHandlerY, + "coordinate Y for handler " + name + " is correct."); + + for (let coordinate in expectedCoords) { + let value = yield getElementAttribute("arrow-" + name, coordinate); + + is(Math.floor(value), expectedCoords[coordinate], + coordinate + " coordinate for arrow " + name + " is correct"); + } +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_05.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_05.js new file mode 100644 index 000000000..649a4be3b --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_05.js @@ -0,0 +1,119 @@ +/* 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/. */ + + /* Globals defined in: devtools/client/inspector/test/head.js */ + +"use strict"; + +// Test that the arrows/handlers and offsetparent and currentnode elements of +// the geometry highlighter only appear when needed. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_02.html"; +const ID = "geometry-editor-"; +const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter"; + +const TEST_DATA = [{ + selector: "body", + isOffsetParentVisible: false, + isCurrentNodeVisible: false, + hasVisibleArrowsAndHandlers: false +}, { + selector: "h1", + isOffsetParentVisible: false, + isCurrentNodeVisible: false, + hasVisibleArrowsAndHandlers: false +}, { + selector: ".absolute", + isOffsetParentVisible: false, + isCurrentNodeVisible: true, + hasVisibleArrowsAndHandlers: true +}, { + selector: "#absolute-container", + isOffsetParentVisible: false, + isCurrentNodeVisible: true, + hasVisibleArrowsAndHandlers: false +}, { + selector: ".absolute-bottom-right", + isOffsetParentVisible: true, + isCurrentNodeVisible: true, + hasVisibleArrowsAndHandlers: true +}, { + selector: ".absolute-width-margin", + isOffsetParentVisible: true, + isCurrentNodeVisible: true, + hasVisibleArrowsAndHandlers: true +}, { + selector: ".absolute-all-4", + isOffsetParentVisible: true, + isCurrentNodeVisible: true, + hasVisibleArrowsAndHandlers: true +}, { + selector: ".relative", + isOffsetParentVisible: true, + isCurrentNodeVisible: true, + hasVisibleArrowsAndHandlers: true +}, { + selector: ".static", + isOffsetParentVisible: false, + isCurrentNodeVisible: false, + hasVisibleArrowsAndHandlers: false +}, { + selector: ".static-size", + isOffsetParentVisible: false, + isCurrentNodeVisible: true, + hasVisibleArrowsAndHandlers: false +}, { + selector: ".fixed", + isOffsetParentVisible: false, + isCurrentNodeVisible: true, + hasVisibleArrowsAndHandlers: true +}]; + +add_task(function* () { + let helper = yield openInspectorForURL(TEST_URL) + .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE)); + + helper.prefix = ID; + + let { hide, finalize } = helper; + + for (let data of TEST_DATA) { + yield testNode(helper, data); + } + + info("Hiding the highlighter"); + yield hide(); + yield finalize(); +}); + +function* testNode(helper, data) { + let { selector } = data; + yield helper.show(data.selector); + + is((yield isOffsetParentVisible(helper)), data.isOffsetParentVisible, + "The offset-parent highlighter visibility is correct for node " + selector); + is((yield isCurrentNodeVisible(helper)), data.isCurrentNodeVisible, + "The current-node highlighter visibility is correct for node " + selector); + is((yield hasVisibleArrowsAndHandlers(helper)), + data.hasVisibleArrowsAndHandlers, + "The arrows visibility is correct for node " + selector); +} + +function* isOffsetParentVisible({isElementHidden}) { + return !(yield isElementHidden("offset-parent")); +} + +function* isCurrentNodeVisible({isElementHidden}) { + return !(yield isElementHidden("current-node")); +} + +function* hasVisibleArrowsAndHandlers({isElementHidden}) { + for (let side of ["top", "left", "bottom", "right"]) { + let hidden = yield isElementHidden("arrow-" + side); + if (!hidden) { + return !(yield isElementHidden("handler-" + side)); + } + } + return false; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-geometry_06.js b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_06.js new file mode 100644 index 000000000..cc22473b7 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_06.js @@ -0,0 +1,166 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that the geometry editor resizes properly an element on all sides, +// with different unit measures, and that arrow/handlers are updated correctly. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_01.html"; +const ID = "geometry-editor-"; +const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter"; + +const SIDES = ["top", "right", "bottom", "left"]; + +// The object below contains all the tests for this unit test. +// The property's name is the test's description, that points to an +// object contains the steps (what side of the geometry editor to drag, +// the amount of pixels) and the expectation. +const TESTS = { + "Drag top's handler along x and y, south-east direction": { + "expects": "Only y axis is used to updated the top's element value", + "drag": "top", + "by": {x: 10, y: 10} + }, + "Drag right's handler along x and y, south-east direction": { + "expects": "Only x axis is used to updated the right's element value", + "drag": "right", + "by": {x: 10, y: 10} + }, + "Drag bottom's handler along x and y, south-east direction": { + "expects": "Only y axis is used to updated the bottom's element value", + "drag": "bottom", + "by": {x: 10, y: 10} + }, + "Drag left's handler along x and y, south-east direction": { + "expects": "Only y axis is used to updated the left's element value", + "drag": "left", + "by": {x: 10, y: 10} + }, + "Drag top's handler along x and y, north-west direction": { + "expects": "Only y axis is used to updated the top's element value", + "drag": "top", + "by": {x: -20, y: -20} + }, + "Drag right's handler along x and y, north-west direction": { + "expects": "Only x axis is used to updated the right's element value", + "drag": "right", + "by": {x: -20, y: -20} + }, + "Drag bottom's handler along x and y, north-west direction": { + "expects": "Only y axis is used to updated the bottom's element value", + "drag": "bottom", + "by": {x: -20, y: -20} + }, + "Drag left's handler along x and y, north-west direction": { + "expects": "Only y axis is used to updated the left's element value", + "drag": "left", + "by": {x: -20, y: -20} + } +}; + +add_task(function* () { + let inspector = yield openInspectorForURL(TEST_URL); + let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector); + + helper.prefix = ID; + + let { show, hide, finalize } = helper; + + info("Showing the highlighter"); + yield show("#node2"); + + for (let desc in TESTS) { + yield executeTest(helper, desc, TESTS[desc]); + } + + info("Hiding the highlighter"); + yield hide(); + yield finalize(); +}); + +function* executeTest(helper, desc, data) { + info(desc); + + ok((yield areElementAndHighlighterMovedCorrectly( + helper, data.drag, data.by)), data.expects); +} + +function* areElementAndHighlighterMovedCorrectly(helper, side, by) { + let { mouse, reflow, highlightedNode } = helper; + + let {x, y} = yield getHandlerCoords(helper, side); + + let dx = x + by.x; + let dy = y + by.y; + + let beforeDragStyle = yield highlightedNode.getComputedStyle(); + + // simulate drag & drop + yield mouse.down(x, y); + yield mouse.move(dx, dy); + yield mouse.up(); + + yield reflow(); + + info(`Checking ${side} handler is moved correctly`); + yield isHandlerPositionUpdated(helper, side, x, y, by); + + let delta = (side === "left" || side === "right") ? by.x : by.y; + delta = delta * ((side === "right" || side === "bottom") ? -1 : 1); + + info("Checking element's sides are correct after drag & drop"); + return yield areElementSideValuesCorrect(highlightedNode, beforeDragStyle, + side, delta); +} + +function* isHandlerPositionUpdated(helper, name, x, y, by) { + let {x: afterDragX, y: afterDragY} = yield getHandlerCoords(helper, name); + + if (name === "left" || name === "right") { + is(afterDragX, x + by.x, + `${name} handler's x axis updated.`); + is(afterDragY, y, + `${name} handler's y axis unchanged.`); + } else { + is(afterDragX, x, + `${name} handler's x axis unchanged.`); + is(afterDragY, y + by.y, + `${name} handler's y axis updated.`); + } +} + +function* areElementSideValuesCorrect(node, beforeDragStyle, name, delta) { + let afterDragStyle = yield node.getComputedStyle(); + let isSideCorrect = true; + + for (let side of SIDES) { + let afterValue = Math.round(parseFloat(afterDragStyle[side].value)); + let beforeValue = Math.round(parseFloat(beforeDragStyle[side].value)); + + if (side === name) { + // `isSideCorrect` is used only as test's return value, not to perform + // the actual test, because with `is` instead of `ok` we gather more + // information in case of failure + isSideCorrect = isSideCorrect && (afterValue === beforeValue + delta); + + is(afterValue, beforeValue + delta, + `${side} is updated.`); + } else { + isSideCorrect = isSideCorrect && (afterValue === beforeValue); + + is(afterValue, beforeValue, + `${side} is unchaged.`); + } + } + + return isSideCorrect; +} + +function* getHandlerCoords({getElementAttribute}, side) { + return { + x: Math.round(yield getElementAttribute("handler-" + side, "cx")), + y: Math.round(yield getElementAttribute("handler-" + side, "cy")) + }; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-hover_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-hover_01.js new file mode 100644 index 000000000..85f897080 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-hover_01.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 when first hovering over a node and immediately after selecting it +// by clicking on it leaves the highlighter visible for as long as the mouse is +// over the node + +const TEST_URL = "data:text/html;charset=utf-8," + + "<p>It's going to be legen....</p>"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + info("hovering over the <p> line in the markup-view"); + yield hoverContainer("p", inspector); + let isVisible = yield testActor.isHighlighting(); + ok(isVisible, "the highlighter is still visible"); + + info("selecting the <p> line by clicking in the markup-view"); + yield clickContainer("p", inspector); + + yield testActor.setProperty("p", "textContent", "wait for it ...."); + info("wait and see if the highlighter stays visible even after the node " + + "was selected"); + yield waitForTheBrieflyShowBoxModelTimeout(); + + yield testActor.setProperty("p", "textContent", "dary!!!!"); + isVisible = yield testActor.isHighlighting(); + ok(isVisible, "the highlighter is still visible"); +}); + +function waitForTheBrieflyShowBoxModelTimeout() { + let deferred = defer(); + // Note that the current timeout is 1 sec and is neither configurable nor + // exported anywhere we can access, so hard-coding the timeout + setTimeout(deferred.resolve, 1500); + return deferred.promise; +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-hover_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-hover_02.js new file mode 100644 index 000000000..e853b3963 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-hover_02.js @@ -0,0 +1,38 @@ +/* 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 when after an element is selected and highlighted on hover, if the +// mouse leaves the markup-view and comes back again on the same element, that +// the highlighter is shown again on the node + +const TEST_URL = "data:text/html;charset=utf-8,<p>Select me!</p>"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + info("hover over the <p> line in the markup-view so that it's the " + + "currently hovered node"); + yield hoverContainer("p", inspector); + + info("select the <p> markup-container line by clicking"); + yield clickContainer("p", inspector); + let isVisible = yield testActor.isHighlighting(); + ok(isVisible, "the highlighter is shown"); + + info("listen to the highlighter's hidden event"); + let onHidden = testActor.waitForHighlighterEvent("hidden", + inspector.highlighter); + info("mouse-leave the markup-view"); + yield mouseLeaveMarkupView(inspector); + yield onHidden; + isVisible = yield testActor.isHighlighting(); + ok(!isVisible, "the highlighter is hidden after mouseleave"); + + info("hover over the <p> line again, which is still selected"); + yield hoverContainer("p", inspector); + isVisible = yield testActor.isHighlighting(); + ok(isVisible, "the highlighter is visible again"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-hover_03.js b/devtools/client/inspector/test/browser_inspector_highlighter-hover_03.js new file mode 100644 index 000000000..fcd88be7f --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-hover_03.js @@ -0,0 +1,55 @@ +/* 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 once a node has been hovered over and marked as such, if it is +// navigated away using the keyboard, the highlighter moves to the new node, and +// if it is then navigated back to, it is briefly highlighted again + +const TEST_PAGE = "data:text/html;charset=utf-8," + + "<p id=\"one\">one</p><p id=\"two\">two</p>"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_PAGE); + + info("Making sure the markup-view frame is focused"); + inspector.markup._frame.focus(); + + // Mock the highlighter to easily track which node gets highlighted. + // We don't need to test here that the highlighter is actually visible, we + // just care about whether the markup-view asks it to be shown + let highlightedNode = null; + inspector.toolbox._highlighter.showBoxModel = function (nodeFront) { + highlightedNode = nodeFront; + return promise.resolve(); + }; + inspector.toolbox._highlighter.hideBoxModel = function () { + return promise.resolve(); + }; + + function* isHighlighting(selector, desc) { + let nodeFront = yield getNodeFront(selector, inspector); + is(highlightedNode, nodeFront, desc); + } + + info("Hover over <p#one> line in the markup-view"); + yield hoverContainer("#one", inspector); + yield isHighlighting("#one", "<p#one> is highlighted"); + + info("Navigate to <p#two> with the keyboard"); + let onUpdated = inspector.once("inspector-updated"); + EventUtils.synthesizeKey("VK_DOWN", {}, inspector.panelWin); + yield onUpdated; + onUpdated = inspector.once("inspector-updated"); + EventUtils.synthesizeKey("VK_DOWN", {}, inspector.panelWin); + yield onUpdated; + yield isHighlighting("#two", "<p#two> is highlighted"); + + info("Navigate back to <p#one> with the keyboard"); + onUpdated = inspector.once("inspector-updated"); + EventUtils.synthesizeKey("VK_UP", {}, inspector.panelWin); + yield onUpdated; + yield isHighlighting("#one", "<p#one> is highlighted again"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-iframes_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-iframes_01.js new file mode 100644 index 000000000..6475937c4 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-iframes_01.js @@ -0,0 +1,64 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// Testing that moving the mouse over the document with the element picker +// started highlights nodes + +const NESTED_FRAME_SRC = "data:text/html;charset=utf-8," + + "nested iframe<div>nested div</div>"; + +const OUTER_FRAME_SRC = "data:text/html;charset=utf-8," + + "little frame<div>little div</div>" + + "<iframe src='" + NESTED_FRAME_SRC + "' />"; + +const TEST_URI = "data:text/html;charset=utf-8," + + "iframe tests for inspector" + + "<iframe src=\"" + OUTER_FRAME_SRC + "\" />"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URI); + let outerFrameDiv = ["iframe", "div"]; + let innerFrameDiv = ["iframe", "iframe", "div"]; + + info("Waiting for element picker to activate."); + yield startPicker(inspector.toolbox); + + info("Moving mouse over outerFrameDiv"); + yield moveMouseOver(outerFrameDiv); + ok((yield testActor.assertHighlightedNode(outerFrameDiv)), + "outerFrameDiv is highlighted."); + + info("Moving mouse over innerFrameDiv"); + yield moveMouseOver(innerFrameDiv); + ok((yield testActor.assertHighlightedNode(innerFrameDiv)), + "innerFrameDiv is highlighted."); + + info("Selecting root node"); + yield selectNode(inspector.walker.rootNode, inspector); + + info("Selecting an element from the nested iframe directly"); + let innerFrameFront = yield getNodeFrontInFrame("iframe", "iframe", + inspector); + let innerFrameDivFront = yield getNodeFrontInFrame("div", innerFrameFront, + inspector); + yield selectNode(innerFrameDivFront, inspector); + + is(inspector.breadcrumbs.nodeHierarchy.length, 9, + "Breadcrumbs have 9 items."); + + info("Waiting for element picker to deactivate."); + yield inspector.toolbox.highlighterUtils.stopPicker(); + + function moveMouseOver(selector) { + info("Waiting for element " + selector + " to be highlighted"); + testActor.synthesizeMouse({ + selector: selector, + options: {type: "mousemove"}, + center: true + }).then(() => inspector.toolbox.once("picker-node-hovered")); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-iframes_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-iframes_02.js new file mode 100644 index 000000000..12f44ce32 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-iframes_02.js @@ -0,0 +1,59 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// Test that the highlighter is correctly positioned when switching context +// to an iframe that has an offset from the parent viewport (eg. 100px margin) + +const TEST_URI = "data:text/html;charset=utf-8," + + "<div id=\"outer\"></div>" + + "<iframe style='margin:100px' src='data:text/html," + + "<div id=\"inner\">Look I am here!</div>'>"; + +add_task(function* () { + info("Enable command-button-frames preference setting"); + Services.prefs.setBoolPref("devtools.command-button-frames.enabled", true); + let {inspector, toolbox, testActor} = yield openInspectorForURL(TEST_URI); + + info("Switch to the iframe context."); + yield switchToFrameContext(1, toolbox, inspector); + + info("Check navigation was successful."); + let hasOuterNode = yield testActor.hasNode("#outer"); + ok(!hasOuterNode, "Check testActor has no access to outer element"); + let hasTestNode = yield testActor.hasNode("#inner"); + ok(hasTestNode, "Check testActor has access to inner element"); + + info("Check highlighting is correct after switching iframe context"); + yield selectAndHighlightNode("#inner", inspector); + let isHighlightCorrect = yield testActor.assertHighlightedNode("#inner"); + ok(isHighlightCorrect, "The selected node is properly highlighted."); + + info("Cleanup command-button-frames preferences."); + Services.prefs.clearUserPref("devtools.command-button-frames.enabled"); +}); + +/** + * Helper designed to switch context to another frame at the provided index. + * Returns a promise that will resolve when the navigation is complete. + * @return {Promise} + */ +function* switchToFrameContext(frameIndex, toolbox, inspector) { + // Open frame menu and wait till it's available on the screen. + let btn = toolbox.doc.getElementById("command-button-frames"); + let menu = toolbox.showFramesMenu({target: btn}); + yield once(menu, "open"); + + info("Select the iframe in the frame list."); + let newRoot = inspector.once("new-root"); + + menu.items[frameIndex].click(); + + yield newRoot; + yield inspector.once("inspector-updated"); + + info("Navigation to the iframe is done."); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-inline.js b/devtools/client/inspector/test/browser_inspector_highlighter-inline.js new file mode 100644 index 000000000..4e39a92f9 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-inline.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +requestLongerTimeout(2); + +// Test that highlighting various inline boxes displays the right number of +// polygons in the page. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_inline.html"; +const TEST_DATA = [ + "body", + "h1", + "h2", + "h2 em", + "p", + "p span", + // The following test case used to fail. See bug 1139925. + "[dir=rtl] > span" +]; + +add_task(function* () { + info("Loading the test document and opening the inspector"); + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + for (let selector of TEST_DATA) { + info("Selecting and highlighting node " + selector); + yield selectAndHighlightNode(selector, inspector); + + info("Get all quads for this node"); + let data = yield testActor.getAllAdjustedQuads(selector); + + info("Iterate over the box-model regions and verify that the highlighter " + + "is correct"); + for (let region of ["margin", "border", "padding", "content"]) { + let {points} = yield testActor.getHighlighterRegionPath(region); + is(points.length, data[region].length, "The highlighter's " + region + + " path defines the correct number of boxes"); + } + + info("Verify that the guides define a rectangle that contains all " + + "content boxes"); + + let expectedContentRect = { + p1: {x: Infinity, y: Infinity}, + p2: {x: -Infinity, y: Infinity}, + p3: {x: -Infinity, y: -Infinity}, + p4: {x: Infinity, y: -Infinity} + }; + for (let {p1, p2, p3, p4} of data.content) { + expectedContentRect.p1.x = Math.min(expectedContentRect.p1.x, p1.x); + expectedContentRect.p1.y = Math.min(expectedContentRect.p1.y, p1.y); + expectedContentRect.p2.x = Math.max(expectedContentRect.p2.x, p2.x); + expectedContentRect.p2.y = Math.min(expectedContentRect.p2.y, p2.y); + expectedContentRect.p3.x = Math.max(expectedContentRect.p3.x, p3.x); + expectedContentRect.p3.y = Math.max(expectedContentRect.p3.y, p3.y); + expectedContentRect.p4.x = Math.min(expectedContentRect.p4.x, p4.x); + expectedContentRect.p4.y = Math.max(expectedContentRect.p4.y, p4.y); + } + + let contentRect = yield testActor.getGuidesRectangle(); + + for (let point of ["p1", "p2", "p3", "p4"]) { + is((contentRect[point].x), + (expectedContentRect[point].x), + "x coordinate of point " + point + + " of the content rectangle defined by the outer guides is correct"); + is((contentRect[point].y), + (expectedContentRect[point].y), + "y coordinate of point " + point + + " of the content rectangle defined by the outer guides is correct"); + } + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_01.js new file mode 100644 index 000000000..100dd1e6e --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_01.js @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that the keybindings for Picker work alright + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_dom.html"; + +add_task(function* () { + let {inspector, toolbox, testActor} = yield openInspectorForURL(TEST_URL); + + yield startPicker(toolbox); + + info("Selecting the simple-div1 DIV"); + yield moveMouseOver("#simple-div1"); + + ok((yield testActor.assertHighlightedNode("#simple-div1")), + "The highlighter shows #simple-div1. OK."); + + // First Child selection + info("Testing first-child selection."); + + yield doKeyHover({key: "VK_RIGHT", options: {}}); + ok((yield testActor.assertHighlightedNode("#useless-para")), + "The highlighter shows #useless-para. OK."); + + info("Selecting the useful-para paragraph DIV"); + yield moveMouseOver("#useful-para"); + ok((yield testActor.assertHighlightedNode("#useful-para")), + "The highlighter shows #useful-para. OK."); + + yield doKeyHover({key: "VK_RIGHT", options: {}}); + ok((yield testActor.assertHighlightedNode("#bold")), + "The highlighter shows #bold. OK."); + + info("Going back up to the simple-div1 DIV"); + yield doKeyHover({key: "VK_LEFT", options: {}}); + yield doKeyHover({key: "VK_LEFT", options: {}}); + ok((yield testActor.assertHighlightedNode("#simple-div1")), + "The highlighter shows #simple-div1. OK."); + + info("First child selection test Passed."); + + info("Stopping the picker"); + yield toolbox.highlighterUtils.stopPicker(); + + function doKeyHover(args) { + info("Key pressed. Waiting for element to be highlighted/hovered"); + testActor.synthesizeKey(args); + return inspector.toolbox.once("picker-node-hovered"); + } + + function moveMouseOver(selector) { + info("Waiting for element " + selector + " to be highlighted"); + testActor.synthesizeMouse({ + options: {type: "mousemove"}, + center: true, + selector: selector + }); + return inspector.toolbox.once("picker-node-hovered"); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_02.js new file mode 100644 index 000000000..96d5449f9 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_02.js @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that the keybindings for Picker work alright + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_dom.html"; + +add_task(function* () { + let {inspector, toolbox, testActor} = yield openInspectorForURL(TEST_URL); + + yield startPicker(toolbox); + + // Previously chosen child memory + info("Testing whether previously chosen child is remembered"); + + info("Selecting the ahoy paragraph DIV"); + yield moveMouseOver("#ahoy"); + + yield doKeyHover({key: "VK_LEFT", options: {}}); + ok((yield testActor.assertHighlightedNode("#simple-div2")), + "The highlighter shows #simple-div2. OK."); + + yield doKeyHover({key: "VK_RIGHT", options: {}}); + ok((yield testActor.assertHighlightedNode("#ahoy")), + "The highlighter shows #ahoy. OK."); + + info("Going back up to the complex-div DIV"); + yield doKeyHover({key: "VK_LEFT", options: {}}); + yield doKeyHover({key: "VK_LEFT", options: {}}); + ok((yield testActor.assertHighlightedNode("#complex-div")), + "The highlighter shows #complex-div. OK."); + + yield doKeyHover({key: "VK_RIGHT", options: {}}); + ok((yield testActor.assertHighlightedNode("#simple-div2")), + "The highlighter shows #simple-div2. OK."); + + info("Previously chosen child is remembered. Passed."); + + info("Stopping the picker"); + yield toolbox.highlighterUtils.stopPicker(); + + function doKeyHover(args) { + info("Key pressed. Waiting for element to be highlighted/hovered"); + let onHighlighterReady = toolbox.once("highlighter-ready"); + let onPickerNodeHovered = inspector.toolbox.once("picker-node-hovered"); + testActor.synthesizeKey(args); + return promise.all([onHighlighterReady, onPickerNodeHovered]); + } + + function moveMouseOver(selector) { + info("Waiting for element " + selector + " to be highlighted"); + let onHighlighterReady = toolbox.once("highlighter-ready"); + let onPickerNodeHovered = inspector.toolbox.once("picker-node-hovered"); + testActor.synthesizeMouse({ + options: {type: "mousemove"}, + center: true, + selector: selector + }); + return promise.all([onHighlighterReady, onPickerNodeHovered]); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_03.js b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_03.js new file mode 100644 index 000000000..99a2316bb --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_03.js @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that the keybindings for Picker work alright + +const IS_OSX = Services.appinfo.OS === "Darwin"; +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_dom.html"; + +add_task(function* () { + let {inspector, toolbox, testActor} = yield openInspectorForURL(TEST_URL); + + yield startPicker(toolbox); + yield moveMouseOver("#another"); + + info("Testing enter/return key as pick-node command"); + yield doKeyPick({key: "VK_RETURN", options: {}}); + is(inspector.selection.nodeFront.id, "another", + "The #another node was selected. Passed."); + + info("Testing escape key as cancel-picker command"); + yield startPicker(toolbox); + yield moveMouseOver("#ahoy"); + yield doKeyStop({key: "VK_ESCAPE", options: {}}); + is(inspector.selection.nodeFront.id, "another", + "The #another DIV is still selected. Passed."); + + info("Testing Ctrl+Shift+C shortcut as cancel-picker command"); + yield startPicker(toolbox); + yield moveMouseOver("#ahoy"); + let shortcutOpts = {key: "VK_C", options: {}}; + if (IS_OSX) { + shortcutOpts.options.metaKey = true; + shortcutOpts.options.altKey = true; + } else { + shortcutOpts.options.ctrlKey = true; + shortcutOpts.options.shiftKey = true; + } + yield doKeyStop(shortcutOpts); + is(inspector.selection.nodeFront.id, "another", + "The #another DIV is still selected. Passed."); + + function doKeyPick(args) { + info("Key pressed. Waiting for element to be picked"); + testActor.synthesizeKey(args); + return promise.all([ + inspector.selection.once("new-node-front"), + inspector.once("inspector-updated") + ]); + } + + function doKeyStop(args) { + info("Key pressed. Waiting for picker to be canceled"); + testActor.synthesizeKey(args); + return inspector.toolbox.once("picker-stopped"); + } + + function moveMouseOver(selector) { + info("Waiting for element " + selector + " to be highlighted"); + let onHighlighterReady = toolbox.once("highlighter-ready"); + let onPickerNodeHovered = inspector.toolbox.once("picker-node-hovered"); + testActor.synthesizeMouse({ + options: {type: "mousemove"}, + center: true, + selector: selector + }); + return promise.all([onHighlighterReady, onPickerNodeHovered]); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_04.js b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_04.js new file mode 100644 index 000000000..f53ca8ee6 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-keybinding_04.js @@ -0,0 +1,46 @@ +/* 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"; + +// Tests that pressing ESC twice while in picker mode first stops the picker and +// then opens the split-console (see bug 988278). + +const TEST_URL = "data:text/html;charset=utf8,<div></div>"; + +add_task(function* () { + let {inspector, toolbox, testActor} = yield openInspectorForURL(TEST_URL); + + yield startPicker(toolbox); + + info("Start using the picker by hovering over nodes"); + let onHover = toolbox.once("picker-node-hovered"); + testActor.synthesizeMouse({ + options: {type: "mousemove"}, + center: true, + selector: "div" + }); + yield onHover; + + info("Press escape and wait for the picker to stop"); + let onPickerStopped = toolbox.once("picker-stopped"); + testActor.synthesizeKey({ + key: "VK_ESCAPE", + options: {} + }); + yield onPickerStopped; + + info("Press escape again and wait for the split console to open"); + let onSplitConsole = toolbox.once("split-console"); + let onConsoleReady = toolbox.once("webconsole-ready"); + // The escape key is synthesized in the main process, which is where the focus + // should be after the picker was stopped. + EventUtils.synthesizeKey("VK_ESCAPE", {}, inspector.panelWin); + yield onSplitConsole; + yield onConsoleReady; + ok(toolbox.splitConsole, "The split console is shown."); + + // Hide the split console. + yield toolbox.toggleSplitConsole(); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-measure_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-measure_01.js new file mode 100644 index 000000000..b7f20e2fb --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-measure_01.js @@ -0,0 +1,88 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URL = `data:text/html;charset=utf-8, + <div style=' + position:absolute; + left: 0; + top: 0; + width: 40000px; + height: 8000px'> + </div>`; + +const PREFIX = "measuring-tool-highlighter-"; +const HIGHLIGHTER_TYPE = "MeasuringToolHighlighter"; + +const X = 32; +const Y = 20; + +add_task(function* () { + let helper = yield openInspectorForURL(TEST_URL) + .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE)); + + let { finalize } = helper; + + helper.prefix = PREFIX; + + yield isHiddenByDefault(helper); + yield areLabelsHiddenByDefaultWhenShows(helper); + yield areLabelsProperlyDisplayedWhenMouseMoved(helper); + + yield finalize(); +}); + +function* isHiddenByDefault({isElementHidden}) { + info("Checking the highlighter is hidden by default"); + + let hidden = yield isElementHidden("elements"); + ok(hidden, "highlighter's root is hidden by default"); + + hidden = yield isElementHidden("label-size"); + ok(hidden, "highlighter's label size is hidden by default"); + + hidden = yield isElementHidden("label-position"); + ok(hidden, "highlighter's label position is hidden by default"); +} + +function* areLabelsHiddenByDefaultWhenShows({isElementHidden, show}) { + info("Checking the highlighter is displayed when asked"); + + yield show(); + + let hidden = yield isElementHidden("elements"); + is(hidden, false, "highlighter is visible after show"); + + hidden = yield isElementHidden("label-size"); + ok(hidden, "label's size still hidden"); + + hidden = yield isElementHidden("label-position"); + ok(hidden, "label's position still hidden"); +} + +function* areLabelsProperlyDisplayedWhenMouseMoved({isElementHidden, + synthesizeMouse, getElementTextContent}) { + info("Checking labels are properly displayed when mouse moved"); + + yield synthesizeMouse({ + selector: ":root", + options: {type: "mousemove"}, + x: X, + y: Y + }); + + let hidden = yield isElementHidden("label-position"); + is(hidden, false, "label's position is displayed after the mouse is moved"); + + hidden = yield isElementHidden("label-size"); + ok(hidden, "label's size still hidden"); + + let text = yield getElementTextContent("label-position"); + + let [x, y] = text.replace(/ /g, "").split(/\n/); + + is(+x, X, "label's position shows the proper X coord"); + is(+y, Y, "label's position shows the proper Y coord"); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-measure_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-measure_02.js new file mode 100644 index 000000000..424cc183a --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-measure_02.js @@ -0,0 +1,130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URL = `data:text/html;charset=utf-8, + <div style=' + position:absolute; + left: 0; + top: 0; + width: 40000px; + height: 8000px'> + </div>`; + +const PREFIX = "measuring-tool-highlighter-"; +const HIGHLIGHTER_TYPE = "MeasuringToolHighlighter"; + +const SIDES = ["top", "right", "bottom", "left"]; + +const X = 32; +const Y = 20; +const WIDTH = 160; +const HEIGHT = 100; +const HYPOTENUSE = Math.hypot(WIDTH, HEIGHT).toFixed(2); + +add_task(function* () { + let helper = yield openInspectorForURL(TEST_URL) + .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE)); + + let { show, finalize } = helper; + + helper.prefix = PREFIX; + + yield show(); + + yield hasNoLabelsWhenStarts(helper); + yield hasSizeLabelWhenMoved(helper); + yield hasCorrectSizeLabelValue(helper); + yield hasSizeLabelAndGuidesWhenStops(helper); + yield hasCorrectSizeLabelValue(helper); + + yield finalize(); +}); + +function* hasNoLabelsWhenStarts({isElementHidden, synthesizeMouse}) { + info("Checking highlighter has no labels when we start to select"); + + yield synthesizeMouse({ + selector: ":root", + options: {type: "mousedown"}, + x: X, + y: Y + }); + + let hidden = yield isElementHidden("label-size"); + ok(hidden, "label's size still hidden"); + + hidden = yield isElementHidden("label-position"); + ok(hidden, "label's position still hidden"); + + info("Checking highlighter has no guides when we start to select"); + + let guidesHidden = true; + for (let side of SIDES) { + guidesHidden = guidesHidden && (yield isElementHidden("guide-" + side)); + } + + ok(guidesHidden, "guides are hidden during dragging"); +} + +function* hasSizeLabelWhenMoved({isElementHidden, synthesizeMouse}) { + info("Checking highlighter has size label when we select the area"); + + yield synthesizeMouse({ + selector: ":root", + options: {type: "mousemove"}, + x: X + WIDTH, + y: Y + HEIGHT + }); + + let hidden = yield isElementHidden("label-size"); + is(hidden, false, "label's size is visible during selection"); + + hidden = yield isElementHidden("label-position"); + ok(hidden, "label's position still hidden"); + + info("Checking highlighter has no guides when we select the area"); + + let guidesHidden = true; + for (let side of SIDES) { + guidesHidden = guidesHidden && (yield isElementHidden("guide-" + side)); + } + + ok(guidesHidden, "guides are hidden during selection"); +} + +function* hasSizeLabelAndGuidesWhenStops({isElementHidden, synthesizeMouse}) { + info("Checking highlighter has size label and guides when we stop"); + + yield synthesizeMouse({ + selector: ":root", + options: {type: "mouseup"}, + x: X + WIDTH, + y: Y + HEIGHT + }); + + let hidden = yield isElementHidden("label-size"); + is(hidden, false, "label's size is visible when the selection is done"); + + hidden = yield isElementHidden("label-position"); + ok(hidden, "label's position still hidden"); + + let guidesVisible = true; + for (let side of SIDES) { + guidesVisible = guidesVisible && !(yield isElementHidden("guide-" + side)); + } + + ok(guidesVisible, "guides are visible when the selection is done"); +} + +function* hasCorrectSizeLabelValue({getElementTextContent}) { + let text = yield getElementTextContent("label-size"); + + let [width, height, hypot] = text.match(/\d.*px/g); + + is(parseFloat(width), WIDTH, "width on label's size is correct"); + is(parseFloat(height), HEIGHT, "height on label's size is correct"); + is(parseFloat(hypot), HYPOTENUSE, "hypotenuse on label's size is correct"); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-options.js b/devtools/client/inspector/test/browser_inspector_highlighter-options.js new file mode 100644 index 000000000..65a6ec4b0 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-options.js @@ -0,0 +1,204 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Check that the box-model highlighter supports configuration options + +const TEST_URL = ` + <body style="padding:2em;"> + <div style="width:100px;height:100px;padding:2em; + border:.5em solid black;margin:1em;">test</div> + </body> +`; + +// Test data format: +// - desc: a string that will be output to the console. +// - options: json object to be passed as options to the highlighter. +// - checkHighlighter: a generator (async) function that should check the +// highlighter is correct. +const TEST_DATA = [ + { + desc: "Guides and infobar should be shown by default", + options: {}, + checkHighlighter: function* (testActor) { + let hidden = yield testActor.getHighlighterNodeAttribute( + "box-model-infobar-container", "hidden"); + ok(!hidden, "Node infobar is visible"); + + hidden = yield testActor.getHighlighterNodeAttribute( + "box-model-elements", "hidden"); + ok(!hidden, "SVG container is visible"); + + for (let side of ["top", "right", "bottom", "left"]) { + hidden = yield testActor.getHighlighterNodeAttribute( + "box-model-guide-" + side, "hidden"); + ok(!hidden, side + " guide is visible"); + } + } + }, + { + desc: "All regions should be shown by default", + options: {}, + checkHighlighter: function* (testActor) { + for (let region of ["margin", "border", "padding", "content"]) { + let {d} = yield testActor.getHighlighterRegionPath(region); + ok(d, "Region " + region + " has set coordinates"); + } + } + }, + { + desc: "Guides can be hidden", + options: {hideGuides: true}, + checkHighlighter: function* (testActor) { + for (let side of ["top", "right", "bottom", "left"]) { + let hidden = yield testActor.getHighlighterNodeAttribute( + "box-model-guide-" + side, "hidden"); + is(hidden, "true", side + " guide has been hidden"); + } + } + }, + { + desc: "Infobar can be hidden", + options: {hideInfoBar: true}, + checkHighlighter: function* (testActor) { + let hidden = yield testActor.getHighlighterNodeAttribute( + "box-model-infobar-container", "hidden"); + is(hidden, "true", "infobar has been hidden"); + } + }, + { + desc: "One region only can be shown (1)", + options: {showOnly: "content"}, + checkHighlighter: function* (testActor) { + let {d} = yield testActor.getHighlighterRegionPath("margin"); + ok(!d, "margin region is hidden"); + + ({d} = yield testActor.getHighlighterRegionPath("border")); + ok(!d, "border region is hidden"); + + ({d} = yield testActor.getHighlighterRegionPath("padding")); + ok(!d, "padding region is hidden"); + + ({d} = yield testActor.getHighlighterRegionPath("content")); + ok(d, "content region is shown"); + } + }, + { + desc: "One region only can be shown (2)", + options: {showOnly: "margin"}, + checkHighlighter: function* (testActor) { + let {d} = yield testActor.getHighlighterRegionPath("margin"); + ok(d, "margin region is shown"); + + ({d} = yield testActor.getHighlighterRegionPath("border")); + ok(!d, "border region is hidden"); + + ({d} = yield testActor.getHighlighterRegionPath("padding")); + ok(!d, "padding region is hidden"); + + ({d} = yield testActor.getHighlighterRegionPath("content")); + ok(!d, "content region is hidden"); + } + }, + { + desc: "Guides can be drawn around a given region (1)", + options: {region: "padding"}, + checkHighlighter: function* (testActor) { + let topY1 = yield testActor.getHighlighterNodeAttribute( + "box-model-guide-top", "y1"); + let rightX1 = yield testActor.getHighlighterNodeAttribute( + "box-model-guide-right", "x1"); + let bottomY1 = yield testActor.getHighlighterNodeAttribute( + "box-model-guide-bottom", "y1"); + let leftX1 = yield testActor.getHighlighterNodeAttribute( + "box-model-guide-left", "x1"); + + let {points} = yield testActor.getHighlighterRegionPath("padding"); + points = points[0]; + + is(Math.ceil(topY1), points[0][1], "Top guide's y1 is correct"); + is(Math.floor(rightX1), points[1][0], "Right guide's x1 is correct"); + is(Math.floor(bottomY1), points[2][1], "Bottom guide's y1 is correct"); + is(Math.ceil(leftX1), points[3][0], "Left guide's x1 is correct"); + } + }, + { + desc: "Guides can be drawn around a given region (2)", + options: {region: "margin"}, + checkHighlighter: function* (testActor) { + let topY1 = yield testActor.getHighlighterNodeAttribute( + "box-model-guide-top", "y1"); + let rightX1 = yield testActor.getHighlighterNodeAttribute( + "box-model-guide-right", "x1"); + let bottomY1 = yield testActor.getHighlighterNodeAttribute( + "box-model-guide-bottom", "y1"); + let leftX1 = yield testActor.getHighlighterNodeAttribute( + "box-model-guide-left", "x1"); + + let {points} = yield testActor.getHighlighterRegionPath("margin"); + points = points[0]; + + is(Math.ceil(topY1), points[0][1], "Top guide's y1 is correct"); + is(Math.floor(rightX1), points[1][0], "Right guide's x1 is correct"); + is(Math.floor(bottomY1), points[2][1], "Bottom guide's y1 is correct"); + is(Math.ceil(leftX1), points[3][0], "Left guide's x1 is correct"); + } + }, + { + desc: "When showOnly is used, other regions can be faded", + options: {showOnly: "margin", onlyRegionArea: true}, + checkHighlighter: function* (testActor) { + for (let region of ["margin", "border", "padding", "content"]) { + let {d} = yield testActor.getHighlighterRegionPath(region); + ok(d, "Region " + region + " is shown (it has a d attribute)"); + + let faded = yield testActor.getHighlighterNodeAttribute( + "box-model-" + region, "faded"); + if (region === "margin") { + ok(!faded, "The margin region is not faded"); + } else { + is(faded, "true", "Region " + region + " is faded"); + } + } + } + }, + { + desc: "When showOnly is used, other regions can be faded (2)", + options: {showOnly: "padding", onlyRegionArea: true}, + checkHighlighter: function* (testActor) { + for (let region of ["margin", "border", "padding", "content"]) { + let {d} = yield testActor.getHighlighterRegionPath(region); + ok(d, "Region " + region + " is shown (it has a d attribute)"); + + let faded = yield testActor.getHighlighterNodeAttribute( + "box-model-" + region, "faded"); + if (region === "padding") { + ok(!faded, "The padding region is not faded"); + } else { + is(faded, "true", "Region " + region + " is faded"); + } + } + } + } +]; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURI(TEST_URL)); + + let divFront = yield getNodeFront("div", inspector); + + for (let {desc, options, checkHighlighter} of TEST_DATA) { + info("Running test: " + desc); + + info("Show the box-model highlighter with options " + options); + yield inspector.highlighter.showBoxModel(divFront, options); + + yield checkHighlighter(testActor); + + info("Hide the box-model highlighter"); + yield inspector.highlighter.hideBoxModel(); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-preview.js b/devtools/client/inspector/test/browser_inspector_highlighter-preview.js new file mode 100644 index 000000000..5f525786a --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-preview.js @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that the highlighter is correctly displayed and picker mode is not stopped after +// a shift-click (preview) + +const TEST_URI = `data:text/html;charset=utf-8, + <p id="one">one</p><p id="two">two</p><p id="three">three</p>`; + +add_task(function* () { + let {inspector, toolbox, testActor} = yield openInspectorForURL(TEST_URI); + + let body = yield getNodeFront("body", inspector); + is(inspector.selection.nodeFront, body, "By default the body node is selected"); + + info("Start the element picker"); + yield startPicker(toolbox); + + info("Shift-clicking element #one should select it but keep the picker ON"); + yield clickElement("#one", testActor, inspector, true); + yield checkElementSelected("#one", inspector); + checkPickerMode(toolbox, true); + + info("Shift-clicking element #two should select it but keep the picker ON"); + yield clickElement("#two", testActor, inspector, true); + yield checkElementSelected("#two", inspector); + checkPickerMode(toolbox, true); + + info("Clicking element #three should select it and turn the picker OFF"); + yield clickElement("#three", testActor, inspector, false); + yield checkElementSelected("#three", inspector); + checkPickerMode(toolbox, false); +}); + +function* clickElement(selector, testActor, inspector, isShift) { + let onSelectionChanged = inspector.once("inspector-updated"); + yield testActor.synthesizeMouse({ + selector: selector, + center: true, + options: { shiftKey: isShift } + }); + yield onSelectionChanged; +} + +function* checkElementSelected(selector, inspector) { + let el = yield getNodeFront(selector, inspector); + is(inspector.selection.nodeFront, el, `The element ${selector} is now selected`); +} + +function checkPickerMode(toolbox, isOn) { + let pickerButton = toolbox.doc.querySelector("#command-button-pick"); + is(pickerButton.hasAttribute("checked"), isOn, "The picker mode is correct"); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-rect_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-rect_01.js new file mode 100644 index 000000000..9645e25d9 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-rect_01.js @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that the custom rect highlighter provides the right API, ensures that +// the input is valid and that it does create a box with the right dimensions, +// at the right position. + +const TEST_URL = "data:text/html;charset=utf-8,Rect Highlighter Test"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + let front = inspector.inspector; + let highlighter = yield front.getHighlighterByType("RectHighlighter"); + let body = yield getNodeFront("body", inspector); + + info("Make sure the highlighter returned is correct"); + + ok(highlighter, "The RectHighlighter custom type was created"); + is(highlighter.typeName, "customhighlighter", + "The RectHighlighter has the right type"); + ok(highlighter.show && highlighter.hide, + "The RectHighlighter has the expected show/hide methods"); + + info("Check that the highlighter is hidden by default"); + + let hidden = yield testActor.getHighlighterNodeAttribute( + "highlighted-rect", "hidden", highlighter); + is(hidden, "true", "The highlighter is hidden by default"); + + info("Check that nothing is shown if no rect is passed"); + + yield highlighter.show(body); + hidden = yield testActor.getHighlighterNodeAttribute( + "highlighted-rect", "hidden", highlighter); + is(hidden, "true", "The highlighter is hidden when no rect is passed"); + + info("Check that nothing is shown if rect is incomplete or invalid"); + + yield highlighter.show(body, { + rect: {x: 0, y: 0} + }); + hidden = yield testActor.getHighlighterNodeAttribute( + "highlighted-rect", "hidden", highlighter); + is(hidden, "true", "The highlighter is hidden when the rect is incomplete"); + + yield highlighter.show(body, { + rect: {x: 0, y: 0, width: -Infinity, height: 0} + }); + hidden = yield testActor.getHighlighterNodeAttribute( + "highlighted-rect", "hidden", highlighter); + is(hidden, "true", "The highlighter is hidden when the rect is invalid (1)"); + + yield highlighter.show(body, { + rect: {x: 0, y: 0, width: 5, height: -45} + }); + hidden = yield testActor.getHighlighterNodeAttribute( + "highlighted-rect", "hidden", highlighter); + is(hidden, "true", "The highlighter is hidden when the rect is invalid (2)"); + + yield highlighter.show(body, { + rect: {x: "test", y: 0, width: 5, height: 5} + }); + hidden = yield testActor.getHighlighterNodeAttribute( + "highlighted-rect", "hidden", highlighter); + is(hidden, "true", "The highlighter is hidden when the rect is invalid (3)"); + + info("Check that the highlighter is displayed when valid options are passed"); + + yield highlighter.show(body, { + rect: {x: 5, y: 5, width: 50, height: 50} + }); + hidden = yield testActor.getHighlighterNodeAttribute( + "highlighted-rect", "hidden", highlighter); + ok(!hidden, "The highlighter is displayed"); + let style = yield testActor.getHighlighterNodeAttribute( + "highlighted-rect", "style", highlighter); + is(style, "left:5px;top:5px;width:50px;height:50px;", + "The highlighter is positioned correctly"); + + info("Check that the highlighter can be displayed at x=0 y=0"); + + yield highlighter.show(body, { + rect: {x: 0, y: 0, width: 50, height: 50} + }); + hidden = yield testActor.getHighlighterNodeAttribute( + "highlighted-rect", "hidden", highlighter); + ok(!hidden, "The highlighter is displayed when x=0 and y=0"); + style = yield testActor.getHighlighterNodeAttribute( + "highlighted-rect", "style", highlighter); + is(style, "left:0px;top:0px;width:50px;height:50px;", + "The highlighter is positioned correctly"); + + info("Check that the highlighter is hidden when dimensions are 0"); + + yield highlighter.show(body, { + rect: {x: 0, y: 0, width: 0, height: 0} + }); + hidden = yield testActor.getHighlighterNodeAttribute( + "highlighted-rect", "hidden", highlighter); + is(hidden, "true", "The highlighter is hidden width and height are 0"); + + info("Check that a fill color can be passed"); + + yield highlighter.show(body, { + rect: {x: 100, y: 200, width: 500, height: 200}, + fill: "red" + }); + hidden = yield testActor.getHighlighterNodeAttribute( + "highlighted-rect", "hidden", highlighter); + ok(!hidden, "The highlighter is displayed"); + style = yield testActor.getHighlighterNodeAttribute( + "highlighted-rect", "style", highlighter); + is(style, "left:100px;top:200px;width:500px;height:200px;background:red;", + "The highlighter has the right background color"); + + yield highlighter.hide(); + yield highlighter.finalize(); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-rect_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-rect_02.js new file mode 100644 index 000000000..716a5deda --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-rect_02.js @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that the custom rect highlighter positions the rectangle relative to the +// viewport of the context node we pass to it. + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_rect.html"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + let front = inspector.inspector; + let highlighter = yield front.getHighlighterByType("RectHighlighter"); + + info("Showing the rect highlighter in the context of the iframe"); + + // Get the reference to a context node inside the iframe + let childBody = yield getNodeFrontInFrame("body", "iframe", inspector); + yield highlighter.show(childBody, { + rect: {x: 50, y: 50, width: 100, height: 100} + }); + + let style = yield testActor.getHighlighterNodeAttribute("highlighted-rect", + "style", highlighter); + + // The parent body has margin=50px and border=10px + // The parent iframe also has margin=50px and border=10px + // = 50 + 10 + 50 + 10 = 120px + // The rect is aat x=50 and y=50, so left and top should be 170px + is(style, "left:170px;top:170px;width:100px;height:100px;", + "The highlighter is correctly positioned"); + + yield highlighter.hide(); + yield highlighter.finalize(); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-rulers_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-rulers_01.js new file mode 100644 index 000000000..f5deb0222 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-rulers_01.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test the creation of the geometry highlighter elements. + +const TEST_URL = "data:text/html;charset=utf-8," + + "<div style='position:absolute;left: 0; top: 0; " + + "width: 40000px; height: 8000px'></div>"; + +const ID = "rulers-highlighter-"; + +// Maximum size, in pixel, for the horizontal ruler and vertical ruler +// used by RulersHighlighter +const RULERS_MAX_X_AXIS = 10000; +const RULERS_MAX_Y_AXIS = 15000; +// Number of steps after we add a text in RulersHighliter; +// currently the unit is in pixel. +const RULERS_TEXT_STEP = 100; + +add_task(function* () { + let { inspector, testActor } = yield openInspectorForURL(TEST_URL); + let front = inspector.inspector; + + let highlighter = yield front.getHighlighterByType("RulersHighlighter"); + + yield isHiddenByDefault(highlighter, inspector, testActor); + yield hasRightLabelsContent(highlighter, inspector, testActor); + + yield highlighter.finalize(); +}); + +function* isHiddenByDefault(highlighterFront, inspector, testActor) { + info("Checking the highlighter is hidden by default"); + + let hidden = yield testActor.getHighlighterNodeAttribute( + ID + "elements", "hidden", highlighterFront); + + is(hidden, "true", "highlighter is hidden by default"); + + info("Checking the highlighter is displayed when asked"); + // the rulers doesn't need any node, but as highligher it seems mandatory + // ones, so the body is given + let body = yield getNodeFront("body", inspector); + yield highlighterFront.show(body); + + hidden = yield testActor.getHighlighterNodeAttribute( + ID + "elements", "hidden", highlighterFront); + + isnot(hidden, "true", "highlighter is visible after show"); +} + +function* hasRightLabelsContent(highlighterFront, inspector, testActor) { + info("Checking the rulers have the proper text, based on rulers' size"); + + let contentX = yield testActor.getHighlighterNodeTextContent( + `${ID}x-axis-text`, highlighterFront); + let contentY = yield testActor.getHighlighterNodeTextContent( + `${ID}y-axis-text`, highlighterFront); + + let expectedX = ""; + for (let i = RULERS_TEXT_STEP; i < RULERS_MAX_X_AXIS; i += RULERS_TEXT_STEP) { + expectedX += i; + } + + is(contentX, expectedX, "x axis text content is correct"); + + let expectedY = ""; + for (let i = RULERS_TEXT_STEP; i < RULERS_MAX_Y_AXIS; i += RULERS_TEXT_STEP) { + expectedY += i; + } + + is(contentY, expectedY, "y axis text content is correct"); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-rulers_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-rulers_02.js new file mode 100644 index 000000000..fac1c801e --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-rulers_02.js @@ -0,0 +1,103 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test the creation of the geometry highlighter elements. + +const TEST_URL = "data:text/html;charset=utf-8," + + "<div style='position:absolute;left: 0; top: 0; " + + "width: 40000px; height: 8000px'></div>"; + +const ID = "rulers-highlighter-"; + +add_task(function* () { + let { inspector, testActor } = yield openInspectorForURL(TEST_URL); + let front = inspector.inspector; + + let highlighter = yield front.getHighlighterByType("RulersHighlighter"); + + // the rulers doesn't need any node, but as highligher it seems mandatory + // ones, so the body is given + let body = yield getNodeFront("body", inspector); + yield highlighter.show(body); + + yield isUpdatedAfterScroll(highlighter, inspector, testActor); + + yield highlighter.finalize(); +}); + +function* isUpdatedAfterScroll(highlighterFront, inspector, testActor) { + info("Check the rulers' position by default"); + + let xAxisRulerTransform = yield testActor.getHighlighterNodeAttribute( + `${ID}x-axis-ruler`, "transform", highlighterFront); + let xAxisTextTransform = yield testActor.getHighlighterNodeAttribute( + `${ID}x-axis-text`, "transform", highlighterFront); + let yAxisRulerTransform = yield testActor.getHighlighterNodeAttribute( + `${ID}y-axis-ruler`, "transform", highlighterFront); + let yAxisTextTransform = yield testActor.getHighlighterNodeAttribute( + `${ID}y-axis-text`, "transform", highlighterFront); + + is(xAxisRulerTransform, null, "x axis ruler is positioned properly"); + is(xAxisTextTransform, null, "x axis text are positioned properly"); + is(yAxisRulerTransform, null, "y axis ruler is positioned properly"); + is(yAxisTextTransform, null, "y axis text are positioned properly"); + + info("Ask the content window to scroll to specific coords"); + + let x = 200, y = 300; + + let data = yield testActor.scrollWindow(x, y); + + is(data.x, x, "window scrolled properly horizontally"); + is(data.y, y, "window scrolled properly vertically"); + + info("Check the rulers are properly positioned after the scrolling"); + + xAxisRulerTransform = yield testActor.getHighlighterNodeAttribute( + `${ID}x-axis-ruler`, "transform", highlighterFront); + xAxisTextTransform = yield testActor.getHighlighterNodeAttribute( + `${ID}x-axis-text`, "transform", highlighterFront); + yAxisRulerTransform = yield testActor.getHighlighterNodeAttribute( + `${ID}y-axis-ruler`, "transform", highlighterFront); + yAxisTextTransform = yield testActor.getHighlighterNodeAttribute( + `${ID}y-axis-text`, "transform", highlighterFront); + + is(xAxisRulerTransform, `translate(-${x})`, + "x axis ruler is positioned properly"); + is(xAxisTextTransform, `translate(-${x})`, + "x axis text are positioned properly"); + is(yAxisRulerTransform, `translate(0, -${y})`, + "y axis ruler is positioned properly"); + is(yAxisTextTransform, `translate(0, -${y})`, + "y axis text are positioned properly"); + + info("Ask the content window to scroll relative to the current position"); + + data = yield testActor.scrollWindow(-50, -60, true); + + is(data.x, x - 50, "window scrolled properly horizontally"); + is(data.y, y - 60, "window scrolled properly vertically"); + + info("Check the rulers are properly positioned after the relative scrolling"); + + xAxisRulerTransform = yield testActor.getHighlighterNodeAttribute( + `${ID}x-axis-ruler`, "transform", highlighterFront); + xAxisTextTransform = yield testActor.getHighlighterNodeAttribute( + `${ID}x-axis-text`, "transform", highlighterFront); + yAxisRulerTransform = yield testActor.getHighlighterNodeAttribute( + `${ID}y-axis-ruler`, "transform", highlighterFront); + yAxisTextTransform = yield testActor.getHighlighterNodeAttribute( + `${ID}y-axis-text`, "transform", highlighterFront); + + is(xAxisRulerTransform, `translate(-${x - 50})`, + "x axis ruler is positioned properly"); + is(xAxisTextTransform, `translate(-${x - 50})`, + "x axis text are positioned properly"); + is(yAxisRulerTransform, `translate(0, -${y - 60})`, + "y axis ruler is positioned properly"); + is(yAxisTextTransform, `translate(0, -${y - 60})`, + "y axis text are positioned properly"); +} diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-selector_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-selector_01.js new file mode 100644 index 000000000..4edfe6051 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-selector_01.js @@ -0,0 +1,63 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that the custom selector highlighter creates as many box-model +// highlighters as there are nodes that match the given selector + +const TEST_URL = "data:text/html;charset=utf-8," + + "<div id='test-node'>test node</div>" + + "<ul>" + + " <li class='item'>item</li>" + + " <li class='item'>item</li>" + + " <li class='item'>item</li>" + + " <li class='item'>item</li>" + + " <li class='item'>item</li>" + + "</ul>"; + +const TEST_DATA = [{ + selector: "#test-node", + containerCount: 1 +}, { + selector: null, + containerCount: 0, +}, { + selector: undefined, + containerCount: 0, +}, { + selector: ".invalid-class", + containerCount: 0 +}, { + selector: ".item", + containerCount: 5 +}, { + selector: "#test-node, ul, .item", + containerCount: 7 +}]; + +requestLongerTimeout(5); + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + let front = inspector.inspector; + let highlighter = yield front.getHighlighterByType("SelectorHighlighter"); + + let contextNode = yield getNodeFront("body", inspector); + + for (let {selector, containerCount} of TEST_DATA) { + info("Showing the highlighter on " + selector + ". Expecting " + + containerCount + " highlighter containers"); + + yield highlighter.show(contextNode, {selector}); + + let nb = yield testActor.getSelectorHighlighterBoxNb(highlighter.actorID); + ok(nb !== null, "The number of highlighters was retrieved"); + + is(nb, containerCount, "The correct number of highlighers were created"); + yield highlighter.hide(); + } + + yield highlighter.finalize(); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-selector_02.js b/devtools/client/inspector/test/browser_inspector_highlighter-selector_02.js new file mode 100644 index 000000000..85fcaeb1c --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-selector_02.js @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that the custom selector highlighter creates highlighters for nodes in +// the right frame. + +const FRAME_SRC = "data:text/html;charset=utf-8," + + "<div class=sub-level-node></div>"; + +const TEST_URL = "data:text/html;charset=utf-8," + + "<div class=root-level-node></div>" + + "<iframe src=\"" + FRAME_SRC + "\" />"; + +const TEST_DATA = [{ + selector: ".root-level-node", + containerCount: 1 +}, { + selector: ".sub-level-node", + containerCount: 0 +}, { + inIframe: true, + selector: ".root-level-node", + containerCount: 0 +}, { + inIframe: true, + selector: ".sub-level-node", + containerCount: 1 +}]; + +requestLongerTimeout(5); + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + let front = inspector.inspector; + let highlighter = yield front.getHighlighterByType("SelectorHighlighter"); + + for (let {inIframe, selector, containerCount} of TEST_DATA) { + info("Showing the highlighter on " + selector + ". Expecting " + + containerCount + " highlighter containers"); + + let contextNode; + if (inIframe) { + contextNode = yield getNodeFrontInFrame("body", "iframe", inspector); + } else { + contextNode = yield getNodeFront("body", inspector); + } + + yield highlighter.show(contextNode, {selector}); + + let nb = yield testActor.getSelectorHighlighterBoxNb(highlighter.actorID); + ok(nb !== null, "The number of highlighters was retrieved"); + + is(nb, containerCount, "The correct number of highlighers were created"); + yield highlighter.hide(); + } + + yield highlighter.finalize(); +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-xbl.js b/devtools/client/inspector/test/browser_inspector_highlighter-xbl.js new file mode 100644 index 000000000..2421fd3f3 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-xbl.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the picker works correctly with XBL anonymous nodes + +const TEST_URL = URL_ROOT + "doc_inspector_highlighter_xbl.xul"; + +add_task(function* () { + let {inspector, toolbox, testActor} = yield openInspectorForURL(TEST_URL); + + yield startPicker(toolbox); + + info("Selecting the scale"); + yield moveMouseOver("#scale"); + yield doKeyPick({key: "VK_RETURN", options: {}}); + is(inspector.selection.nodeFront.className, "scale-slider", + "The .scale-slider inside the scale was selected"); + + function doKeyPick(msg) { + info("Key pressed. Waiting for element to be picked"); + testActor.synthesizeKey(msg); + return promise.all([ + inspector.selection.once("new-node-front"), + inspector.once("inspector-updated") + ]); + } + + function moveMouseOver(selector) { + info("Waiting for element " + selector + " to be highlighted"); + testActor.synthesizeMouse({ + options: {type: "mousemove"}, + center: true, + selector: selector + }); + return inspector.toolbox.once("picker-node-hovered"); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-zoom.js b/devtools/client/inspector/test/browser_inspector_highlighter-zoom.js new file mode 100644 index 000000000..1919975ef --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_highlighter-zoom.js @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that the highlighter stays correctly positioned and has the right aspect +// ratio even when the page is zoomed in or out. + +const TEST_URL = "data:text/html;charset=utf-8,<div>zoom me</div>"; + +// TEST_LEVELS entries should contain the following properties: +// - level: the zoom level to test +// - expected: the style attribute value to check for on the root highlighter +// element. +const TEST_LEVELS = [{ + level: 2, + expected: "position:absolute;transform-origin:top left;" + + "transform:scale(0.5);width:200%;height:200%;" +}, { + level: 1, + expected: "position:absolute;width:100%;height:100%;" +}, { + level: .5, + expected: "position:absolute;transform-origin:top left;" + + "transform:scale(2);width:50%;height:50%;" +}]; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URL); + + info("Highlighting the test node"); + + yield hoverElement("div", inspector); + let isVisible = yield testActor.isHighlighting(); + ok(isVisible, "The highlighter is visible"); + + for (let {level, expected} of TEST_LEVELS) { + info("Zoom to level " + level + + " and check that the highlighter is correct"); + + yield testActor.zoomPageTo(level); + isVisible = yield testActor.isHighlighting(); + ok(isVisible, "The highlighter is still visible at zoom level " + level); + + yield testActor.isNodeCorrectlyHighlighted("div", is); + + info("Check that the highlighter root wrapper node was scaled down"); + + let style = yield getRootNodeStyle(testActor); + is(style, expected, "The style attribute of the root element is correct"); + } +}); + +function* hoverElement(selector, inspector) { + info("Hovering node " + selector + " in the markup view"); + let container = yield getContainerForSelector(selector, inspector); + yield hoverContainer(container, inspector); +} + +function* hoverContainer(container, inspector) { + let onHighlight = inspector.toolbox.once("node-highlight"); + EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"}, + inspector.markup.doc.defaultView); + yield onHighlight; +} + +function* getRootNodeStyle(testActor) { + let value = yield testActor.getHighlighterNodeAttribute( + "box-model-root", "style"); + return value; +} diff --git a/devtools/client/inspector/test/browser_inspector_iframe-navigation.js b/devtools/client/inspector/test/browser_inspector_iframe-navigation.js new file mode 100644 index 000000000..df638b5cb --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_iframe-navigation.js @@ -0,0 +1,43 @@ +/* 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 highlighter element picker still works through iframe +// navigations. + +const TEST_URI = "data:text/html;charset=utf-8," + + "<p>bug 699308 - test iframe navigation</p>" + + "<iframe src='data:text/html;charset=utf-8,hello world'></iframe>"; + +add_task(function* () { + let { toolbox, testActor } = yield openInspectorForURL(TEST_URI); + + info("Starting element picker."); + yield startPicker(toolbox); + + info("Waiting for highlighter to activate."); + let highlighterShowing = toolbox.once("highlighter-ready"); + testActor.synthesizeMouse({ + selector: "body", + options: {type: "mousemove"}, + x: 1, + y: 1 + }); + yield highlighterShowing; + + let isVisible = yield testActor.isHighlighting(); + ok(isVisible, "Inspector is highlighting."); + + yield testActor.reloadFrame("iframe"); + info("Frame reloaded. Reloading again."); + + yield testActor.reloadFrame("iframe"); + info("Frame reloaded twice."); + + isVisible = yield testActor.isHighlighting(); + ok(isVisible, "Inspector is highlighting after iframe nav."); + + info("Stopping element picker."); + yield toolbox.highlighterUtils.stopPicker(); +}); diff --git a/devtools/client/inspector/test/browser_inspector_infobar_01.js b/devtools/client/inspector/test/browser_inspector_infobar_01.js new file mode 100644 index 000000000..441cd9e48 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_infobar_01.js @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Check the position and text content of the highlighter nodeinfo bar. + +const TEST_URI = URL_ROOT + "doc_inspector_infobar_01.html"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URI); + + let testData = [ + { + selector: "#top", + position: "bottom", + tag: "div", + id: "top", + classes: ".class1.class2", + dims: "500" + " \u00D7 " + "100" + }, + { + selector: "#vertical", + position: "overlap", + tag: "div", + id: "vertical", + classes: "" + // No dims as they will vary between computers + }, + { + selector: "#bottom", + position: "top", + tag: "div", + id: "bottom", + classes: "", + dims: "500" + " \u00D7 " + "100" + }, + { + selector: "body", + position: "bottom", + tag: "body", + classes: "" + // No dims as they will vary between computers + }, + { + selector: "clipPath", + position: "bottom", + tag: "clipPath", + id: "clip", + classes: "" + // No dims as element is not displayed and we just want to test tag name + }, + ]; + + for (let currTest of testData) { + yield testPosition(currTest, inspector, testActor); + } +}); + +function* testPosition(test, inspector, testActor) { + info("Testing " + test.selector); + + yield selectAndHighlightNode(test.selector, inspector); + + let position = yield testActor.getHighlighterNodeAttribute( + "box-model-infobar-container", "position"); + is(position, test.position, "Node " + test.selector + ": position matches"); + + let tag = yield testActor.getHighlighterNodeTextContent( + "box-model-infobar-tagname"); + is(tag, test.tag, "node " + test.selector + ": tagName matches."); + + if (test.id) { + let id = yield testActor.getHighlighterNodeTextContent( + "box-model-infobar-id"); + is(id, "#" + test.id, "node " + test.selector + ": id matches."); + } + + let classes = yield testActor.getHighlighterNodeTextContent( + "box-model-infobar-classes"); + is(classes, test.classes, "node " + test.selector + ": classes match."); + + if (test.dims) { + let dims = yield testActor.getHighlighterNodeTextContent( + "box-model-infobar-dimensions"); + is(dims, test.dims, "node " + test.selector + ": dims match."); + } +} diff --git a/devtools/client/inspector/test/browser_inspector_infobar_02.js b/devtools/client/inspector/test/browser_inspector_infobar_02.js new file mode 100644 index 000000000..1b5bd5edf --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_infobar_02.js @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Check the text content of the highlighter info bar for namespaced elements. + +const XHTML = ` + <!DOCTYPE html> + <html xmlns="http://www.w3.org/1999/xhtml" + xmlns:svg="http://www.w3.org/2000/svg"> + <body> + <svg:svg width="100" height="100"> + <svg:circle cx="0" cy="0" r="5"></svg:circle> + </svg:svg> + </body> + </html> +`; + +const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML); + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URI); + + let testData = [ + { + selector: "svg", + tag: "svg:svg" + }, + { + selector: "circle", + tag: "svg:circle" + }, + ]; + + for (let currTest of testData) { + yield testNode(currTest, inspector, testActor); + } +}); + +function* testNode(test, inspector, testActor) { + info("Testing " + test.selector); + + yield selectAndHighlightNode(test.selector, inspector); + + let tag = yield testActor.getHighlighterNodeTextContent( + "box-model-infobar-tagname"); + is(tag, test.tag, "node " + test.selector + ": tagName matches."); +} diff --git a/devtools/client/inspector/test/browser_inspector_infobar_03.js b/devtools/client/inspector/test/browser_inspector_infobar_03.js new file mode 100644 index 000000000..023d5bb38 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_infobar_03.js @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Bug 1102269 - Make sure info-bar never gets outside of visible area after scrolling + +const TEST_URI = URL_ROOT + "doc_inspector_infobar_03.html"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URI); + + let testData = { + selector: "body", + position: "overlap", + style: "top:0px", + }; + + yield testPositionAndStyle(testData, inspector, testActor); +}); + +function* testPositionAndStyle(test, inspector, testActor) { + info("Testing " + test.selector); + + yield selectAndHighlightNode(test.selector, inspector); + + let style = yield testActor.getHighlighterNodeAttribute( + "box-model-infobar-container", "style"); + + is(style.split(";")[0], test.style, + "Infobar shows on top of the page when page isn't scrolled"); + + yield testActor.scrollWindow(0, 500); + + style = yield testActor.getHighlighterNodeAttribute( + "box-model-infobar-container", "style"); + + is(style.split(";")[0], test.style, + "Infobar shows on top of the page even if the page is scrolled"); +} diff --git a/devtools/client/inspector/test/browser_inspector_infobar_textnode.js b/devtools/client/inspector/test/browser_inspector_infobar_textnode.js new file mode 100644 index 000000000..c9315eb9b --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_infobar_textnode.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Bug 1309212 - Make sure info-bar is displayed with dimensions for text nodes. + +const TEST_URI = URL_ROOT + "doc_inspector_infobar_textnode.html"; + +add_task(function* () { + let { inspector, testActor } = yield openInspectorForURL(TEST_URI); + let { walker } = inspector; + + info("Retrieve the children of #textnode-container"); + let div = yield walker.querySelector(walker.rootNode, "#textnode-container"); + let { nodes } = yield inspector.walker.children(div); + + // Children 0, 2 and 4 are text nodes, for which we expect to see an infobar containing + // dimensions. + + // Regular text node. + info("Select the first text node"); + yield selectNode(nodes[0], inspector, "test-highlight"); + yield checkTextNodeInfoBar(testActor); + + // Whitespace-only text node. + info("Select the second text node"); + yield selectNode(nodes[2], inspector, "test-highlight"); + yield checkTextNodeInfoBar(testActor); + + // Regular text node. + info("Select the third text node"); + yield selectNode(nodes[4], inspector, "test-highlight"); + yield checkTextNodeInfoBar(testActor); +}); + +function* checkTextNodeInfoBar(testActor) { + let tag = yield testActor.getHighlighterNodeTextContent( + "box-model-infobar-tagname"); + is(tag, "#text", "node display name is #text"); + let dims = yield testActor.getHighlighterNodeTextContent( + "box-model-infobar-dimensions"); + // Do not assert dimensions as they might be platform specific. + ok(!!dims, "node has dims"); +} diff --git a/devtools/client/inspector/test/browser_inspector_initialization.js b/devtools/client/inspector/test/browser_inspector_initialization.js new file mode 100644 index 000000000..55db060f3 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_initialization.js @@ -0,0 +1,112 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* globals getTestActorWithoutToolbox */ +"use strict"; + +// Tests for different ways to initialize the inspector. + +const HTML = ` + <div id="first" style="margin: 10em; font-size: 14pt; + font-family: helvetica, sans-serif; color: gray"> + <h1>Some header text</h1> + <p id="salutation" style="font-size: 12pt">hi.</p> + <p id="body" style="font-size: 12pt">I am a test-case. This text exists + solely to provide some things to test the inspector initialization.</p> + <p>If you are reading this, you should go do something else instead. Maybe + read a book. Or better yet, write some test-cases for another bit of code. + <span style="font-style: italic">Inspector's!</span> + </p> + <p id="closing">end transmission</p> + </div> +`; + +const TEST_URI = "data:text/html;charset=utf-8," + encodeURI(HTML); + +add_task(function* () { + let tab = yield addTab(TEST_URI); + let testActor = yield getTestActorWithoutToolbox(tab); + + yield testToolboxInitialization(testActor, tab); + yield testContextMenuInitialization(testActor); + yield testContextMenuInspectorAlreadyOpen(testActor); +}); + +function* testToolboxInitialization(testActor, tab) { + let target = TargetFactory.forTab(tab); + + info("Opening inspector with gDevTools."); + let toolbox = yield gDevTools.showToolbox(target, "inspector"); + let inspector = toolbox.getCurrentPanel(); + + ok(true, "Inspector started, and notification received."); + ok(inspector, "Inspector instance is accessible."); + ok(inspector.isReady, "Inspector instance is ready."); + is(inspector.target.tab, tab, "Valid target."); + + yield selectNode("p", inspector); + yield testMarkupView("p", inspector); + yield testBreadcrumbs("p", inspector); + + yield testActor.scrollIntoView("span"); + + yield selectNode("span", inspector); + yield testMarkupView("span", inspector); + yield testBreadcrumbs("span", inspector); + + info("Destroying toolbox"); + let destroyed = toolbox.once("destroyed"); + toolbox.destroy(); + yield destroyed; + + ok("true", "'destroyed' notification received."); + ok(!gDevTools.getToolbox(target), "Toolbox destroyed."); +} + +function* testContextMenuInitialization(testActor) { + info("Opening inspector by clicking on 'Inspect Element' context menu item"); + yield clickOnInspectMenuItem(testActor, "#salutation"); + + info("Checking inspector state."); + yield testMarkupView("#salutation"); + yield testBreadcrumbs("#salutation"); +} + +function* testContextMenuInspectorAlreadyOpen(testActor) { + info("Changing node by clicking on 'Inspect Element' context menu item"); + + let inspector = getActiveInspector(); + ok(inspector, "Inspector is active"); + + yield clickOnInspectMenuItem(testActor, "#closing"); + + ok(true, "Inspector was updated when 'Inspect Element' was clicked."); + yield testMarkupView("#closing", inspector); + yield testBreadcrumbs("#closing", inspector); +} + +function* testMarkupView(selector, inspector) { + inspector = inspector || getActiveInspector(); + let nodeFront = yield getNodeFront(selector, inspector); + try { + is(inspector.selection.nodeFront, nodeFront, + "Right node is selected in the markup view"); + } catch (ex) { + ok(false, "Got exception while resolving selected node of markup view."); + console.error(ex); + } +} + +function* testBreadcrumbs(selector, inspector) { + inspector = inspector || getActiveInspector(); + let nodeFront = yield getNodeFront(selector, inspector); + + let b = inspector.breadcrumbs; + let expectedText = b.prettyPrintNodeAsText(nodeFront); + let button = b.container.querySelector("button[checked=true]"); + ok(button, "A crumbs is checked=true"); + is(button.getAttribute("title"), expectedText, + "Crumb refers to the right node"); +} diff --git a/devtools/client/inspector/test/browser_inspector_inspect-object-element.js b/devtools/client/inspector/test/browser_inspector_inspect-object-element.js new file mode 100644 index 000000000..ca646c506 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_inspect-object-element.js @@ -0,0 +1,18 @@ +/* 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"; + +// A regression test for bug 665880 to make sure elements inside <object> can +// be inspected without exceptions. + +const TEST_URI = "data:text/html;charset=utf-8," + + "<object><p>browser_inspector_inspect-object-element.js</p></object>"; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URI); + + yield selectNode("object", inspector); + + ok(true, "Selected <object> without throwing"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_invalidate.js b/devtools/client/inspector/test/browser_inspector_invalidate.js new file mode 100644 index 000000000..040bd1c1c --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_invalidate.js @@ -0,0 +1,35 @@ +/* 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 highlighter handles geometry changes correctly. + +const TEST_URI = "data:text/html;charset=utf-8," + + "browser_inspector_invalidate.js\n" + + "<div style=\"width: 100px; height: 100px; background:yellow;\"></div>"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URI); + let divFront = yield getNodeFront("div", inspector); + + info("Waiting for highlighter to activate"); + yield inspector.highlighter.showBoxModel(divFront); + + let rect = yield testActor.getSimpleBorderRect(); + is(rect.width, 100, "The highlighter has the right width."); + + info("Changing the test element's size and waiting for the highlighter " + + "to update"); + yield testActor.changeHighlightedNodeWaitForUpdate( + "style", + "width: 200px; height: 100px; background:yellow;" + ); + + rect = yield testActor.getSimpleBorderRect(); + is(rect.width, 200, "The highlighter has the right width after update"); + + info("Waiting for highlighter to hide"); + yield inspector.highlighter.hideBoxModel(); +}); diff --git a/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js b/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js new file mode 100644 index 000000000..46b0ce5f5 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.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 copy outer HTML from the keyboard/copy event + +const TEST_URL = URL_ROOT + "doc_inspector_outerhtml.html"; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URL); + let root = inspector.markup._elt; + + info("Test copy outerHTML for COMMENT node"); + let comment = getElementByType(inspector, Ci.nsIDOMNode.COMMENT_NODE); + yield setSelectionNodeFront(comment, inspector); + yield checkClipboard("<!-- Comment -->", root); + + info("Test copy outerHTML for DOCTYPE node"); + let doctype = getElementByType(inspector, Ci.nsIDOMNode.DOCUMENT_TYPE_NODE); + yield setSelectionNodeFront(doctype, inspector); + yield checkClipboard("<!DOCTYPE html>", root); + + info("Test copy outerHTML for ELEMENT node"); + yield selectAndHighlightNode("div", inspector); + yield checkClipboard("<div><p>Test copy OuterHTML</p></div>", root); +}); + +function* setSelectionNodeFront(node, inspector) { + let updated = inspector.once("inspector-updated"); + inspector.selection.setNodeFront(node); + yield updated; +} + +function* checkClipboard(expectedText, node) { + try { + yield waitForClipboardPromise(() => fireCopyEvent(node), expectedText); + ok(true, "Clipboard successfully filled with : " + expectedText); + } catch (e) { + ok(false, "Clipboard could not be filled with the expected text : " + + expectedText); + } +} + +function getElementByType(inspector, type) { + for (let [node] of inspector.markup._containers) { + if (node.nodeType === type) { + return node; + } + } + return null; +} diff --git a/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts.js b/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts.js new file mode 100644 index 000000000..f03a33fd1 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts.js @@ -0,0 +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"; + +// Tests that the keybindings for highlighting different elements work as +// intended. + +const TEST_URI = "data:text/html;charset=utf-8," + + "<html><head><title>Test for the highlighter keybindings</title></head>" + + "<body><p><strong>Greetings, earthlings!</strong>" + + " I come in peace.</p></body></html>"; + +const TEST_DATA = [ + { key: "VK_LEFT", selectedNode: "p" }, + { key: "VK_LEFT", selectedNode: "body" }, + { key: "VK_LEFT", selectedNode: "html" }, + { key: "VK_RIGHT", selectedNode: "body" }, + { key: "VK_RIGHT", selectedNode: "p" }, + { key: "VK_RIGHT", selectedNode: "strong" }, +]; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URI); + + info("Selecting the deepest element to start with"); + yield selectNode("strong", inspector); + + let nodeFront = yield getNodeFront("strong", inspector); + is(inspector.selection.nodeFront, nodeFront, + "<strong> should be selected initially"); + + info("Focusing the currently active breadcrumb button"); + let bc = inspector.breadcrumbs; + bc.nodeHierarchy[bc.currentIndex].button.focus(); + + for (let { key, selectedNode } of TEST_DATA) { + info("Pressing " + key + " to select " + selectedNode); + + let updated = inspector.once("inspector-updated"); + EventUtils.synthesizeKey(key, {}); + yield updated; + + let selectedNodeFront = yield getNodeFront(selectedNode, inspector); + is(inspector.selection.nodeFront, selectedNodeFront, + selectedNode + " is selected."); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js b/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js new file mode 100644 index 000000000..59dbbbcc0 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js @@ -0,0 +1,278 @@ +/* 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 context menu items are enabled / disabled correctly. + +const TEST_URL = URL_ROOT + "doc_inspector_menu.html"; + +const PASTE_MENU_ITEMS = [ + "node-menu-pasteinnerhtml", + "node-menu-pasteouterhtml", + "node-menu-pastebefore", + "node-menu-pasteafter", + "node-menu-pastefirstchild", + "node-menu-pastelastchild", +]; + +const ACTIVE_ON_DOCTYPE_ITEMS = [ + "node-menu-showdomproperties", + "node-menu-useinconsole" +]; + +const ALL_MENU_ITEMS = [ + "node-menu-edithtml", + "node-menu-copyinner", + "node-menu-copyouter", + "node-menu-copyuniqueselector", + "node-menu-copyimagedatauri", + "node-menu-delete", + "node-menu-pseudo-hover", + "node-menu-pseudo-active", + "node-menu-pseudo-focus", + "node-menu-scrollnodeintoview", + "node-menu-screenshotnode", + "node-menu-add-attribute", + "node-menu-edit-attribute", + "node-menu-remove-attribute" +].concat(PASTE_MENU_ITEMS, ACTIVE_ON_DOCTYPE_ITEMS); + +const INACTIVE_ON_DOCTYPE_ITEMS = + ALL_MENU_ITEMS.filter(item => ACTIVE_ON_DOCTYPE_ITEMS.indexOf(item) === -1); + +/** + * Test cases, each item of this array may define the following properties: + * desc: string that will be logged + * selector: selector of the node to be selected + * disabled: items that should have disabled state + * clipboardData: clipboard content + * clipboardDataType: clipboard content type + * attributeTrigger: attribute that will be used as context menu trigger + */ +const TEST_CASES = [ + { + desc: "doctype node with empty clipboard", + selector: null, + disabled: INACTIVE_ON_DOCTYPE_ITEMS, + }, + { + desc: "doctype node with html on clipboard", + clipboardData: "<p>some text</p>", + clipboardDataType: "html", + selector: null, + disabled: INACTIVE_ON_DOCTYPE_ITEMS, + }, + { + desc: "element node HTML on the clipboard", + clipboardData: "<p>some text</p>", + clipboardDataType: "html", + disabled: [ + "node-menu-copyimagedatauri", + "node-menu-edit-attribute", + "node-menu-remove-attribute" + ], + selector: "#sensitivity", + }, + { + desc: "<html> element", + clipboardData: "<p>some text</p>", + clipboardDataType: "html", + selector: "html", + disabled: [ + "node-menu-copyimagedatauri", + "node-menu-pastebefore", + "node-menu-pasteafter", + "node-menu-pastefirstchild", + "node-menu-pastelastchild", + "node-menu-edit-attribute", + "node-menu-remove-attribute" + ], + }, + { + desc: "<body> with HTML on clipboard", + clipboardData: "<p>some text</p>", + clipboardDataType: "html", + selector: "body", + disabled: [ + "node-menu-copyimagedatauri", + "node-menu-pastebefore", + "node-menu-pasteafter", + "node-menu-edit-attribute", + "node-menu-remove-attribute" + ] + }, + { + desc: "<img> with HTML on clipboard", + clipboardData: "<p>some text</p>", + clipboardDataType: "html", + selector: "img", + disabled: [ + "node-menu-edit-attribute", + "node-menu-remove-attribute" + ] + }, + { + desc: "<head> with HTML on clipboard", + clipboardData: "<p>some text</p>", + clipboardDataType: "html", + selector: "head", + disabled: [ + "node-menu-copyimagedatauri", + "node-menu-pastebefore", + "node-menu-pasteafter", + "node-menu-screenshotnode", + "node-menu-edit-attribute", + "node-menu-remove-attribute" + ], + }, + { + desc: "<head> with no html on clipboard", + selector: "head", + disabled: PASTE_MENU_ITEMS.concat([ + "node-menu-copyimagedatauri", + "node-menu-screenshotnode", + "node-menu-edit-attribute", + "node-menu-remove-attribute" + ]), + }, + { + desc: "<element> with text on clipboard", + clipboardData: "some text", + clipboardDataType: undefined, + selector: "#paste-area", + disabled: [ + "node-menu-copyimagedatauri", + "node-menu-edit-attribute", + "node-menu-remove-attribute" + ] + }, + { + desc: "<element> with base64 encoded image data uri on clipboard", + clipboardData: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABC" + + "AAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==", + clipboardDataType: undefined, + selector: "#paste-area", + disabled: PASTE_MENU_ITEMS.concat([ + "node-menu-copyimagedatauri", + "node-menu-edit-attribute", + "node-menu-remove-attribute" + ]), + }, + { + desc: "<element> with empty string on clipboard", + clipboardData: "", + clipboardDataType: undefined, + selector: "#paste-area", + disabled: PASTE_MENU_ITEMS.concat([ + "node-menu-copyimagedatauri", + "node-menu-edit-attribute", + "node-menu-remove-attribute" + ]), + }, + { + desc: "<element> with whitespace only on clipboard", + clipboardData: " \n\n\t\n\n \n", + clipboardDataType: undefined, + selector: "#paste-area", + disabled: PASTE_MENU_ITEMS.concat([ + "node-menu-copyimagedatauri", + "node-menu-edit-attribute", + "node-menu-remove-attribute" + ]), + }, + { + desc: "<element> that isn't visible on the page, empty clipboard", + selector: "#hiddenElement", + disabled: PASTE_MENU_ITEMS.concat([ + "node-menu-copyimagedatauri", + "node-menu-screenshotnode", + "node-menu-edit-attribute", + "node-menu-remove-attribute" + ]), + }, + { + desc: "<element> nested in another hidden element, empty clipboard", + selector: "#nestedHiddenElement", + disabled: PASTE_MENU_ITEMS.concat([ + "node-menu-copyimagedatauri", + "node-menu-screenshotnode", + "node-menu-edit-attribute", + "node-menu-remove-attribute" + ]), + }, + { + desc: "<element> with context menu triggered on attribute, empty clipboard", + selector: "#attributes", + disabled: PASTE_MENU_ITEMS.concat(["node-menu-copyimagedatauri"]), + attributeTrigger: "data-edit" + } +]; + +var clipboard = require("sdk/clipboard"); +registerCleanupFunction(() => { + clipboard = null; +}); + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URL); + for (let test of TEST_CASES) { + let { desc, disabled, selector, attributeTrigger } = test; + + info(`Test ${desc}`); + setupClipboard(test.clipboardData, test.clipboardDataType); + + let front = yield getNodeFrontForSelector(selector, inspector); + + info("Selecting the specified node."); + yield selectNode(front, inspector); + + info("Simulating context menu click on the selected node container."); + let nodeFrontContainer = getContainerForNodeFront(front, inspector); + let contextMenuTrigger = attributeTrigger + ? nodeFrontContainer.tagLine.querySelector( + `[data-attr="${attributeTrigger}"]`) + : nodeFrontContainer.tagLine; + + let allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: contextMenuTrigger, + }); + + for (let id of ALL_MENU_ITEMS) { + let menuItem = allMenuItems.find(item => item.id === id); + let shouldBeDisabled = disabled.indexOf(id) !== -1; + is(menuItem.disabled, shouldBeDisabled, + `#${id} should be ${shouldBeDisabled ? "disabled" : "enabled"} `); + } + } +}); + +/** + * A helper that fetches a front for a node that matches the given selector or + * doctype node if the selector is falsy. + */ +function* getNodeFrontForSelector(selector, inspector) { + if (selector) { + info("Retrieving front for selector " + selector); + return getNodeFront(selector, inspector); + } + + info("Retrieving front for doctype node"); + let {nodes} = yield inspector.walker.children(inspector.walker.rootNode); + return nodes[0]; +} + +/** + * A helper that populates the clipboard with data of given type. Clears the + * clipboard if data is falsy. + */ +function setupClipboard(data, type) { + if (data) { + info("Populating clipboard with " + type + " data."); + clipboard.set(data, type); + } else { + info("Clearing clipboard."); + clipboard.set("", "text"); + } +} diff --git a/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js b/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js new file mode 100644 index 000000000..0c96e9bbe --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js @@ -0,0 +1,49 @@ +/* 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 the various copy items in the context menu works correctly. + +const TEST_URL = URL_ROOT + "doc_inspector_menu.html"; +const COPY_ITEMS_TEST_DATA = [ + { + desc: "copy inner html", + id: "node-menu-copyinner", + selector: "[data-id=\"copy\"]", + text: "Paragraph for testing copy", + }, + { + desc: "copy outer html", + id: "node-menu-copyouter", + selector: "[data-id=\"copy\"]", + text: "<p data-id=\"copy\">Paragraph for testing copy</p>", + }, + { + desc: "copy unique selector", + id: "node-menu-copyuniqueselector", + selector: "[data-id=\"copy\"]", + text: "body > div:nth-child(1) > p:nth-child(2)", + }, + { + desc: "copy image data uri", + id: "node-menu-copyimagedatauri", + selector: "#copyimage", + text: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABC" + + "AAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==", + }, +]; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URL); + for (let {desc, id, selector, text} of COPY_ITEMS_TEST_DATA) { + info("Testing " + desc); + yield selectNode(selector, inspector); + + let allMenuItems = openContextMenuAndGetAllItems(inspector); + let item = allMenuItems.find(i => i.id === id); + ok(item, "The popup has a " + desc + " menu item."); + + yield waitForClipboardPromise(() => item.click(), text); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_menu-03-paste-items-svg.js b/devtools/client/inspector/test/browser_inspector_menu-03-paste-items-svg.js new file mode 100644 index 000000000..26ae3ff00 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_menu-03-paste-items-svg.js @@ -0,0 +1,42 @@ +/* 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 HTML can be pasted in SVG elements. + +const TEST_URL = URL_ROOT + "doc_inspector_svg.svg"; +const PASTE_AS_FIRST_CHILD = '<circle xmlns="http://www.w3.org/2000/svg" cx="42" cy="42" r="5"/>'; +const PASTE_AS_LAST_CHILD = '<circle xmlns="http://www.w3.org/2000/svg" cx="42" cy="42" r="15"/>'; + +add_task(function* () { + let clipboard = require("sdk/clipboard"); + + let { inspector, testActor } = yield openInspectorForURL(TEST_URL); + + let refSelector = "svg"; + let oldHTML = yield testActor.getProperty(refSelector, "innerHTML"); + yield selectNode(refSelector, inspector); + let markupTagLine = getContainerForSelector(refSelector, inspector).tagLine; + + yield pasteContent("node-menu-pastefirstchild", PASTE_AS_FIRST_CHILD); + yield pasteContent("node-menu-pastelastchild", PASTE_AS_LAST_CHILD); + + let html = yield testActor.getProperty(refSelector, "innerHTML"); + let expectedHtml = PASTE_AS_FIRST_CHILD + oldHTML + PASTE_AS_LAST_CHILD; + is(html, expectedHtml, "The innerHTML of the SVG node is correct"); + + // Helpers + function* pasteContent(menuId, clipboardData) { + let allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: markupTagLine, + }); + info(`Testing ${menuId} for ${clipboardData}`); + clipboard.set(clipboardData); + + let onMutation = inspector.once("markupmutation"); + allMenuItems.find(item => item.id === menuId).click(); + info("Waiting for mutation to occur"); + yield onMutation; + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_menu-03-paste-items.js b/devtools/client/inspector/test/browser_inspector_menu-03-paste-items.js new file mode 100644 index 000000000..19e5742de --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_menu-03-paste-items.js @@ -0,0 +1,128 @@ +/* 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 different paste items work in the context menu + +const TEST_URL = URL_ROOT + "doc_inspector_menu.html"; +const PASTE_ADJACENT_HTML_DATA = [ + { + desc: "As First Child", + clipboardData: "2", + menuId: "node-menu-pastefirstchild", + }, + { + desc: "As Last Child", + clipboardData: "4", + menuId: "node-menu-pastelastchild", + }, + { + desc: "Before", + clipboardData: "1", + menuId: "node-menu-pastebefore", + }, + { + desc: "After", + clipboardData: "<span>5</span>", + menuId: "node-menu-pasteafter", + }, +]; + +var clipboard = require("sdk/clipboard"); +registerCleanupFunction(() => { + clipboard = null; +}); + +add_task(function* () { + let { inspector, testActor } = yield openInspectorForURL(TEST_URL); + + yield testPasteOuterHTMLMenu(); + yield testPasteInnerHTMLMenu(); + yield testPasteAdjacentHTMLMenu(); + + function* testPasteOuterHTMLMenu() { + info("Testing that 'Paste Outer HTML' menu item works."); + clipboard.set("this was pasted (outerHTML)"); + let outerHTMLSelector = "#paste-area h1"; + + let nodeFront = yield getNodeFront(outerHTMLSelector, inspector); + yield selectNode(nodeFront, inspector); + + let allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: getContainerForNodeFront(nodeFront, inspector).tagLine, + }); + + let onNodeReselected = inspector.markup.once("reselectedonremoved"); + allMenuItems.find(item => item.id === "node-menu-pasteouterhtml").click(); + + info("Waiting for inspector selection to update"); + yield onNodeReselected; + + let outerHTML = yield testActor.getProperty("body", "outerHTML"); + ok(outerHTML.includes(clipboard.get()), + "Clipboard content was pasted into the node's outer HTML."); + ok(!(yield testActor.hasNode(outerHTMLSelector)), + "The original node was removed."); + } + + function* testPasteInnerHTMLMenu() { + info("Testing that 'Paste Inner HTML' menu item works."); + clipboard.set("this was pasted (innerHTML)"); + let innerHTMLSelector = "#paste-area .inner"; + let getInnerHTML = () => testActor.getProperty(innerHTMLSelector, + "innerHTML"); + let origInnerHTML = yield getInnerHTML(); + + let nodeFront = yield getNodeFront(innerHTMLSelector, inspector); + yield selectNode(nodeFront, inspector); + + let allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: getContainerForNodeFront(nodeFront, inspector).tagLine, + }); + + let onMutation = inspector.once("markupmutation"); + allMenuItems.find(item => item.id === "node-menu-pasteinnerhtml").click(); + info("Waiting for mutation to occur"); + yield onMutation; + + ok((yield getInnerHTML()) === clipboard.get(), + "Clipboard content was pasted into the node's inner HTML."); + ok((yield testActor.hasNode(innerHTMLSelector)), + "The original node has been preserved."); + yield undoChange(inspector); + ok((yield getInnerHTML()) === origInnerHTML, + "Previous innerHTML has been restored after undo"); + } + + function* testPasteAdjacentHTMLMenu() { + let refSelector = "#paste-area .adjacent .ref"; + let adjacentNodeSelector = "#paste-area .adjacent"; + let nodeFront = yield getNodeFront(refSelector, inspector); + yield selectNode(nodeFront, inspector); + let markupTagLine = getContainerForNodeFront(nodeFront, inspector).tagLine; + + for (let { clipboardData, menuId } of PASTE_ADJACENT_HTML_DATA) { + let allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: markupTagLine, + }); + info(`Testing ${menuId} for ${clipboardData}`); + clipboard.set(clipboardData); + + let onMutation = inspector.once("markupmutation"); + allMenuItems.find(item => item.id === menuId).click(); + info("Waiting for mutation to occur"); + yield onMutation; + } + + let html = yield testActor.getProperty(adjacentNodeSelector, "innerHTML"); + ok(html.trim() === "1<span class=\"ref\">234</span><span>5</span>", + "The Paste as Last Child / as First Child / Before / After worked as " + + "expected"); + yield undoChange(inspector); + + html = yield testActor.getProperty(adjacentNodeSelector, "innerHTML"); + ok(html.trim() === "1<span class=\"ref\">234</span>", + "Undo works for paste adjacent HTML"); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_menu-04-use-in-console.js b/devtools/client/inspector/test/browser_inspector_menu-04-use-in-console.js new file mode 100644 index 000000000..3908784f6 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_menu-04-use-in-console.js @@ -0,0 +1,61 @@ +/* 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"; + +// Tests "Use in Console" menu item + +const TEST_URL = URL_ROOT + "doc_inspector_menu.html"; + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled"); +}); + +// Use the old webconsole since the node isn't being rendered as an HTML tag +// in the new one (Bug 1304794) +Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false); +registerCleanupFunction(function* () { + Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled"); +}); + +add_task(function* () { + let { inspector, toolbox } = yield openInspectorForURL(TEST_URL); + + yield testUseInConsole(); + + function* testUseInConsole() { + info("Testing 'Use in Console' menu item."); + + yield selectNode("#console-var", inspector); + let container = yield getContainerForSelector("#console-var", inspector); + let allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: container.tagLine, + }); + let menuItem = allMenuItems.find(i => i.id === "node-menu-useinconsole"); + menuItem.click(); + + yield inspector.once("console-var-ready"); + + let hud = toolbox.getPanel("webconsole").hud; + let jsterm = hud.jsterm; + + let jstermInput = jsterm.hud.document.querySelector(".jsterm-input-node"); + is(jstermInput.value, "temp0", "first console variable is named temp0"); + + let result = yield jsterm.execute(); + isnot(result.textContent.indexOf('<p id="console-var">'), -1, + "variable temp0 references correct node"); + + yield selectNode("#console-var-multi", inspector); + menuItem.click(); + yield inspector.once("console-var-ready"); + + is(jstermInput.value, "temp1", "second console variable is named temp1"); + + result = yield jsterm.execute(); + isnot(result.textContent.indexOf('<p id="console-var-multi">'), -1, + "variable temp1 references correct node"); + + jsterm.clearHistory(); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_menu-05-attribute-items.js b/devtools/client/inspector/test/browser_inspector_menu-05-attribute-items.js new file mode 100644 index 000000000..df901f0a4 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_menu-05-attribute-items.js @@ -0,0 +1,79 @@ +/* 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 attribute items work in the context menu + +const TEST_URL = URL_ROOT + "doc_inspector_menu.html"; + +add_task(function* () { + let { inspector, testActor } = yield openInspectorForURL(TEST_URL); + yield selectNode("#attributes", inspector); + + yield testAddAttribute(); + yield testEditAttribute(); + yield testRemoveAttribute(); + + function* testAddAttribute() { + info("Triggering 'Add Attribute' and waiting for mutation to occur"); + let addAttribute = getMenuItem("node-menu-add-attribute"); + addAttribute.click(); + + EventUtils.synthesizeKey('class="u-hidden"', {}); + let onMutation = inspector.once("markupmutation"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onMutation; + + let hasAttribute = testActor.hasNode("#attributes.u-hidden"); + ok(hasAttribute, "attribute was successfully added"); + } + + function* testEditAttribute() { + info("Testing 'Edit Attribute' menu item"); + let editAttribute = getMenuItem("node-menu-edit-attribute"); + + info("Triggering 'Edit Attribute' and waiting for mutation to occur"); + inspector.nodeMenuTriggerInfo = { + type: "attribute", + name: "data-edit" + }; + editAttribute.click(); + EventUtils.synthesizeKey("data-edit='edited'", {}); + let onMutation = inspector.once("markupmutation"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onMutation; + + let isAttributeChanged = + yield testActor.hasNode("#attributes[data-edit='edited']"); + ok(isAttributeChanged, "attribute was successfully edited"); + } + + function* testRemoveAttribute() { + info("Testing 'Remove Attribute' menu item"); + let removeAttribute = getMenuItem("node-menu-remove-attribute"); + + info("Triggering 'Remove Attribute' and waiting for mutation to occur"); + inspector.nodeMenuTriggerInfo = { + type: "attribute", + name: "data-remove" + }; + let onMutation = inspector.once("markupmutation"); + removeAttribute.click(); + yield onMutation; + + let hasAttribute = yield testActor.hasNode("#attributes[data-remove]"); + ok(!hasAttribute, "attribute was successfully removed"); + } + + function getMenuItem(id) { + let allMenuItems = openContextMenuAndGetAllItems(inspector, { + target: getContainerForSelector("#attributes", inspector).tagLine, + }); + let menuItem = allMenuItems.find(i => i.id === id); + ok(menuItem, "Menu item '" + id + "' found"); + // Close the menu so synthesizing future keys won't select menu items. + EventUtils.synthesizeKey("VK_ESCAPE", {}); + return menuItem; + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_menu-06-other.js b/devtools/client/inspector/test/browser_inspector_menu-06-other.js new file mode 100644 index 000000000..9f4310121 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_menu-06-other.js @@ -0,0 +1,95 @@ +/* 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"; + +// Tests for menuitem functionality that doesn't fit into any specific category +const TEST_URL = URL_ROOT + "doc_inspector_menu.html"; +add_task(function* () { + let { inspector, toolbox, testActor } = yield openInspectorForURL(TEST_URL); + yield testShowDOMProperties(); + yield testDuplicateNode(); + yield testDeleteNode(); + yield testDeleteRootNode(); + yield testScrollIntoView(); + function* testShowDOMProperties() { + info("Testing 'Show DOM Properties' menu item."); + let allMenuItems = openContextMenuAndGetAllItems(inspector); + let showDOMPropertiesNode = + allMenuItems.find(item => item.id === "node-menu-showdomproperties"); + ok(showDOMPropertiesNode, "the popup menu has a show dom properties item"); + + let consoleOpened = toolbox.once("webconsole-ready"); + + info("Triggering 'Show DOM Properties' and waiting for inspector open"); + showDOMPropertiesNode.click(); + yield consoleOpened; + + let webconsoleUI = toolbox.getPanel("webconsole").hud.ui; + let messagesAdded = webconsoleUI.once("new-messages"); + yield messagesAdded; + info("Checking if 'inspect($0)' was evaluated"); + ok(webconsoleUI.jsterm.history[0] === "inspect($0)"); + yield toolbox.toggleSplitConsole(); + } + function* testDuplicateNode() { + info("Testing 'Duplicate Node' menu item for normal elements."); + + yield selectNode(".duplicate", inspector); + is((yield testActor.getNumberOfElementMatches(".duplicate")), 1, + "There should initially be 1 .duplicate node"); + + let allMenuItems = openContextMenuAndGetAllItems(inspector); + let menuItem = + allMenuItems.find(item => item.id === "node-menu-duplicatenode"); + ok(menuItem, "'Duplicate node' menu item should exist"); + + info("Triggering 'Duplicate Node' and waiting for inspector to update"); + let updated = inspector.once("markupmutation"); + menuItem.click(); + yield updated; + + is((yield testActor.getNumberOfElementMatches(".duplicate")), 2, + "The duplicated node should be in the markup."); + + let container = yield getContainerForSelector(".duplicate + .duplicate", + inspector); + ok(container, "A MarkupContainer should be created for the new node"); + } + + function* testDeleteNode() { + info("Testing 'Delete Node' menu item for normal elements."); + yield selectNode("#delete", inspector); + let allMenuItems = openContextMenuAndGetAllItems(inspector); + let deleteNode = allMenuItems.find(item => item.id === "node-menu-delete"); + ok(deleteNode, "the popup menu has a delete menu item"); + let updated = inspector.once("inspector-updated"); + + info("Triggering 'Delete Node' and waiting for inspector to update"); + deleteNode.click(); + yield updated; + + ok(!(yield testActor.hasNode("#delete")), "Node deleted"); + } + + function* testDeleteRootNode() { + info("Testing 'Delete Node' menu item does not delete root node."); + yield selectNode("html", inspector); + + let allMenuItems = openContextMenuAndGetAllItems(inspector); + let deleteNode = allMenuItems.find(item => item.id === "node-menu-delete"); + deleteNode.click(); + + let deferred = defer(); + executeSoon(deferred.resolve); + yield deferred.promise; + + ok((yield testActor.eval("!!content.document.documentElement")), + "Document element still alive."); + } + + function* testScrollIntoView() { + // Follow up bug to add this test - https://bugzilla.mozilla.org/show_bug.cgi?id=1154107 + todo(false, "Verify that node is scrolled into the viewport."); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_navigate_to_errors.js b/devtools/client/inspector/test/browser_inspector_navigate_to_errors.js new file mode 100644 index 000000000..c2266c852 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_navigate_to_errors.js @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// Test that inspector works when navigating to error pages. + +const TEST_URL_1 = "data:text/html,<html><body id=\"test-doc-1\">page</body></html>"; +const TEST_URL_2 = "http://127.0.0.1:36325/"; +const TEST_URL_3 = "http://www.wronguri.wronguri/"; +const TEST_URL_4 = "data:text/html,<html><body>test-doc-4</body></html>"; + +add_task(function* () { + // Open the inspector on a valid URL + let { inspector, testActor } = yield openInspectorForURL(TEST_URL_1); + + info("Navigate to closed port"); + yield navigateTo(inspector, TEST_URL_2); + + let documentURI = yield testActor.eval("document.documentURI;"); + ok(documentURI.startsWith("about:neterror"), "content is correct."); + + let hasPage = yield getNodeFront("#test-doc-1", inspector); + ok(!hasPage, "Inspector actor is no longer able to reach previous page DOM node"); + + let hasNetErrorNode = yield getNodeFront("#errorShortDesc", inspector); + ok(hasNetErrorNode, "Inspector actor is able to reach error page DOM node"); + + let bundle = Services.strings.createBundle("chrome://global/locale/appstrings.properties"); + let domain = TEST_URL_2.match(/^http:\/\/(.*)\/$/)[1]; + let errorMsg = bundle.formatStringFromName("connectionFailure", + [domain], 1); + is(yield getDisplayedNodeTextContent("#errorShortDescText", inspector), errorMsg, + "Inpector really inspects the error page"); + + info("Navigate to unknown domain"); + yield navigateTo(inspector, TEST_URL_3); + + domain = TEST_URL_3.match(/^http:\/\/(.*)\/$/)[1]; + errorMsg = bundle.formatStringFromName("dnsNotFound", + [domain], 1); + is(yield getDisplayedNodeTextContent("#errorShortDescText", inspector), errorMsg, + "Inspector really inspects the new error page"); + + info("Navigate to a valid url"); + yield navigateTo(inspector, TEST_URL_4); + + is(yield getDisplayedNodeTextContent("body", inspector), "test-doc-4", + "Inspector really inspects the valid url"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_navigation.js b/devtools/client/inspector/test/browser_inspector_navigation.js new file mode 100644 index 000000000..dab6f7007 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_navigation.js @@ -0,0 +1,43 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// Test that inspector updates when page is navigated. + +const TEST_URL_FILE = "browser/devtools/client/inspector/test/" + + "doc_inspector_breadcrumbs.html"; + +const TEST_URL_1 = "http://test1.example.org/" + TEST_URL_FILE; +const TEST_URL_2 = "http://test2.example.org/" + TEST_URL_FILE; + +add_task(function* () { + let { inspector, testActor } = yield openInspectorForURL(TEST_URL_1); + + yield selectNode("#i1", inspector); + + info("Navigating to a different page."); + yield navigateTo(inspector, TEST_URL_2); + + ok(true, "New page loaded"); + yield selectNode("#i1", inspector); + + let markuploaded = inspector.once("markuploaded"); + let onUpdated = inspector.once("inspector-updated"); + + info("Going back in history"); + yield testActor.eval("history.go(-1)"); + + info("Waiting for markup view to load after going back in history."); + yield markuploaded; + + info("Check that the inspector updates"); + yield onUpdated; + + ok(true, "Old page loaded"); + is((yield testActor.eval("location.href;")), TEST_URL_1, "URL is correct."); + + yield selectNode("#i1", inspector); +}); diff --git a/devtools/client/inspector/test/browser_inspector_open_on_neterror.js b/devtools/client/inspector/test/browser_inspector_open_on_neterror.js new file mode 100644 index 000000000..01e065a1a --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_open_on_neterror.js @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// Test that inspector works correctly when opened against a net error page + +const TEST_URL_1 = "http://127.0.0.1:36325/"; +const TEST_URL_2 = "data:text/html,<html><body>test-doc-2</body></html>"; + +add_task(function* () { + // Unfortunately, net error page are not firing load event, so that we can't + // use addTab helper and have to do that: + let tab = gBrowser.selectedTab = gBrowser.addTab("data:text/html,empty"); + yield BrowserTestUtils.browserLoaded(tab.linkedBrowser); + yield ContentTask.spawn(tab.linkedBrowser, { url: TEST_URL_1 }, function* ({ url }) { + // Also, the neterror being privileged, the DOMContentLoaded only fires on + // the chromeEventHandler. + let { chromeEventHandler } = docShell; // eslint-disable-line no-undef + let onDOMContentLoaded = ContentTaskUtils.waitForEvent(chromeEventHandler, + "DOMContentLoaded", true); + content.location = url; + yield onDOMContentLoaded; + }); + + let { inspector, testActor } = yield openInspector(); + ok(true, "Inspector loaded on the already opened net error"); + + let documentURI = yield testActor.eval("document.documentURI;"); + ok(documentURI.startsWith("about:neterror"), "content is really a net error page."); + + info("Navigate to a valid url"); + yield navigateTo(inspector, TEST_URL_2); + + is(yield getDisplayedNodeTextContent("body", inspector), "test-doc-2", + "Inspector really inspects the valid url"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_pane-toggle-01.js b/devtools/client/inspector/test/browser_inspector_pane-toggle-01.js new file mode 100644 index 000000000..1ec95cec3 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-01.js @@ -0,0 +1,27 @@ +/* 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 inspector panel has a sidebar pane toggle button, and that +// this button is visible both in BOTTOM and SIDE hosts. + +add_task(function* () { + info("Open the inspector in a bottom toolbox host"); + let {toolbox, inspector} = yield openInspectorForURL("about:blank", "bottom"); + + let button = inspector.panelDoc.querySelector(".sidebar-toggle"); + ok(button, "The toggle button exists in the DOM"); + is(button.parentNode.id, "inspector-sidebar-toggle-box", + "The toggle button has the right parent"); + ok(button.getAttribute("title"), "The tool tip has initial state"); + ok(!button.classList.contains("pane-collapsed"), "The button is in expanded state"); + ok(!!button.getClientRects().length, "The button is visible"); + + info("Switch the host to side type"); + yield toolbox.switchHost("side"); + + ok(!!button.getClientRects().length, "The button is still visible"); + ok(!button.classList.contains("pane-collapsed"), + "The button is still in expanded state"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_pane-toggle-02.js b/devtools/client/inspector/test/browser_inspector_pane-toggle-02.js new file mode 100644 index 000000000..54b68c655 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-02.js @@ -0,0 +1,43 @@ +/* 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 inspector toggled panel is visible by default, is hidden after +// clicking on the toggle button and remains expanded/collapsed when switching +// hosts. + +add_task(function* () { + info("Open the inspector in a side toolbox host"); + let {toolbox, inspector} = yield openInspectorForURL("about:blank", "side"); + + let panel = inspector.panelDoc.querySelector("#inspector-splitter-box .controlled"); + + let button = inspector.panelDoc.querySelector(".sidebar-toggle"); + ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state"); + + info("Listen to the end of the animation on the sidebar panel"); + let onTransitionEnd = once(panel, "transitionend"); + + info("Click on the toggle button"); + EventUtils.synthesizeMouseAtCenter(button, {}, + inspector.panelDoc.defaultView); + + yield onTransitionEnd; + ok(panel.classList.contains("pane-collapsed"), "The panel is in collapsed state"); + ok(!panel.hasAttribute("animated"), + "The collapsed panel will not perform unwanted animations"); + + info("Switch the host to bottom type"); + yield toolbox.switchHost("bottom"); + ok(panel.classList.contains("pane-collapsed"), "The panel is in collapsed state"); + + info("Click on the toggle button to expand the panel again"); + + onTransitionEnd = once(panel, "transitionend"); + EventUtils.synthesizeMouseAtCenter(button, {}, + inspector.panelDoc.defaultView); + yield onTransitionEnd; + + ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_pane-toggle-03.js b/devtools/client/inspector/test/browser_inspector_pane-toggle-03.js new file mode 100644 index 000000000..02fffd995 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-03.js @@ -0,0 +1,38 @@ +/* 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 toggle button can collapse and expand the inspector side/bottom +// panel, and that the appropriate attributes are updated in the process. + +add_task(function* () { + let {inspector} = yield openInspectorForURL("about:blank"); + + let button = inspector.panelDoc.querySelector(".sidebar-toggle"); + let panel = inspector.panelDoc.querySelector("#inspector-splitter-box .controlled"); + + ok(!button.classList.contains("pane-collapsed"), "The button is in expanded state"); + + info("Listen to the end of the animation on the sidebar panel"); + let onTransitionEnd = once(panel, "transitionend"); + + info("Click on the toggle button"); + EventUtils.synthesizeMouseAtCenter(button, {}, + inspector.panelDoc.defaultView); + + yield onTransitionEnd; + ok(button.classList.contains("pane-collapsed"), "The button is in collapsed state"); + ok(panel.classList.contains("pane-collapsed"), "The panel is in collapsed state"); + + info("Listen again to the end of the animation on the sidebar panel"); + onTransitionEnd = once(panel, "transitionend"); + + info("Click on the toggle button again"); + EventUtils.synthesizeMouseAtCenter(button, {}, + inspector.panelDoc.defaultView); + + yield onTransitionEnd; + ok(!button.classList.contains("pane-collapsed"), "The button is in expanded state"); + ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_pane-toggle-05.js b/devtools/client/inspector/test/browser_inspector_pane-toggle-05.js new file mode 100644 index 000000000..2a0c82037 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-05.js @@ -0,0 +1,33 @@ +/* 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 the keyboard navigation for the pane toggle using +* space and enter +*/ + +add_task(function* () { + let {inspector} = yield openInspectorForURL("about:blank", "side"); + let panel = inspector.panelDoc.querySelector("#inspector-splitter-box .controlled"); + + let button = inspector.panelDoc.querySelector(".sidebar-toggle"); + + ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state"); + + yield togglePane(button, "Press on the toggle button", panel, "VK_RETURN"); + ok(panel.classList.contains("pane-collapsed"), "The panel is in collapsed state"); + + yield togglePane(button, "Press on the toggle button to expand the panel again", + panel, "VK_SPACE"); + ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state"); +}); + +function* togglePane(button, message, panel, keycode) { + let onTransitionEnd = once(panel, "transitionend"); + info(message); + button.focus(); + EventUtils.synthesizeKey(keycode, {}); + yield onTransitionEnd; +} diff --git a/devtools/client/inspector/test/browser_inspector_picker-stop-on-destroy.js b/devtools/client/inspector/test/browser_inspector_picker-stop-on-destroy.js new file mode 100644 index 000000000..bc81b9661 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_picker-stop-on-destroy.js @@ -0,0 +1,30 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// Test that the highlighter's picker should be stopped when the toolbox is +// closed + +const TEST_URI = "data:text/html;charset=utf-8," + + "<p>testing the highlighter goes away on destroy</p>"; + +add_task(function* () { + let { inspector, toolbox } = yield openInspectorForURL(TEST_URI); + let pickerStopped = toolbox.once("picker-stopped"); + + yield selectNode("p", inspector); + + info("Inspector displayed and ready, starting the picker."); + yield startPicker(toolbox); + + info("Destroying the toolbox."); + yield toolbox.destroy(); + + info("Waiting for the picker-stopped event that should be fired when the " + + "toolbox is destroyed."); + yield pickerStopped; + + ok(true, "picker-stopped event fired after switch tools so picker is closed"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_picker-stop-on-tool-change.js b/devtools/client/inspector/test/browser_inspector_picker-stop-on-tool-change.js new file mode 100644 index 000000000..37dc82ec1 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_picker-stop-on-tool-change.js @@ -0,0 +1,27 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// Test that the highlighter's picker is stopped when a different tool is +// selected + +const TEST_URI = "data:text/html;charset=UTF-8," + + "testing the highlighter goes away on tool selection"; + +add_task(function* () { + let { toolbox } = yield openInspectorForURL(TEST_URI); + let pickerStopped = toolbox.once("picker-stopped"); + + info("Starting the inspector picker"); + yield startPicker(toolbox); + + info("Selecting another tool than the inspector in the toolbox"); + yield toolbox.selectNextTool(); + + info("Waiting for the picker-stopped event to be fired"); + yield pickerStopped; + + ok(true, "picker-stopped event fired after switch tools; picker is closed"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_portrait_mode.js b/devtools/client/inspector/test/browser_inspector_portrait_mode.js new file mode 100644 index 000000000..04fcc2b56 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_portrait_mode.js @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that the inspector splitter is properly initialized in horizontal mode if the +// inspector starts in portrait mode. + +add_task(function* () { + let { inspector, toolbox } = yield openInspectorForURL( + "data:text/html;charset=utf-8,<h1>foo</h1><span>bar</span>", "window"); + + let hostWindow = toolbox.win.parent; + let originalWidth = hostWindow.outerWidth; + let originalHeight = hostWindow.outerHeight; + + let splitter = inspector.panelDoc.querySelector(".inspector-sidebar-splitter"); + + // If the inspector is not already in landscape mode. + if (!splitter.classList.contains("vert")) { + info("Resize toolbox window to force inspector to landscape mode"); + let onClassnameMutation = waitForClassMutation(splitter); + hostWindow.resizeTo(800, 500); + yield onClassnameMutation; + + ok(splitter.classList.contains("vert"), "Splitter is in vertical mode"); + } + + info("Resize toolbox window to force inspector to portrait mode"); + let onClassnameMutation = waitForClassMutation(splitter); + hostWindow.resizeTo(500, 500); + yield onClassnameMutation; + + ok(splitter.classList.contains("horz"), "Splitter is in horizontal mode"); + + info("Close the inspector"); + yield gDevTools.closeToolbox(toolbox.target); + + info("Reopen inspector"); + ({ inspector, toolbox } = yield openInspector("window")); + + // Devtools window should still be 500px * 500px, inspector should still be in portrait. + splitter = inspector.panelDoc.querySelector(".inspector-sidebar-splitter"); + ok(splitter.classList.contains("horz"), "Splitter is in horizontal mode"); + + info("Restore original window size"); + toolbox.win.parent.resizeTo(originalWidth, originalHeight); +}); + +/** + * Helper waiting for a class attribute mutation on the provided target. Returns a + * promise. + * + * @param {Node} target + * Node to observe + * @return {Promise} promise that will resolve upon receiving a mutation for the class + * attribute on the target. + */ +function waitForClassMutation(target) { + return new Promise(resolve => { + let observer = new MutationObserver((mutations) => { + for (let mutation of mutations) { + if (mutation.attributeName === "class") { + observer.disconnect(); + resolve(); + return; + } + } + }); + observer.observe(target, { attributes: true }); + }); +} + +registerCleanupFunction(function () { + // Restore the host type for other tests. + Services.prefs.clearUserPref("devtools.toolbox.host"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_pseudoclass-lock.js b/devtools/client/inspector/test/browser_inspector_pseudoclass-lock.js new file mode 100644 index 000000000..bd98bd58f --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_pseudoclass-lock.js @@ -0,0 +1,160 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* globals getTestActorWithoutToolbox */ +"use strict"; + +// Test that locking the pseudoclass displays correctly in the ruleview + +const PSEUDO = ":hover"; +const TEST_URL = "data:text/html;charset=UTF-8," + + "<head>" + + " <style>div {color:red;} div:hover {color:blue;}</style>" + + "</head>" + + "<body>" + + ' <div id="parent-div">' + + ' <div id="div-1">test div</div>' + + ' <div id="div-2">test div2</div>' + + " </div>" + + "</body>"; + +add_task(function* () { + info("Creating the test tab and opening the rule-view"); + let {toolbox, inspector, testActor} = yield openInspectorForURL(TEST_URL); + + info("Selecting the ruleview sidebar"); + inspector.sidebar.select("ruleview"); + + let view = inspector.ruleview.view; + + info("Selecting the test node"); + yield selectNode("#div-1", inspector); + + yield togglePseudoClass(inspector); + yield assertPseudoAddedToNode(inspector, testActor, view, "#div-1"); + + yield togglePseudoClass(inspector); + yield assertPseudoRemovedFromNode(testActor, "#div-1"); + yield assertPseudoRemovedFromView(inspector, testActor, view, "#div-1"); + + yield togglePseudoClass(inspector); + yield testNavigate(inspector, testActor, view); + + info("Toggle pseudo on the parent and ensure everything is toggled off"); + yield selectNode("#parent-div", inspector); + yield togglePseudoClass(inspector); + yield assertPseudoRemovedFromNode(testActor, "#div-1"); + yield assertPseudoRemovedFromView(inspector, testActor, view, "#div-1"); + + yield togglePseudoClass(inspector); + info("Assert pseudo is dismissed when toggling it on a sibling node"); + yield selectNode("#div-2", inspector); + yield togglePseudoClass(inspector); + yield assertPseudoAddedToNode(inspector, testActor, view, "#div-2"); + let hasLock = yield testActor.hasPseudoClassLock("#div-1", PSEUDO); + ok(!hasLock, "pseudo-class lock has been removed for the previous locked node"); + + info("Destroying the toolbox"); + let tab = toolbox.target.tab; + yield toolbox.destroy(); + + // As the toolbox get detroyed, we need to fetch a new test-actor + testActor = yield getTestActorWithoutToolbox(tab); + + yield assertPseudoRemovedFromNode(testActor, "#div-1"); + yield assertPseudoRemovedFromNode(testActor, "#div-2"); +}); + +function* togglePseudoClass(inspector) { + info("Toggle the pseudoclass, wait for it to be applied"); + + // Give the inspector panels a chance to update when the pseudoclass changes + let onPseudo = inspector.selection.once("pseudoclass"); + let onRefresh = inspector.once("rule-view-refreshed"); + + // Walker uses SDK-events so calling walker.once does not return a promise. + let onMutations = once(inspector.walker, "mutations"); + + yield inspector.togglePseudoClass(PSEUDO); + + yield onPseudo; + yield onRefresh; + yield onMutations; +} + +function* testNavigate(inspector, testActor, ruleview) { + yield selectNode("#parent-div", inspector); + + info("Make sure the pseudoclass is still on after navigating to a parent"); + + ok((yield testActor.hasPseudoClassLock("#div-1", PSEUDO)), + "pseudo-class lock is still applied after inspecting ancestor"); + + yield selectNode("#div-2", inspector); + + info("Make sure the pseudoclass is still set after navigating to a " + + "non-hierarchy node"); + ok(yield testActor.hasPseudoClassLock("#div-1", PSEUDO), + "pseudo-class lock is still on after inspecting sibling node"); + + yield selectNode("#div-1", inspector); +} + +function* showPickerOn(selector, inspector) { + let nodeFront = yield getNodeFront(selector, inspector); + yield inspector.highlighter.showBoxModel(nodeFront); +} + +function* assertPseudoAddedToNode(inspector, testActor, ruleview, selector) { + info("Make sure the pseudoclass lock is applied to " + selector + " and its ancestors"); + + let hasLock = yield testActor.hasPseudoClassLock(selector, PSEUDO); + ok(hasLock, "pseudo-class lock has been applied"); + hasLock = yield testActor.hasPseudoClassLock("#parent-div", PSEUDO); + ok(hasLock, "pseudo-class lock has been applied"); + hasLock = yield testActor.hasPseudoClassLock("body", PSEUDO); + ok(hasLock, "pseudo-class lock has been applied"); + + info("Check that the ruleview contains the pseudo-class rule"); + let rules = ruleview.element.querySelectorAll( + ".ruleview-rule.theme-separator"); + is(rules.length, 3, + "rule view is showing 3 rules for pseudo-class locked div"); + is(rules[1]._ruleEditor.rule.selectorText, "div:hover", + "rule view is showing " + PSEUDO + " rule"); + + info("Show the highlighter on " + selector); + yield showPickerOn(selector, inspector); + + info("Check that the infobar selector contains the pseudo-class"); + let value = yield testActor.getHighlighterNodeTextContent( + "box-model-infobar-pseudo-classes"); + is(value, PSEUDO, "pseudo-class in infobar selector"); + yield inspector.highlighter.hideBoxModel(); +} + +function* assertPseudoRemovedFromNode(testActor, selector) { + info("Make sure the pseudoclass lock is removed from #div-1 and its " + + "ancestors"); + + let hasLock = yield testActor.hasPseudoClassLock(selector, PSEUDO); + ok(!hasLock, "pseudo-class lock has been removed"); + hasLock = yield testActor.hasPseudoClassLock("#parent-div", PSEUDO); + ok(!hasLock, "pseudo-class lock has been removed"); + hasLock = yield testActor.hasPseudoClassLock("body", PSEUDO); + ok(!hasLock, "pseudo-class lock has been removed"); +} + +function* assertPseudoRemovedFromView(inspector, testActor, ruleview, selector) { + info("Check that the ruleview no longer contains the pseudo-class rule"); + let rules = ruleview.element.querySelectorAll( + ".ruleview-rule.theme-separator"); + is(rules.length, 2, "rule view is showing 2 rules after removing lock"); + + yield showPickerOn(selector, inspector); + + let value = yield testActor.getHighlighterNodeTextContent( + "box-model-infobar-pseudo-classes"); + is(value, "", "pseudo-class removed from infobar selector"); + yield inspector.highlighter.hideBoxModel(); +} diff --git a/devtools/client/inspector/test/browser_inspector_pseudoclass-menu.js b/devtools/client/inspector/test/browser_inspector_pseudoclass-menu.js new file mode 100644 index 000000000..45bd82b76 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_pseudoclass-menu.js @@ -0,0 +1,46 @@ +/* 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 the inspector has the correct pseudo-class locking menu items and +// that these items actually work + +const TEST_URI = "data:text/html;charset=UTF-8," + + "pseudo-class lock node menu tests" + + "<div>test div</div>"; +const PSEUDOS = ["hover", "active", "focus"]; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URI); + yield selectNode("div", inspector); + + let allMenuItems = openContextMenuAndGetAllItems(inspector); + + yield testMenuItems(testActor, allMenuItems, inspector); +}); + +function* testMenuItems(testActor, allMenuItems, inspector) { + for (let pseudo of PSEUDOS) { + let menuItem = + allMenuItems.find(item => item.id === "node-menu-pseudo-" + pseudo); + ok(menuItem, ":" + pseudo + " menuitem exists"); + is(menuItem.disabled, false, ":" + pseudo + " menuitem is enabled"); + + // Give the inspector panels a chance to update when the pseudoclass changes + let onPseudo = inspector.selection.once("pseudoclass"); + let onRefresh = inspector.once("rule-view-refreshed"); + + // Walker uses SDK-events so calling walker.once does not return a promise. + let onMutations = once(inspector.walker, "mutations"); + + menuItem.click(); + + yield onPseudo; + yield onRefresh; + yield onMutations; + + let hasLock = yield testActor.hasPseudoClassLock("div", ":" + pseudo); + ok(hasLock, "pseudo-class lock has been applied"); + } +} diff --git a/devtools/client/inspector/test/browser_inspector_reload-01.js b/devtools/client/inspector/test/browser_inspector_reload-01.js new file mode 100644 index 000000000..61a1dde27 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_reload-01.js @@ -0,0 +1,32 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// A test to ensure reloading a page doesn't break the inspector. + +// Reload should reselect the currently selected markup view element. +// This should work even when an element whose selector needs escaping +// is selected (bug 1002280). +const TEST_URI = "data:text/html,<p id='1'>p</p>"; + +add_task(function* () { + let { inspector, testActor } = yield openInspectorForURL(TEST_URI); + yield selectNode("p", inspector); + + let markupLoaded = inspector.once("markuploaded"); + + info("Reloading page."); + yield testActor.eval("location.reload()"); + + info("Waiting for markupview to load after reload."); + yield markupLoaded; + + let nodeFront = yield getNodeFront("p", inspector); + is(inspector.selection.nodeFront, nodeFront, "<p> selected after reload."); + + info("Selecting a node to see that inspector still works."); + yield selectNode("body", inspector); +}); diff --git a/devtools/client/inspector/test/browser_inspector_reload-02.js b/devtools/client/inspector/test/browser_inspector_reload-02.js new file mode 100644 index 000000000..c9940a828 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_reload-02.js @@ -0,0 +1,48 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// A test to ensure reloading a page doesn't break the inspector. + +// Reload should reselect the currently selected markup view element. +// This should work even when an element whose selector is inaccessible +// is selected (bug 1038651). +const TEST_URI = 'data:text/xml,<?xml version="1.0" standalone="no"?>' + +'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"' + +' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' + +'<svg width="4cm" height="4cm" viewBox="0 0 400 400"' + +' xmlns="http://www.w3.org/2000/svg" version="1.1">' + +" <title>Example triangle01- simple example of a path</title>" + +" <desc>A path that draws a triangle</desc>" + +' <rect x="1" y="1" width="398" height="398"' + +' fill="none" stroke="blue" />' + +' <path d="M 100 100 L 300 100 L 200 300 z"' + +' fill="red" stroke="blue" stroke-width="3" />' + +"</svg>"; + +add_task(function* () { + let { inspector, testActor } = yield openInspectorForURL(TEST_URI); + + let markupLoaded = inspector.once("markuploaded"); + + info("Reloading page."); + yield testActor.eval("location.reload()"); + + info("Waiting for markupview to load after reload."); + yield markupLoaded; + + let svgFront = yield getNodeFront("svg", inspector); + is(inspector.selection.nodeFront, svgFront, "<svg> selected after reload."); + + info("Selecting a node to see that inspector still works."); + yield selectNode("rect", inspector); + + info("Reloading page."); + yield testActor.eval("location.reload"); + + let rectFront = yield getNodeFront("rect", inspector); + is(inspector.selection.nodeFront, rectFront, "<rect> selected after reload."); +}); diff --git a/devtools/client/inspector/test/browser_inspector_remove-iframe-during-load.js b/devtools/client/inspector/test/browser_inspector_remove-iframe-during-load.js new file mode 100644 index 000000000..2058b85fa --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_remove-iframe-during-load.js @@ -0,0 +1,48 @@ +/* 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"; + +// Testing that the inspector doesn't go blank when navigating to a page that +// deletes an iframe while loading. + +const TEST_URL = URL_ROOT + "doc_inspector_remove-iframe-during-load.html"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL("about:blank"); + yield selectNode("body", inspector); + + // We do not want to wait for the inspector to be fully ready before testing + // so we load TEST_URL and just wait for the content window to be done loading + yield testActor.loadAndWaitForCustomEvent(TEST_URL); + + // The content doc contains a script that creates iframes and deletes them + // immediately after. It does this before the load event, after + // DOMContentLoaded and after load. This is what used to make the inspector go + // blank when navigating to that page. + // At this stage, there should be no iframes in the page anymore. + ok(!(yield testActor.hasNode("iframe")), + "Iframes added by the content page should have been removed"); + + // Create/remove an extra one now, after the load event. + info("Creating and removing an iframe."); + let onMarkupLoaded = inspector.once("markuploaded"); + testActor.eval("new " + function () { + let iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + iframe.remove(); + }); + + ok(!(yield testActor.hasNode("iframe")), + "The after-load iframe should have been removed."); + + info("Waiting for markup-view to load."); + yield onMarkupLoaded; + + // Assert that the markup-view is displayed and works + ok(!(yield testActor.hasNode("iframe")), "Iframe has been removed."); + is((yield testActor.getProperty("#yay", "textContent")), "load", + "Load event fired."); + + yield selectNode("#yay", inspector); +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-01.js b/devtools/client/inspector/test/browser_inspector_search-01.js new file mode 100644 index 000000000..a4fd4d424 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-01.js @@ -0,0 +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/ */ +/* eslint no-inline-comments: 0 */ +"use strict"; + +requestLongerTimeout(2); + +// Test that searching for nodes in the search field actually selects those +// nodes. + +const TEST_URL = URL_ROOT + "doc_inspector_search.html"; + +// The various states of the inspector: [key, id, isValid] +// [ +// what key to press, +// what id should be selected after the keypress, +// is the searched text valid selector +// ] +const KEY_STATES = [ + ["#", "b1", true], // # + ["d", "b1", true], // #d + ["1", "b1", true], // #d1 + ["VK_RETURN", "d1", true], // #d1 + ["VK_BACK_SPACE", "d1", true], // #d + ["2", "d1", true], // #d2 + ["VK_RETURN", "d2", true], // #d2 + ["2", "d2", true], // #d22 + ["VK_RETURN", "d2", false], // #d22 + ["VK_BACK_SPACE", "d2", false], // #d2 + ["VK_RETURN", "d2", true], // #d2 + ["VK_BACK_SPACE", "d2", true], // #d + ["1", "d2", true], // #d1 + ["VK_RETURN", "d1", true], // #d1 + ["VK_BACK_SPACE", "d1", true], // #d + ["VK_BACK_SPACE", "d1", true], // # + ["VK_BACK_SPACE", "d1", true], // + ["d", "d1", true], // d + ["i", "d1", true], // di + ["v", "d1", true], // div + [".", "d1", true], // div. + ["c", "d1", true], // div.c + ["VK_UP", "d1", true], // div.c1 + ["VK_TAB", "d1", true], // div.c1 + ["VK_RETURN", "d2", true], // div.c1 + ["VK_BACK_SPACE", "d2", true], // div.c + ["VK_BACK_SPACE", "d2", true], // div. + ["VK_BACK_SPACE", "d2", true], // div + ["VK_BACK_SPACE", "d2", true], // di + ["VK_BACK_SPACE", "d2", true], // d + ["VK_BACK_SPACE", "d2", true], // + [".", "d2", true], // . + ["c", "d2", true], // .c + ["1", "d2", true], // .c1 + ["VK_RETURN", "d2", true], // .c1 + ["VK_RETURN", "s2", true], // .c1 + ["VK_RETURN", "p1", true], // .c1 + ["P", "p1", true], // .c1P + ["VK_RETURN", "p1", false], // .c1P +]; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URL); + let { searchBox } = inspector; + + yield selectNode("#b1", inspector); + yield focusSearchBoxUsingShortcut(inspector.panelWin); + + let index = 0; + for (let [ key, id, isValid ] of KEY_STATES) { + info(index + ": Pressing key " + key + " to get id " + id + "."); + let done = inspector.searchSuggestions.once("processing-done"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + yield done; + info("Got processing-done event"); + + if (key === "VK_RETURN") { + info("Waiting for " + (isValid ? "NO " : "") + "results"); + yield inspector.search.once("search-result"); + } + + info("Waiting for search query to complete"); + yield inspector.searchSuggestions._lastQuery; + + info(inspector.selection.nodeFront.id + " is selected with text " + + searchBox.value); + let nodeFront = yield getNodeFront("#" + id, inspector); + is(inspector.selection.nodeFront, nodeFront, + "Correct node is selected for state " + index); + + is(!searchBox.classList.contains("devtools-style-searchbox-no-match"), isValid, + "Correct searchbox result state for state " + index); + + index++; + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-02.js b/devtools/client/inspector/test/browser_inspector_search-02.js new file mode 100644 index 000000000..5e75f5dd2 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-02.js @@ -0,0 +1,169 @@ +/* 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"; + +// Testing that searching for combining selectors using the inspector search +// field produces correct suggestions. + +const TEST_URL = URL_ROOT + "doc_inspector_search-suggestions.html"; + +// An array of (key, suggestions) pairs where key is a key to press and +// suggestions is an array of suggestions that should be shown in the popup. +// Suggestion is an object with label of the entry and optional count +// (defaults to 1) +const TEST_DATA = [ + { + key: "d", + suggestions: [ + {label: "div"}, + {label: "#d1"}, + {label: "#d2"} + ] + }, + { + key: "i", + suggestions: [{label: "div"}] + }, + { + key: "v", + suggestions: [] + }, + { + key: " ", + suggestions: [ + {label: "div div"}, + {label: "div span"} + ] + }, + { + key: ">", + suggestions: [ + {label: "div >div"}, + {label: "div >span"} + ] + }, + { + key: "VK_BACK_SPACE", + suggestions: [ + {label: "div div"}, + {label: "div span"} + ] + }, + { + key: "+", + suggestions: [{label: "div +span"}] + }, + { + key: "VK_BACK_SPACE", + suggestions: [ + {label: "div div"}, + {label: "div span"} + ] + }, + { + key: "VK_BACK_SPACE", + suggestions: [] + }, + { + key: "VK_BACK_SPACE", + suggestions: [{label: "div"}] + }, + { + key: "VK_BACK_SPACE", + suggestions: [ + {label: "div"}, + {label: "#d1"}, + {label: "#d2"} + ] + }, + { + key: "VK_BACK_SPACE", + suggestions: [] + }, + { + key: "p", + suggestions: [ + {label: "p"}, + {label: "#p1"}, + {label: "#p2"}, + {label: "#p3"}, + ] + }, + { + key: " ", + suggestions: [{label: "p strong"}] + }, + { + key: "+", + suggestions: [ + {label: "p +button" }, + {label: "p +p"} + ] + }, + { + key: "b", + suggestions: [{label: "p +button"}] + }, + { + key: "u", + suggestions: [{label: "p +button"}] + }, + { + key: "t", + suggestions: [{label: "p +button"}] + }, + { + key: "t", + suggestions: [{label: "p +button"}] + }, + { + key: "o", + suggestions: [{label: "p +button"}] + }, + { + key: "n", + suggestions: [] + }, + { + key: "+", + suggestions: [{label: "p +button+p"}] + } +]; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URL); + let searchBox = inspector.searchBox; + let popup = inspector.searchSuggestions.searchPopup; + + yield focusSearchBoxUsingShortcut(inspector.panelWin); + + for (let { key, suggestions } of TEST_DATA) { + info("Pressing " + key + " to get " + formatSuggestions(suggestions)); + + let command = once(searchBox, "input"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + yield command; + + info("Waiting for search query to complete"); + yield inspector.searchSuggestions._lastQuery; + + info("Query completed. Performing checks for input '" + searchBox.value + + "' - key pressed: " + key); + let actualSuggestions = popup.getItems().reverse(); + + is(popup.isOpen ? actualSuggestions.length : 0, suggestions.length, + "There are expected number of suggestions."); + + for (let i = 0; i < suggestions.length; i++) { + is(actualSuggestions[i].label, suggestions[i].label, + "The suggestion at " + i + "th index is correct."); + } + } +}); + +function formatSuggestions(suggestions) { + return "[" + suggestions + .map(s => "'" + s.label + "'") + .join(", ") + "]"; +} diff --git a/devtools/client/inspector/test/browser_inspector_search-03.js b/devtools/client/inspector/test/browser_inspector_search-03.js new file mode 100644 index 000000000..215b536a6 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-03.js @@ -0,0 +1,250 @@ +/* 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"; + +// Testing that searching for elements using the inspector search field +// produces correct suggestions. + +const TEST_URL = URL_ROOT + "doc_inspector_search.html"; + +// An array of (key, suggestions) pairs where key is a key to press and +// suggestions is an array of suggestions that should be shown in the popup. +// Suggestion is an object with label of the entry and optional count +// (defaults to 1) +var TEST_DATA = [ + { + key: "d", + suggestions: [ + {label: "div"}, + {label: "#d1"}, + {label: "#d2"} + ] + }, + { + key: "i", + suggestions: [{label: "div"}] + }, + { + key: "v", + suggestions: [] + }, + { + key: ".", + suggestions: [{label: "div.c1"}] + }, + { + key: "VK_BACK_SPACE", + suggestions: [] + }, + { + key: "#", + suggestions: [ + {label: "div#d1"}, + {label: "div#d2"} + ] + }, + { + key: "VK_BACK_SPACE", + suggestions: [] + }, + { + key: "VK_BACK_SPACE", + suggestions: [{label: "div"}] + }, + { + key: "VK_BACK_SPACE", + suggestions: [ + {label: "div"}, + {label: "#d1"}, + {label: "#d2"} + ] + }, + { + key: "VK_BACK_SPACE", + suggestions: [] + }, + { + key: ".", + suggestions: [ + {label: ".c1"}, + {label: ".c2"} + ] + }, + { + key: "c", + suggestions: [ + {label: ".c1"}, + {label: ".c2"} + ] + }, + { + key: "2", + suggestions: [] + }, + { + key: "VK_BACK_SPACE", + suggestions: [ + {label: ".c1"}, + {label: ".c2"} + ] + }, + { + key: "1", + suggestions: [] + }, + { + key: "#", + suggestions: [ + {label: "#d2"}, + {label: "#p1"}, + {label: "#s2"} + ] + }, + { + key: "VK_BACK_SPACE", + suggestions: [] + }, + { + key: "VK_BACK_SPACE", + suggestions: [ + {label: ".c1"}, + {label: ".c2"} + ] + }, + { + key: "VK_BACK_SPACE", + suggestions: [ + {label: ".c1"}, + {label: ".c2"} + ] + }, + { + key: "VK_BACK_SPACE", + suggestions: [] + }, + { + key: "#", + suggestions: [ + {label: "#b1"}, + {label: "#d1"}, + {label: "#d2"}, + {label: "#p1"}, + {label: "#p2"}, + {label: "#p3"}, + {label: "#s1"}, + {label: "#s2"} + ] + }, + { + key: "p", + suggestions: [ + {label: "#p1"}, + {label: "#p2"}, + {label: "#p3"} + ] + }, + { + key: "VK_BACK_SPACE", + suggestions: [ + {label: "#b1"}, + {label: "#d1"}, + {label: "#d2"}, + {label: "#p1"}, + {label: "#p2"}, + {label: "#p3"}, + {label: "#s1"}, + {label: "#s2"} + ] + }, + { + key: "VK_BACK_SPACE", + suggestions: [] + }, + { + key: "p", + suggestions: [ + {label: "p"}, + {label: "#p1"}, + {label: "#p2"}, + {label: "#p3"} + ] + }, + { + key: "[", suggestions: [] + }, + { + key: "i", suggestions: [] + }, + { + key: "d", suggestions: [] + }, + { + key: "*", suggestions: [] + }, + { + key: "=", suggestions: [] + }, + { + key: "p", suggestions: [] + }, + { + key: "]", suggestions: [] + }, + { + key: ".", + suggestions: [ + {label: "p[id*=p].c1"}, + {label: "p[id*=p].c2"} + ] + }, + { + key: "VK_BACK_SPACE", + suggestions: [] + }, + { + key: "#", + suggestions: [ + {label: "p[id*=p]#p1"}, + {label: "p[id*=p]#p2"}, + {label: "p[id*=p]#p3"} + ] + } +]; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URL); + let searchBox = inspector.searchBox; + let popup = inspector.searchSuggestions.searchPopup; + + yield focusSearchBoxUsingShortcut(inspector.panelWin); + + for (let { key, suggestions } of TEST_DATA) { + info("Pressing " + key + " to get " + formatSuggestions(suggestions)); + + let command = once(searchBox, "input"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + yield command; + + info("Waiting for search query to complete"); + yield inspector.searchSuggestions._lastQuery; + + info("Query completed. Performing checks for input '" + + searchBox.value + "'"); + let actualSuggestions = popup.getItems().reverse(); + + is(popup.isOpen ? actualSuggestions.length : 0, suggestions.length, + "There are expected number of suggestions."); + + for (let i = 0; i < suggestions.length; i++) { + is(actualSuggestions[i].label, suggestions[i].label, + "The suggestion at " + i + "th index is correct."); + } + } +}); + +function formatSuggestions(suggestions) { + return "[" + suggestions + .map(s => "'" + s.label + "'") + .join(", ") + "]"; +} diff --git a/devtools/client/inspector/test/browser_inspector_search-04.js b/devtools/client/inspector/test/browser_inspector_search-04.js new file mode 100644 index 000000000..a5aee8156 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-04.js @@ -0,0 +1,112 @@ +/* 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"; + +// Testing that searching for elements inside iframes does work. + +const IFRAME_SRC = "doc_inspector_search.html"; +const TEST_URL = "data:text/html;charset=utf-8," + + "<div class=\"c1 c2\">" + + "<iframe src=\"" + URL_ROOT + IFRAME_SRC + "\"></iframe>" + + "<iframe src=\"" + URL_ROOT + IFRAME_SRC + "\"></iframe>"; + +// An array of (key, suggestions) pairs where key is a key to press and +// suggestions is an array of suggestions that should be shown in the popup. +// Suggestion is an object with label of the entry and optional count +// (defaults to 1) +var TEST_DATA = [ + { + key: "d", + suggestions: [ + {label: "div"}, + {label: "#d1"}, + {label: "#d2"} + ] + }, + { + key: "i", + suggestions: [{label: "div"}] + }, + { + key: "v", + suggestions: [] + }, + { + key: "VK_BACK_SPACE", + suggestions: [{label: "div"}] + }, + { + key: "VK_BACK_SPACE", + suggestions: [ + {label: "div"}, + {label: "#d1"}, + {label: "#d2"} + ] + }, + { + key: "VK_BACK_SPACE", + suggestions: [] + }, + { + key: ".", + suggestions: [ + {label: ".c1"}, + {label: ".c2"} + ] + }, + { + key: "VK_BACK_SPACE", + suggestions: [] + }, + { + key: "#", + suggestions: [ + {label: "#b1"}, + {label: "#d1"}, + {label: "#d2"}, + {label: "#p1"}, + {label: "#p2"}, + {label: "#p3"}, + {label: "#s1"}, + {label: "#s2"} + ] + }, +]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + let searchBox = inspector.searchBox; + let popup = inspector.searchSuggestions.searchPopup; + + yield focusSearchBoxUsingShortcut(inspector.panelWin); + + for (let {key, suggestions} of TEST_DATA) { + info("Pressing " + key + " to get " + formatSuggestions(suggestions)); + + let command = once(searchBox, "input"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + yield command; + + info("Waiting for search query to complete"); + yield inspector.searchSuggestions._lastQuery; + + info("Query completed. Performing checks for input '" + + searchBox.value + "'"); + let actualSuggestions = popup.getItems().reverse(); + + is(popup.isOpen ? actualSuggestions.length : 0, suggestions.length, + "There are expected number of suggestions."); + + for (let i = 0; i < suggestions.length; i++) { + is(actualSuggestions[i].label, suggestions[i].label, + "The suggestion at " + i + "th index is correct."); + } + } +}); + +function formatSuggestions(suggestions) { + return "[" + suggestions + .map(s => "'" + s.label + "'") + .join(", ") + "]"; +} diff --git a/devtools/client/inspector/test/browser_inspector_search-05.js b/devtools/client/inspector/test/browser_inspector_search-05.js new file mode 100644 index 000000000..542d0ccc5 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-05.js @@ -0,0 +1,93 @@ +/* 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"; + +// Testing that when search results contain suggestions for nodes in other +// frames, selecting these suggestions actually selects the right nodes. + +requestLongerTimeout(2); + +const IFRAME_SRC = "doc_inspector_search.html"; +const NESTED_IFRAME_SRC = ` + <button id="b1">Nested button</button> + <iframe id="iframe-4" src="${URL_ROOT + IFRAME_SRC}"></iframe> +`; +const TEST_URL = ` + <iframe id="iframe-1" src="${URL_ROOT + IFRAME_SRC}"></iframe> + <iframe id="iframe-2" src="${URL_ROOT + IFRAME_SRC}"></iframe> + <iframe id="iframe-3" + src="data:text/html;charset=utf-8,${encodeURI(NESTED_IFRAME_SRC)}"> + </iframe> +`; + +add_task(function* () { + let {inspector} = yield openInspectorForURL( + "data:text/html;charset=utf-8," + encodeURI(TEST_URL)); + + info("Focus the search box"); + yield focusSearchBoxUsingShortcut(inspector.panelWin); + + info("Enter # to search for all ids"); + let processingDone = once(inspector.searchSuggestions, "processing-done"); + EventUtils.synthesizeKey("#", {}, inspector.panelWin); + yield processingDone; + + info("Wait for search query to complete"); + yield inspector.searchSuggestions._lastQuery; + + info("Press tab to fill the search input with the first suggestion"); + processingDone = once(inspector.searchSuggestions, "processing-done"); + EventUtils.synthesizeKey("VK_TAB", {}, inspector.panelWin); + yield processingDone; + + info("Press enter and expect a new selection"); + let onSelect = inspector.once("inspector-updated"); + EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin); + yield onSelect; + + yield checkCorrectButton(inspector, "#iframe-1"); + + info("Press enter to cycle through multiple nodes matching this suggestion"); + onSelect = inspector.once("inspector-updated"); + EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin); + yield onSelect; + + yield checkCorrectButton(inspector, "#iframe-2"); + + info("Press enter to cycle through multiple nodes matching this suggestion"); + onSelect = inspector.once("inspector-updated"); + EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin); + yield onSelect; + + yield checkCorrectButton(inspector, "#iframe-3"); + + info("Press enter to cycle through multiple nodes matching this suggestion"); + onSelect = inspector.once("inspector-updated"); + EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin); + yield onSelect; + + yield checkCorrectButton(inspector, "#iframe-4"); + + info("Press enter to cycle through multiple nodes matching this suggestion"); + onSelect = inspector.once("inspector-updated"); + EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin); + yield onSelect; + + yield checkCorrectButton(inspector, "#iframe-1"); +}); + +let checkCorrectButton = Task.async(function* (inspector, frameSelector) { + let {walker} = inspector; + let node = inspector.selection.nodeFront; + + ok(node.id, "b1", "The selected node is #b1"); + ok(node.tagName.toLowerCase(), "button", + "The selected node is <button>"); + + let selectedNodeDoc = yield walker.document(node); + let iframe = yield walker.multiFrameQuerySelectorAll(frameSelector); + iframe = yield iframe.item(0); + let iframeDoc = (yield walker.children(iframe)).nodes[0]; + is(selectedNodeDoc, iframeDoc, "The selected node is in " + frameSelector); +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-06.js b/devtools/client/inspector/test/browser_inspector_search-06.js new file mode 100644 index 000000000..1b3950c00 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-06.js @@ -0,0 +1,87 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Check that searching again for nodes after they are removed or added from the +// DOM works correctly. + +const TEST_URL = URL_ROOT + "doc_inspector_search.html"; + +add_task(function* () { + let { inspector, testActor } = yield openInspectorForURL(TEST_URL); + + info("Searching for test node #d1"); + yield focusSearchBoxUsingShortcut(inspector.panelWin); + yield synthesizeKeys(["#", "d", "1", "VK_RETURN"], inspector); + + yield inspector.search.once("search-result"); + assertHasResult(inspector, true); + + info("Removing node #d1"); + // Expect an inspector-updated event here, because removing #d1 causes the + // breadcrumbs to update (since #d1 is displayed in it). + let onUpdated = inspector.once("inspector-updated"); + yield mutatePage(inspector, testActor, + "document.getElementById(\"d1\").remove()"); + yield onUpdated; + + info("Pressing return button to search again for node #d1."); + yield synthesizeKeys("VK_RETURN", inspector); + + yield inspector.search.once("search-result"); + assertHasResult(inspector, false); + + info("Emptying the field and searching for a node that doesn't exist: #d3"); + let keys = ["VK_BACK_SPACE", "VK_BACK_SPACE", "VK_BACK_SPACE", "#", "d", "3", + "VK_RETURN"]; + yield synthesizeKeys(keys, inspector); + + yield inspector.search.once("search-result"); + assertHasResult(inspector, false); + + info("Create the #d3 node in the page"); + // No need to expect an inspector-updated event here, Creating #d3 isn't going + // to update the breadcrumbs in any ways. + yield mutatePage(inspector, testActor, + `document.getElementById("d2").insertAdjacentHTML( + "afterend", "<div id=d3></div>")`); + + info("Pressing return button to search again for node #d3."); + yield synthesizeKeys("VK_RETURN", inspector); + + yield inspector.search.once("search-result"); + assertHasResult(inspector, true); + + // Catch-all event for remaining server requests when searching for the new + // node. + yield inspector.once("inspector-updated"); +}); + +function* synthesizeKeys(keys, inspector) { + if (typeof keys === "string") { + keys = [keys]; + } + + for (let key of keys) { + info("Synthesizing key " + key + " in the search box"); + let eventHandled = once(inspector.searchBox, "keypress", true); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + yield eventHandled; + info("Waiting for the search query to complete"); + yield inspector.searchSuggestions._lastQuery; + } +} + +function assertHasResult(inspector, expectResult) { + is(inspector.searchBox.classList.contains("devtools-style-searchbox-no-match"), + !expectResult, + "There are" + (expectResult ? "" : " no") + " search results"); +} + +function* mutatePage(inspector, testActor, expression) { + let onMutation = inspector.once("markupmutation"); + yield testActor.eval(expression); + yield onMutation; +} diff --git a/devtools/client/inspector/test/browser_inspector_search-07.js b/devtools/client/inspector/test/browser_inspector_search-07.js new file mode 100644 index 000000000..79e2021cd --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-07.js @@ -0,0 +1,49 @@ +/* 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 searching for classes on SVG elements does work (see bug 1219920). + +const TEST_URL = URL_ROOT + "doc_inspector_search-svg.html"; + +// An array of (key, suggestions) pairs where key is a key to press and +// suggestions is an array of suggestions that should be shown in the popup. +const TEST_DATA = [{ + key: "c", + suggestions: ["circle", "clipPath", ".class1", ".class2"] +}, { + key: "VK_BACK_SPACE", + suggestions: [] +}, { + key: ".", + suggestions: [".class1", ".class2"] +}]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + let {searchBox} = inspector; + let popup = inspector.searchSuggestions.searchPopup; + + yield focusSearchBoxUsingShortcut(inspector.panelWin); + + for (let {key, suggestions} of TEST_DATA) { + info("Pressing " + key + " to get " + suggestions); + + let command = once(searchBox, "input"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + yield command; + + info("Waiting for search query to complete and getting the suggestions"); + yield inspector.searchSuggestions._lastQuery; + let actualSuggestions = popup.getItems().reverse(); + + is(popup.isOpen ? actualSuggestions.length : 0, suggestions.length, + "There are expected number of suggestions."); + + for (let i = 0; i < suggestions.length; i++) { + is(actualSuggestions[i].label, suggestions[i], + "The suggestion at " + i + "th index is correct."); + } + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-08.js b/devtools/client/inspector/test/browser_inspector_search-08.js new file mode 100644 index 000000000..f5c77fcac --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-08.js @@ -0,0 +1,64 @@ +/* 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 searching for namespaced elements does work. + +const XHTML = ` + <!DOCTYPE html> + <html xmlns="http://www.w3.org/1999/xhtml" + xmlns:svg="http://www.w3.org/2000/svg"> + <body> + <svg:svg width="100" height="100"> + <svg:clipPath> + <svg:rect x="0" y="0" width="10" height="5"></svg:rect> + </svg:clipPath> + <svg:circle cx="0" cy="0" r="5"></svg:circle> + </svg:svg> + </body> + </html> +`; + +const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML); + +// An array of (key, suggestions) pairs where key is a key to press and +// suggestions is an array of suggestions that should be shown in the popup. +const TEST_DATA = [{ + key: "c", + suggestions: ["circle", "clipPath"] +}, { + key: "VK_BACK_SPACE", + suggestions: [] +}, { + key: "s", + suggestions: ["svg"] +}]; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URI); + let {searchBox} = inspector; + let popup = inspector.searchSuggestions.searchPopup; + + yield focusSearchBoxUsingShortcut(inspector.panelWin); + + for (let {key, suggestions} of TEST_DATA) { + info("Pressing " + key + " to get " + suggestions.join(", ")); + + let command = once(searchBox, "input"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + yield command; + + info("Waiting for search query to complete and getting the suggestions"); + yield inspector.searchSuggestions._lastQuery; + let actualSuggestions = popup.getItems().reverse(); + + is(popup.isOpen ? actualSuggestions.length : 0, suggestions.length, + "There are expected number of suggestions."); + + for (let i = 0; i < suggestions.length; i++) { + is(actualSuggestions[i].label, suggestions[i], + "The suggestion at " + i + "th index is correct."); + } + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-clear.js b/devtools/client/inspector/test/browser_inspector_search-clear.js new file mode 100644 index 000000000..4388c70a6 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-clear.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"; + +// Bug 1295081 Test searchbox clear button's display behavior is correct + +const XHTML = ` + <!DOCTYPE html> + <html xmlns="http://www.w3.org/1999/xhtml" + xmlns:svg="http://www.w3.org/2000/svg"> + <body> + <svg:svg width="100" height="100"> + <svg:clipPath> + <svg:rect x="0" y="0" width="10" height="5"></svg:rect> + </svg:clipPath> + <svg:circle cx="0" cy="0" r="5"></svg:circle> + </svg:svg> + </body> + </html> +`; + +const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML); + +// Type "d" in inspector-searchbox, Enter [Back space] key and check if the +// clear button is shown correctly +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URI); + let {searchBox, searchClearButton} = inspector; + + yield focusSearchBoxUsingShortcut(inspector.panelWin); + + info("Type d and the clear button will be shown"); + + let command = once(searchBox, "input"); + EventUtils.synthesizeKey("c", {}, inspector.panelWin); + yield command; + + info("Waiting for search query to complete and getting the suggestions"); + yield inspector.searchSuggestions._lastQuery; + + ok(!searchClearButton.hidden, + "The clear button is shown when some word is in searchBox"); + + EventUtils.synthesizeKey("VK_BACK_SPACE", {}, inspector.panelWin); + yield command; + + info("Waiting for search query to complete and getting the suggestions"); + yield inspector.searchSuggestions._lastQuery; + + ok(searchClearButton.hidden, "The clear button is hidden when no word is in searchBox"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-filter_context-menu.js b/devtools/client/inspector/test/browser_inspector_search-filter_context-menu.js new file mode 100644 index 000000000..137456468 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-filter_context-menu.js @@ -0,0 +1,82 @@ +/* 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 inspector's markup view search filter context menu works properly. + +const TEST_INPUT = "h1"; +const TEST_URI = "<h1>test filter context menu</h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector} = yield openInspector(); + let {searchBox} = inspector; + yield selectNode("h1", inspector); + + let win = inspector.panelWin; + let searchContextMenu = toolbox.textBoxContextMenuPopup; + ok(searchContextMenu, + "The search filter context menu is loaded in the inspector"); + + let cmdUndo = searchContextMenu.querySelector("[command=cmd_undo]"); + let cmdDelete = searchContextMenu.querySelector("[command=cmd_delete]"); + let cmdSelectAll = searchContextMenu.querySelector("[command=cmd_selectAll]"); + let cmdCut = searchContextMenu.querySelector("[command=cmd_cut]"); + let cmdCopy = searchContextMenu.querySelector("[command=cmd_copy]"); + let cmdPaste = searchContextMenu.querySelector("[command=cmd_paste]"); + + emptyClipboard(); + + info("Opening context menu"); + let onFocus = once(searchBox, "focus"); + searchBox.focus(); + yield onFocus; + + let onContextMenuPopup = once(searchContextMenu, "popupshowing"); + EventUtils.synthesizeMouse(searchBox, 2, 2, + {type: "contextmenu", button: 2}, win); + yield onContextMenuPopup; + + is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled"); + is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled"); + is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled"); + + // Cut/Copy items are enabled in context menu even if there + // is no selection. See also Bug 1303033 + is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled"); + is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled"); + + if (isWindows()) { + // emptyClipboard only works on Windows (666254), assert paste only for this OS. + is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled"); + } + + info("Closing context menu"); + let onContextMenuHidden = once(searchContextMenu, "popuphidden"); + searchContextMenu.hidePopup(); + yield onContextMenuHidden; + + info("Copy text in search field using the context menu"); + searchBox.value = TEST_INPUT; + searchBox.select(); + searchBox.focus(); + EventUtils.synthesizeMouse(searchBox, 2, 2, + {type: "contextmenu", button: 2}, win); + yield onContextMenuPopup; + yield waitForClipboardPromise(() => cmdCopy.click(), TEST_INPUT); + searchContextMenu.hidePopup(); + yield onContextMenuHidden; + + info("Reopen context menu and check command properties"); + EventUtils.synthesizeMouse(searchBox, 2, 2, + {type: "contextmenu", button: 2}, win); + yield onContextMenuPopup; + + is(cmdUndo.getAttribute("disabled"), "", "cmdUndo is enabled"); + is(cmdDelete.getAttribute("disabled"), "", "cmdDelete is enabled"); + is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled"); + is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled"); + is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled"); + is(cmdPaste.getAttribute("disabled"), "", "cmdPaste is enabled"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-label.js b/devtools/client/inspector/test/browser_inspector_search-label.js new file mode 100644 index 000000000..669ad79b8 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-label.js @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Check that search label updated correctcly based on the search result. + +const TEST_URL = URL_ROOT + "doc_inspector_search.html"; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URL); + let { panelWin, searchResultsLabel } = inspector; + + info("Searching for test node #d1"); + // Expect the label shows 1 result + yield focusSearchBoxUsingShortcut(panelWin); + synthesizeKeys("#d1", panelWin); + EventUtils.synthesizeKey("VK_RETURN", {}, panelWin); + + yield inspector.search.once("search-result"); + is(searchResultsLabel.textContent, "1 of 1"); + + info("Click the clear button"); + // Expect the label is cleared after clicking the clear button. + + inspector.searchClearButton.click(); + is(searchResultsLabel.textContent, ""); + + // Catch-all event for remaining server requests when searching for the new + // node. + yield inspector.once("inspector-updated"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-navigation.js b/devtools/client/inspector/test/browser_inspector_search-navigation.js new file mode 100644 index 000000000..bf409fcc7 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-navigation.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"; + +// Check that searchbox value is correct when suggestions popup is navigated +// with keyboard. + +// Test data as pairs of [key to press, expected content of searchbox]. +const KEY_STATES = [ + ["d", "d"], + ["i", "di"], + ["v", "div"], + [".", "div."], + ["VK_UP", "div.c1"], + ["VK_DOWN", "div.l1"], + ["VK_BACK_SPACE", "div.l"], + ["VK_TAB", "div.l1"], + [" ", "div.l1 "], + ["VK_UP", "div.l1 div"], + ["VK_UP", "div.l1 span"], + ["VK_UP", "div.l1 div"], + [".", "div.l1 div."], + ["VK_TAB", "div.l1 div.c1"], + ["VK_BACK_SPACE", "div.l1 div.c"], + ["VK_BACK_SPACE", "div.l1 div."], + ["VK_BACK_SPACE", "div.l1 div"], + ["VK_BACK_SPACE", "div.l1 di"], + ["VK_BACK_SPACE", "div.l1 d"], + ["VK_BACK_SPACE", "div.l1 "], + ["VK_UP", "div.l1 div"], + ["VK_BACK_SPACE", "div.l1 di"], + ["VK_BACK_SPACE", "div.l1 d"], + ["VK_BACK_SPACE", "div.l1 "], + ["VK_UP", "div.l1 div"], + ["VK_UP", "div.l1 span"], + ["VK_UP", "div.l1 div"], + ["VK_TAB", "div.l1 div"], + ["VK_BACK_SPACE", "div.l1 di"], + ["VK_BACK_SPACE", "div.l1 d"], + ["VK_BACK_SPACE", "div.l1 "], + ["VK_DOWN", "div.l1 div"], + ["VK_DOWN", "div.l1 span"], + ["VK_BACK_SPACE", "div.l1 spa"], + ["VK_BACK_SPACE", "div.l1 sp"], + ["VK_BACK_SPACE", "div.l1 s"], + ["VK_BACK_SPACE", "div.l1 "], + ["VK_BACK_SPACE", "div.l1"], + ["VK_BACK_SPACE", "div.l"], + ["VK_BACK_SPACE", "div."], + ["VK_BACK_SPACE", "div"], + ["VK_BACK_SPACE", "di"], + ["VK_BACK_SPACE", "d"], + ["VK_BACK_SPACE", ""], +]; + +const TEST_URL = URL_ROOT + + "doc_inspector_search-suggestions.html"; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URL); + yield focusSearchBoxUsingShortcut(inspector.panelWin); + + for (let [key, query] of KEY_STATES) { + info("Pressing key " + key + " to get searchbox value as " + query); + + let done = inspector.searchSuggestions.once("processing-done"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + yield done; + + info("Waiting for search query to complete"); + yield inspector.searchSuggestions._lastQuery; + + is(inspector.searchBox.value, query, "The searchbox value is correct"); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-reserved.js b/devtools/client/inspector/test/browser_inspector_search-reserved.js new file mode 100644 index 000000000..e8141eb08 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-reserved.js @@ -0,0 +1,132 @@ +/* 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"; + +// Testing searching for ids and classes that contain reserved characters. +const TEST_URL = URL_ROOT + "doc_inspector_search-reserved.html"; + +// An array of (key, suggestions) pairs where key is a key to press and +// suggestions is an array of suggestions that should be shown in the popup. +// Suggestion is an object with label of the entry and optional count +// (defaults to 1) +const TEST_DATA = [ + { + key: "#", + suggestions: [{label: "#d1\\.d2"}] + }, + { + key: "d", + suggestions: [{label: "#d1\\.d2"}] + }, + { + key: "VK_BACK_SPACE", + suggestions: [{label: "#d1\\.d2"}] + }, + { + key: "VK_BACK_SPACE", + suggestions: [] + }, + { + key: ".", + suggestions: [{label: ".c1\\.c2"}] + }, + { + key: "c", + suggestions: [{label: ".c1\\.c2"}] + }, + { + key: "VK_BACK_SPACE", + suggestions: [{label: ".c1\\.c2"}] + }, + { + key: "VK_BACK_SPACE", + suggestions: [] + }, + { + key: "d", + suggestions: [{label: "div"}, + {label: "#d1\\.d2"}] + }, + { + key: "VK_BACK_SPACE", + suggestions: [] + }, + { + key: "c", + suggestions: [{label: ".c1\\.c2"}] + }, + { + key: "VK_BACK_SPACE", + suggestions: [] + }, + { + key: "b", + suggestions: [{label: "body"}] + }, + { + key: "o", + suggestions: [{label: "body"}] + }, + { + key: "d", + suggestions: [{label: "body"}] + }, + { + key: "y", + suggestions: [] + }, + { + key: " ", + suggestions: [{label: "body div"}] + }, + { + key: ".", + suggestions: [{label: "body .c1\\.c2"}] + }, + { + key: "VK_BACK_SPACE", + suggestions: [{label: "body div"}] + }, + { + key: "#", + suggestions: [{label: "body #d1\\.d2"}] + } +]; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URL); + let searchBox = inspector.searchBox; + let popup = inspector.searchSuggestions.searchPopup; + + yield focusSearchBoxUsingShortcut(inspector.panelWin); + + for (let { key, suggestions } of TEST_DATA) { + info("Pressing " + key + " to get " + formatSuggestions(suggestions)); + + let command = once(searchBox, "input"); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + yield command; + + info("Waiting for search query to complete"); + yield inspector.searchSuggestions._lastQuery; + + info("Query completed. Performing checks for input '" + + searchBox.value + "'"); + let actualSuggestions = popup.getItems().reverse(); + + is(popup.isOpen ? actualSuggestions.length : 0, suggestions.length, + "There are expected number of suggestions."); + + for (let i = 0; i < suggestions.length; i++) { + is(suggestions[i].label, actualSuggestions[i].label, + "The suggestion at " + i + "th index is correct."); + } + } +}); + +function formatSuggestions(suggestions) { + return "[" + suggestions + .map(s => "'" + s.label + "'") + .join(", ") + "]"; +} diff --git a/devtools/client/inspector/test/browser_inspector_search-selection.js b/devtools/client/inspector/test/browser_inspector_search-selection.js new file mode 100644 index 000000000..99f1e34bb --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-selection.js @@ -0,0 +1,62 @@ +/* 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"; + +// Testing navigation between nodes in search results +const {AppConstants} = require("resource://gre/modules/AppConstants.jsm"); + +const TEST_URL = URL_ROOT + "doc_inspector_search.html"; + +add_task(function* () { + let {inspector} = yield openInspectorForURL(TEST_URL); + + info("Focus the search box"); + yield focusSearchBoxUsingShortcut(inspector.panelWin); + + info("Enter body > p to search"); + let processingDone = once(inspector.searchSuggestions, "processing-done"); + EventUtils.sendString("body > p", inspector.panelWin); + yield processingDone; + + info("Wait for search query to complete"); + yield inspector.searchSuggestions._lastQuery; + + let msg = "Press enter and expect a new selection"; + yield sendKeyAndCheck(inspector, msg, "VK_RETURN", {}, "#p1"); + + msg = "Press enter to cycle through multiple nodes"; + yield sendKeyAndCheck(inspector, msg, "VK_RETURN", {}, "#p2"); + + msg = "Press shift-enter to select the previous node"; + yield sendKeyAndCheck(inspector, msg, "VK_RETURN", { shiftKey: true }, "#p1"); + + if (AppConstants.platform === "macosx") { + msg = "Press meta-g to cycle through multiple nodes"; + yield sendKeyAndCheck(inspector, msg, "VK_G", { metaKey: true }, "#p2"); + + msg = "Press shift+meta-g to select the previous node"; + yield sendKeyAndCheck(inspector, msg, "VK_G", + { metaKey: true, shiftKey: true }, "#p1"); + } else { + msg = "Press ctrl-g to cycle through multiple nodes"; + yield sendKeyAndCheck(inspector, msg, "VK_G", { ctrlKey: true }, "#p2"); + + msg = "Press shift+ctrl-g to select the previous node"; + yield sendKeyAndCheck(inspector, msg, "VK_G", + { ctrlKey: true, shiftKey: true }, "#p1"); + } +}); + +let sendKeyAndCheck = Task.async(function* (inspector, description, key, + modifiers, expectedId) { + info(description); + let onSelect = inspector.once("inspector-updated"); + EventUtils.synthesizeKey(key, modifiers, inspector.panelWin); + yield onSelect; + + let selectedNode = inspector.selection.nodeFront; + info(selectedNode.id + " is selected with text " + inspector.searchBox.value); + let targetNode = yield getNodeFront(expectedId, inspector); + is(selectedNode, targetNode, "Correct node " + expectedId + " is selected"); +}); diff --git a/devtools/client/inspector/test/browser_inspector_search-sidebar.js b/devtools/client/inspector/test/browser_inspector_search-sidebar.js new file mode 100644 index 000000000..d65a670ac --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-sidebar.js @@ -0,0 +1,74 @@ +/* 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 depending where the user last clicked in the inspector, the right search +// field is focused when ctrl+F is pressed. + +add_task(function* () { + let {inspector} = yield openInspectorForURL("data:text/html;charset=utf-8,Search!"); + + info("Check that by default, the inspector search field gets focused"); + pressCtrlF(); + isInInspectorSearchBox(inspector); + + info("Click somewhere in the rule-view"); + clickInRuleView(inspector); + + info("Check that the rule-view search field gets focused"); + pressCtrlF(); + isInRuleViewSearchBox(inspector); + + info("Click in the inspector again"); + yield clickContainer("head", inspector); + + info("Check that now we're back in the inspector, its search field gets focused"); + pressCtrlF(); + isInInspectorSearchBox(inspector); + + info("Switch to the computed view, and click somewhere inside it"); + selectComputedView(inspector); + clickInComputedView(inspector); + + info("Check that the computed-view search field gets focused"); + pressCtrlF(); + isInComputedViewSearchBox(inspector); + + info("Click in the inspector yet again"); + yield clickContainer("body", inspector); + + info("We're back in the inspector again, check the inspector search field focuses"); + pressCtrlF(); + isInInspectorSearchBox(inspector); +}); + +function pressCtrlF() { + EventUtils.synthesizeKey("f", {accelKey: true}); +} + +function clickInRuleView(inspector) { + let el = inspector.panelDoc.querySelector("#sidebar-panel-ruleview"); + EventUtils.synthesizeMouseAtCenter(el, {}, inspector.panelDoc.defaultView); +} + +function clickInComputedView(inspector) { + let el = inspector.panelDoc.querySelector("#sidebar-panel-computedview"); + EventUtils.synthesizeMouseAtCenter(el, {}, inspector.panelDoc.defaultView); +} + +function isInInspectorSearchBox(inspector) { + // Focus ends up in an anonymous child of the XUL textbox. + ok(inspector.panelDoc.activeElement.closest("#inspector-searchbox"), + "The inspector search field is focused when ctrl+F is pressed"); +} + +function isInRuleViewSearchBox(inspector) { + is(inspector.panelDoc.activeElement, inspector.ruleview.view.searchField, + "The rule-view search field is focused when ctrl+F is pressed"); +} + +function isInComputedViewSearchBox(inspector) { + is(inspector.panelDoc.activeElement, inspector.computedview.computedView.searchField, + "The computed-view search field is focused when ctrl+F is pressed"); +} diff --git a/devtools/client/inspector/test/browser_inspector_search-suggests-ids-and-classes.js b/devtools/client/inspector/test/browser_inspector_search-suggests-ids-and-classes.js new file mode 100644 index 000000000..b20c72342 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search-suggests-ids-and-classes.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the selector-search input proposes ids and classes even when . and +// # is missing, but that this only occurs when the query is one word (no +// selector combination) + +// The various states of the inspector: [key, suggestions array] +// [ +// what key to press, +// suggestions array with count [ +// [suggestion1, count1], [suggestion2] ... +// ] count can be left to represent 1 +// ] +const KEY_STATES = [ + ["s", [["span", 1], [".span", 1], ["#span", 1]]], + ["p", [["span", 1], [".span", 1], ["#span", 1]]], + ["a", [["span", 1], [".span", 1], ["#span", 1]]], + ["n", []], + [" ", [["span div", 1]]], + // mixed tag/class/id suggestions only work for the first word + ["d", [["span div", 1]]], + ["VK_BACK_SPACE", [["span div", 1]]], + ["VK_BACK_SPACE", []], + ["VK_BACK_SPACE", [["span", 1], [".span", 1], ["#span", 1]]], + ["VK_BACK_SPACE", [["span", 1], [".span", 1], ["#span", 1]]], + ["VK_BACK_SPACE", [["span", 1], [".span", 1], ["#span", 1]]], + ["VK_BACK_SPACE", []], + // Test that mixed tags, classes and ids are grouped by types, sorted by + // count and alphabetical order + ["b", [ + ["button", 3], + ["body", 1], + [".bc", 3], + [".ba", 1], + [".bb", 1], + ["#ba", 1], + ["#bb", 1], + ["#bc", 1] + ]], +]; + +const TEST_URL = `<span class="span" id="span"> + <div class="div" id="div"></div> + </span> + <button class="ba bc" id="bc"></button> + <button class="bb bc" id="bb"></button> + <button class="bc" id="ba"></button>`; + +add_task(function* () { + let {inspector} = yield openInspectorForURL("data:text/html;charset=utf-8," + + encodeURI(TEST_URL)); + + let searchBox = inspector.panelWin.document.getElementById( + "inspector-searchbox"); + let popup = inspector.searchSuggestions.searchPopup; + + yield focusSearchBoxUsingShortcut(inspector.panelWin); + + for (let [key, expectedSuggestions] of KEY_STATES) { + info("pressing key " + key + " to get suggestions " + + JSON.stringify(expectedSuggestions)); + + let onCommand = once(searchBox, "input", true); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + yield onCommand; + + info("Waiting for the suggestions to be retrieved"); + yield inspector.searchSuggestions._lastQuery; + + let actualSuggestions = popup.getItems(); + is(popup.isOpen ? actualSuggestions.length : 0, expectedSuggestions.length, + "There are expected number of suggestions"); + actualSuggestions.reverse(); + + for (let i = 0; i < expectedSuggestions.length; i++) { + is(expectedSuggestions[i][0], actualSuggestions[i].label, + "The suggestion at " + i + "th index is correct."); + is(expectedSuggestions[i][1] || 1, actualSuggestions[i].count, + "The count for suggestion at " + i + "th index is correct."); + } + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_search_keyboard_trap.js b/devtools/client/inspector/test/browser_inspector_search_keyboard_trap.js new file mode 100644 index 000000000..391d812a2 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_search_keyboard_trap.js @@ -0,0 +1,94 @@ +/* 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 ability to tab to and away from inspector search using keyboard. + +const TEST_URL = URL_ROOT + "doc_inspector_search.html"; + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * focused {Boolean} flag, indicating if search box contains focus + * keys: {Array} list of keys that include key code and optional + * event data (shiftKey, etc) + * } + * + */ +const TEST_DATA = [ + { + desc: "Move focus to a next focusable element", + focused: false, + keys: [ + { + key: "VK_TAB", + options: { } + } + ] + }, + { + desc: "Move focus back to searchbox", + focused: true, + keys: [ + { + key: "VK_TAB", + options: { shiftKey: true } + } + ] + }, + { + desc: "Open popup and then tab away (2 times) to the a next focusable " + + "element", + focused: false, + keys: [ + { + key: "d", + options: { } + }, + { + key: "VK_TAB", + options: { } + }, + { + key: "VK_TAB", + options: { } + } + ] + }, + { + desc: "Move focus back to searchbox", + focused: true, + keys: [ + { + key: "VK_TAB", + options: { shiftKey: true } + } + ] + } +]; + +add_task(function* () { + let { inspector } = yield openInspectorForURL(TEST_URL); + let { searchBox } = inspector; + let doc = inspector.panelDoc; + + yield selectNode("#b1", inspector); + yield focusSearchBoxUsingShortcut(inspector.panelWin); + + // Ensure a searchbox is focused. + ok(containsFocus(doc, searchBox), "Focus is in a searchbox"); + + for (let { desc, focused, keys } of TEST_DATA) { + info(desc); + for (let { key, options } of keys) { + let done = !focused ? + inspector.searchSuggestions.once("processing-done") : Promise.resolve(); + EventUtils.synthesizeKey(key, options); + yield done; + } + is(containsFocus(doc, searchBox), focused, "Focus is set correctly"); + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_select-docshell.js b/devtools/client/inspector/test/browser_inspector_select-docshell.js new file mode 100644 index 000000000..6a801fdea --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_select-docshell.js @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test frame selection switching at toolbox level +// when using the inspector + +const FrameURL = "data:text/html;charset=UTF-8," + + encodeURI("<div id=\"frame\">frame</div>"); +const URL = "data:text/html;charset=UTF-8," + + encodeURI('<iframe src="' + FrameURL + + '"></iframe><div id="top">top</div>'); + +add_task(function* () { + Services.prefs.setBoolPref("devtools.command-button-frames.enabled", true); + + let {inspector, toolbox, testActor} = yield openInspectorForURL(URL); + + // Verify we are on the top level document + ok((yield testActor.hasNode("#top")), + "We have the test node on the top level document"); + + assertMarkupViewIsLoaded(inspector); + + // Verify that the frame map button is empty at the moment. + let btn = toolbox.doc.getElementById("command-button-frames"); + ok(!btn.firstChild, "The frame list button doesn't have any children"); + + // Open frame menu and wait till it's available on the screen. + let menu = toolbox.showFramesMenu({target: btn}); + yield once(menu, "open"); + + // Verify that the menu is popuplated. + let frames = menu.items.slice(); + is(frames.length, 2, "We have both frames in the menu"); + + frames.sort(function (a, b) { + return a.label.localeCompare(b.label); + }); + + is(frames[0].label, FrameURL, "Got top level document in the list"); + is(frames[1].label, URL, "Got iframe document in the list"); + + // Listen to will-navigate to check if the view is empty + let willNavigate = toolbox.target.once("will-navigate").then(() => { + info("Navigation to the iframe has started, the inspector should be empty"); + assertMarkupViewIsEmpty(inspector); + }); + + // Only select the iframe after we are able to select an element from the top + // level document. + let newRoot = inspector.once("new-root"); + yield selectNode("#top", inspector); + info("Select the iframe"); + frames[0].click(); + + yield willNavigate; + yield newRoot; + + info("Navigation to the iframe is done, the inspector should be back up"); + + // Verify we are on page one + ok(!(yield testActor.hasNode("iframe")), + "We not longer have access to the top frame elements"); + ok((yield testActor.hasNode("#frame")), + "But now have direct access to the iframe elements"); + + // On page 2 load, verify we have the right content + assertMarkupViewIsLoaded(inspector); + + yield selectNode("#frame", inspector); + + Services.prefs.clearUserPref("devtools.command-button-frames.enabled"); +}); + +function assertMarkupViewIsLoaded(inspector) { + let markupViewBox = inspector.panelDoc.getElementById("markup-box"); + is(markupViewBox.childNodes.length, 1, "The markup-view is loaded"); +} + +function assertMarkupViewIsEmpty(inspector) { + let markupViewBox = inspector.panelDoc.getElementById("markup-box"); + is(markupViewBox.childNodes.length, 0, "The markup-view is unloaded"); +} diff --git a/devtools/client/inspector/test/browser_inspector_select-last-selected.js b/devtools/client/inspector/test/browser_inspector_select-last-selected.js new file mode 100644 index 000000000..0f2050327 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_select-last-selected.js @@ -0,0 +1,95 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +requestLongerTimeout(2); + +// Checks that the expected default node is selected after a page navigation or +// a reload. +var PAGE_1 = URL_ROOT + "doc_inspector_select-last-selected-01.html"; +var PAGE_2 = URL_ROOT + "doc_inspector_select-last-selected-02.html"; + +// An array of test cases with following properties: +// - url: URL to navigate to. If URL == content.location, reload instead. +// - nodeToSelect: a selector for a node to select before navigation. If null, +// whatever is selected stays selected. +// - selectedNode: a selector for a node that is selected after navigation. +var TEST_DATA = [ + { + url: PAGE_1, + nodeToSelect: "#id1", + selectedNode: "#id1" + }, + { + url: PAGE_1, + nodeToSelect: "#id2", + selectedNode: "#id2" + }, + { + url: PAGE_1, + nodeToSelect: "#id3", + selectedNode: "#id3" + }, + { + url: PAGE_1, + nodeToSelect: "#id4", + selectedNode: "#id4" + }, + { + url: PAGE_2, + nodeToSelect: null, + selectedNode: "body" + }, + { + url: PAGE_1, + nodeToSelect: "#id5", + selectedNode: "body" + }, + { + url: PAGE_2, + nodeToSelect: null, + selectedNode: "body" + } +]; + +add_task(function* () { + let { inspector, toolbox, testActor } = yield openInspectorForURL(PAGE_1); + + for (let { url, nodeToSelect, selectedNode } of TEST_DATA) { + if (nodeToSelect) { + info("Selecting node " + nodeToSelect + " before navigation."); + yield selectNode(nodeToSelect, inspector); + } + + yield navigateToAndWaitForNewRoot(url); + + let nodeFront = yield getNodeFront(selectedNode, inspector); + ok(nodeFront, "Got expected node front"); + is(inspector.selection.nodeFront, nodeFront, + selectedNode + " is selected after navigation."); + } + + function* navigateToAndWaitForNewRoot(url) { + info("Navigating and waiting for new-root event after navigation."); + + let current = yield testActor.eval("location.href"); + if (url == current) { + info("Reloading page."); + let markuploaded = inspector.once("markuploaded"); + let onNewRoot = inspector.once("new-root"); + let onUpdated = inspector.once("inspector-updated"); + + let activeTab = toolbox.target.activeTab; + yield activeTab.reload(); + info("Waiting for inspector to be ready."); + yield markuploaded; + yield onNewRoot; + yield onUpdated; + } else { + yield navigateTo(inspector, url); + } + } +}); diff --git a/devtools/client/inspector/test/browser_inspector_sidebarstate.js b/devtools/client/inspector/test/browser_inspector_sidebarstate.js new file mode 100644 index 000000000..a2bb764c1 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_sidebarstate.js @@ -0,0 +1,38 @@ +/* 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"; + +const TEST_URI = "data:text/html;charset=UTF-8," + + "<h1>browser_inspector_sidebarstate.js</h1>"; + +add_task(function* () { + let { inspector, toolbox } = yield openInspectorForURL(TEST_URI); + + info("Selecting ruleview."); + inspector.sidebar.select("ruleview"); + + is(inspector.sidebar.getCurrentTabID(), "ruleview", + "Rule View is selected by default"); + + info("Selecting computed view."); + inspector.sidebar.select("computedview"); + + // Finish initialization of the computed panel before + // destroying the toolbox. + yield waitForTick(); + + info("Closing inspector."); + yield toolbox.destroy(); + + info("Re-opening inspector."); + inspector = (yield openInspector()).inspector; + + if (!inspector.sidebar.getCurrentTabID()) { + info("Default sidebar still to be selected, adding select listener."); + yield inspector.sidebar.once("select"); + } + + is(inspector.sidebar.getCurrentTabID(), "computedview", + "Computed view is selected by default."); +}); diff --git a/devtools/client/inspector/test/browser_inspector_switch-to-inspector-on-pick.js b/devtools/client/inspector/test/browser_inspector_switch-to-inspector-on-pick.js new file mode 100644 index 000000000..53b2892ac --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_switch-to-inspector-on-pick.js @@ -0,0 +1,39 @@ +/* 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"; + +// Testing that clicking the pick button switches the toolbox to the inspector +// panel. + +const TEST_URI = "data:text/html;charset=UTF-8," + + "<p>Switch to inspector on pick</p>"; + +add_task(function* () { + let tab = yield addTab(TEST_URI); + let toolbox = yield openToolbox(tab); + + yield startPickerAndAssertSwitchToInspector(toolbox); + + info("Stoppping element picker."); + yield toolbox.highlighterUtils.stopPicker(); +}); + +function openToolbox(tab) { + info("Opening webconsole."); + let target = TargetFactory.forTab(tab); + return gDevTools.showToolbox(target, "webconsole"); +} + +function* startPickerAndAssertSwitchToInspector(toolbox) { + info("Clicking element picker button."); + let pickButton = toolbox.doc.querySelector("#command-button-pick"); + pickButton.click(); + + info("Waiting for inspector to be selected."); + yield toolbox.once("inspector-selected"); + is(toolbox.currentToolId, "inspector", "Switched to the inspector"); + + info("Waiting for inspector to update."); + yield toolbox.getCurrentPanel().once("inspector-updated"); +} diff --git a/devtools/client/inspector/test/browser_inspector_textbox-menu.js b/devtools/client/inspector/test/browser_inspector_textbox-menu.js new file mode 100644 index 000000000..74190229f --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_textbox-menu.js @@ -0,0 +1,90 @@ +/* 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 when right-clicking on various text boxes throughout the inspector does use +// the toolbox's context menu (copy/cut/paste/selectAll/Undo). + +add_task(function* () { + yield addTab(`data:text/html;charset=utf-8, + <style>h1 { color: red; }</style> + <h1 id="title">textbox context menu test</h1>`); + let {toolbox, inspector} = yield openInspector(); + yield selectNode("h1", inspector); + + info("Testing the markup-view tagname"); + let container = yield focusNode("h1", inspector); + let tag = container.editor.tag; + tag.focus(); + EventUtils.sendKey("return", inspector.panelWin); + yield checkTextBox(inspector.markup.doc.activeElement, toolbox); + + info("Testing the markup-view attribute"); + EventUtils.sendKey("tab", inspector.panelWin); + yield checkTextBox(inspector.markup.doc.activeElement, toolbox); + + info("Testing the markup-view new attribute"); + // It takes 2 tabs to focus the newAttr field, the first one just moves the cursor to + // the end of the field. + EventUtils.sendKey("tab", inspector.panelWin); + EventUtils.sendKey("tab", inspector.panelWin); + yield checkTextBox(inspector.markup.doc.activeElement, toolbox); + + info("Testing the markup-view textcontent"); + EventUtils.sendKey("tab", inspector.panelWin); + yield checkTextBox(inspector.markup.doc.activeElement, toolbox); + // Blur this last markup-view field, since we're moving on to the rule-view next. + EventUtils.sendKey("escape", inspector.panelWin); + + info("Testing the rule-view selector"); + let ruleView = inspector.ruleview.view; + let cssRuleEditor = getRuleViewRuleEditor(ruleView, 1); + EventUtils.synthesizeMouse(cssRuleEditor.selectorText, 0, 0, {}, inspector.panelWin); + yield checkTextBox(inspector.panelDoc.activeElement, toolbox); + + info("Testing the rule-view property name"); + EventUtils.sendKey("tab", inspector.panelWin); + yield checkTextBox(inspector.panelDoc.activeElement, toolbox); + + info("Testing the rule-view property value"); + EventUtils.sendKey("tab", inspector.panelWin); + yield checkTextBox(inspector.panelDoc.activeElement, toolbox); + + info("Testing the rule-view new property"); + // Tabbing out of the value field triggers a ruleview-changed event that we need to wait + // for. + let onRuleViewChanged = once(ruleView, "ruleview-changed"); + EventUtils.sendKey("tab", inspector.panelWin); + yield onRuleViewChanged; + yield checkTextBox(inspector.panelDoc.activeElement, toolbox); + + info("Switching to the computed-view"); + let onComputedViewReady = inspector.once("boxmodel-view-updated"); + selectComputedView(inspector); + yield onComputedViewReady; + + info("Testing the box-model region"); + let margin = inspector.panelDoc.querySelector(".boxmodel-margin.boxmodel-top > span"); + EventUtils.synthesizeMouseAtCenter(margin, {}, inspector.panelWin); + yield checkTextBox(inspector.panelDoc.activeElement, toolbox); +}); + +function* checkTextBox(textBox, {textBoxContextMenuPopup}) { + is(textBoxContextMenuPopup.state, "closed", "The menu is closed"); + + info("Simulating context click on the textbox and expecting the menu to open"); + let onContextMenu = once(textBoxContextMenuPopup, "popupshown"); + EventUtils.synthesizeMouse(textBox, 2, 2, {type: "contextmenu", button: 2}, + textBox.ownerDocument.defaultView); + yield onContextMenu; + + is(textBoxContextMenuPopup.state, "open", "The menu is now visible"); + + info("Closing the menu"); + let onContextMenuHidden = once(textBoxContextMenuPopup, "popuphidden"); + textBoxContextMenuPopup.hidePopup(); + yield onContextMenuHidden; + + is(textBoxContextMenuPopup.state, "closed", "The menu is closed again"); +} diff --git a/devtools/client/inspector/test/doc_inspector_add_node.html b/devtools/client/inspector/test/doc_inspector_add_node.html new file mode 100644 index 000000000..d024b2a99 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_add_node.html @@ -0,0 +1,22 @@ +<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Add elements tests</title>
+ <style>
+ body::before {
+ content: "pseudo-element";
+ }
+ </style>
+</head>
+<body>
+ <div id="foo"></div>
+ <svg>
+ <rect x="0" y="0" width="100" height="50"></rect>
+ </svg>
+ <div id="bar">
+ <div id="baz"></div>
+ </div>
+ <iframe src="data:text/html;charset=utf-8,Test iframe content"></iframe>
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_breadcrumbs.html b/devtools/client/inspector/test/doc_inspector_breadcrumbs.html new file mode 100644 index 000000000..fee063611 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_breadcrumbs.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<html> + <head> + <style> + div { + min-height: 10px; min-width: 10px; + border: 1px solid red; + margin: 10px; + } + #pseudo-container::before { + content: 'before'; + } + #pseudo-container::after { + content: 'after'; + } + </style> + </head> + <body> + <article id="i1"> + <div id="i11"> + <div id="i111"> + <div id="i1111"> + </div> + </div> + </div> + </article> + <article id="i2"> + <div id="i21"> + <div id="i211"> + <div id="i2111"> + </div> + </div> + </div> + <div id="i22"> + <div id="i221"> + </div> + <div id="i222"> + <div id="i2221"> + <div id="i22211"> + </div> + </div> + </div> + </div> + </article> + <article id="i3"> + <link id="i31" /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + <link /> + </article> + <div id='pseudo-container'></div> + <!-- This is a comment node --> + <svg id="vector" viewBox="0 0 10 10"> + <clipPath id="clip"> + <rect id="rectangle" x="0" y="0" width="10" height="5"></rect> + </clipPath> + <circle cx="5" cy="5" r="5" fill="blue" clip-path="url(#clip)"></circle> + </svg> + </body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_breadcrumbs_visibility.html b/devtools/client/inspector/test/doc_inspector_breadcrumbs_visibility.html new file mode 100644 index 000000000..862f32407 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_breadcrumbs_visibility.html @@ -0,0 +1,22 @@ +<html> + <head> + <meta http-equiv="content-type" content="text/html; charset=windows-1252"> + </head> + <body> + <div id="aVeryLongIdToExceedTheBreadcrumbTruncationLimit"> + <div id="anotherVeryLongIdToExceedTheBreadcrumbTruncationLimit"> + <div id="aThirdVeryLongIdToExceedTheTruncationLimit"> + <div id="aFourthOneToExceedTheTruncationLimit"> + <div id="aFifthOneToExceedTheTruncationLimit"> + <div id="aSixthOneToExceedTheTruncationLimit"> + <div id="aSeventhOneToExceedTheTruncationLimit"> + A text node at the end + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_csp.html b/devtools/client/inspector/test/doc_inspector_csp.html new file mode 100644 index 000000000..49af7e53b --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_csp.html @@ -0,0 +1,10 @@ +<!DOCTYPE html>
+<html>
+ <head>
+ <title>Inspector CSP Test</title>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ This HTTP response has CSP headers.
+ </body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_csp.html^headers^ b/devtools/client/inspector/test/doc_inspector_csp.html^headers^ new file mode 100644 index 000000000..3345a82b8 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_csp.html^headers^ @@ -0,0 +1,2 @@ +Content-Type: text/html; charset=UTF-8
+content-security-policy: default-src 'self'; connect-src 'self'; script-src 'self'; style-src 'self';
diff --git a/devtools/client/inspector/test/doc_inspector_delete-selected-node-01.html b/devtools/client/inspector/test/doc_inspector_delete-selected-node-01.html new file mode 100644 index 000000000..70edbd936 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_delete-selected-node-01.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> + +<h1>mop</h1> +<iframe src="data:text/html;charset=utf-8,<!DOCTYPE HTML>%0D%0A<h1>kill me<span>.</span><%2Fh1>"></iframe> diff --git a/devtools/client/inspector/test/doc_inspector_delete-selected-node-02.html b/devtools/client/inspector/test/doc_inspector_delete-selected-node-02.html new file mode 100644 index 000000000..0749b064a --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_delete-selected-node-02.html @@ -0,0 +1,20 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>node delete - reset selection - test</title> +</head> +<body> + <ul id="deleteChildren"> + <li id="deleteManually">Delete me via the inspector</li> + <li id="selectedAfterDelete">This node is selected after manual delete</li> + <li id="deleteAutomatically">Delete me via javascript</li> + </ul> + <iframe id="deleteIframe" src="data:text/html,%3C!DOCTYPE%20html%3E%3Chtml%20lang%3D%22en%22%3E%3Cbody%3E%3Cp%20id%3D%22deleteInIframe%22%3EDelete my container iframe%3C%2Fp%3E%3C%2Fbody%3E%3C%2Fhtml%3E"></iframe> + <div id="deleteToMakeSingleTextNode"> + 1 + <b id="deleteWithNonElement">Delete me and select the non-element node</b> + 2 + </div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_embed.html b/devtools/client/inspector/test/doc_inspector_embed.html new file mode 100644 index 000000000..1d286ade0 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_embed.html @@ -0,0 +1,6 @@ +<!doctype html><html><head><meta charset="UTF-8"></head><body>
+<object>
+ <embed src="doc_inspector_menu.html" type="application/html"
+ width="422" height="258"></embed>
+</object>
+</body></html>
diff --git a/devtools/client/inspector/test/doc_inspector_gcli-inspect-command.html b/devtools/client/inspector/test/doc_inspector_gcli-inspect-command.html new file mode 100644 index 000000000..a7d28828c --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_gcli-inspect-command.html @@ -0,0 +1,25 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>GCLI inspect command test</title> +</head> +<body> + + <!-- This is a list of 0 h1 elements --> + + <!-- This is a list of 1 div elements --> + <div>Hello, I'm a div</div> + + <!-- This is a list of 2 span elements --> + <span>Hello, I'm a span</span> + <span>And me</span> + + <!-- This is a collection of various things that match only once --> + <p class="someclass">.someclass</p> + <p id="someid">#someid</p> + <button disabled>button[disabled]</button> + <p><strong>p>strong</strong></p> + +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlight_after_transition.html b/devtools/client/inspector/test/doc_inspector_highlight_after_transition.html new file mode 100644 index 000000000..b2ba0b066 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlight_after_transition.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + + <style> + div { + opacity: 0; + height: 0; + background: red; + border-top: 1px solid #888; + transition-property: height, opacity; + transition-duration: 3000ms; + transition-timing-function: ease-in-out, ease-in-out, linear; + } + + div[visible] { + opacity: 1; + height: 200px; + } + </style> +</head> +<body> + <div></div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter-comments.html b/devtools/client/inspector/test/doc_inspector_highlighter-comments.html new file mode 100644 index 000000000..3dedc9f36 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter-comments.html @@ -0,0 +1,19 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Inspector Highlighter Test</title> +</head> +<body> + <p></p> + <div id="id1">Visible div 1</div> + <!-- Invisible comment node --> + <div id="id2">Visible div 2</div> + <script type="text/javascript"> + /* Invisible script node */ + </script> + <div id="id3">Visible div 3</div> + <div id="id4" style="display:none;">Invisible div node</div> + Visible text node +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter-geometry_01.html b/devtools/client/inspector/test/doc_inspector_highlighter-geometry_01.html new file mode 100644 index 000000000..f05f15deb --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter-geometry_01.html @@ -0,0 +1,90 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>geometry highlighter test page</title> + <style type="text/css"> + html, body { + margin: 0; + padding: 0; + } + + .relative-sized-parent { + position: relative; + border: 2px solid black; + border-radius: 25px; + } + .size { + width: 300px; + height: 300px; + } + + .positioned-child { + position: absolute; + background: #f06; + } + .pos-top-left { + top: 30px; + left: 25%; + } + .pos-bottom-right { + bottom: 10em; + right: -10px; + } + + .inline-positioned { + background: yellow; + } + + #absolute-container { + position: absolute; + top: 50px; + left: 400px; + width: 500px; + height: 400px; + border: 1px solid black; + } + + .absolute-all-4 { + position: absolute; + top: 10px; + left: 10px; + bottom: 200px; + right: 300px; + border: 1px solid red; + } + + .relative { + position: relative; + top: 10%; + left: 50%; + height: 10px; + border: 1px solid blue; + } + + .fixed { + position: fixed; + top: 400px; + left: 0; + width: 50px; + height: 50px; + border-radius: 50%; + background: green; + } + </style> +</head> +<body> + <div id="node1" class="relative-sized-parent size"> + <div id="node2" class="positioned-child pos-top-left pos-bottom-right"> + <div id="node3" class="inline-positioned positioned-child pos-top-left" style="width:50px;height:50px;"></div> + </div> + </div> + + <div id="absolute-container"> + <div class="absolute-all-4"></div> + <div class="relative"></div> + </div> + + <div class="fixed"></div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter-geometry_02.html b/devtools/client/inspector/test/doc_inspector_highlighter-geometry_02.html new file mode 100644 index 000000000..4392c9042 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter-geometry_02.html @@ -0,0 +1,120 @@ +<!doctype html><html><head><meta charset="UTF-8"></head><body class="header"> + +<style> +.fixed { position: fixed; top: 40px; right: 20px; margin-top: 20px; background: #ccf; } +.fixed-bottom-right { position: fixed; bottom: 4em; right: 25%; margin: 20px; background: #ccf; } + +#absolute-container { position: relative; height: 150px; margin: 20px; } +.absolute { position: absolute; top: 20px; left: 400px; background: #fcc; } +.absolute-bottom-right { position: absolute; bottom: 20px; right: 50px; background: #fcc; } +.absolute-all-4 { position: absolute; top: 100px; bottom: 10px; left: 20px; right: 700px; background: #fcc; } +.absolute-negative { position: absolute; bottom: -25px; background: #fcc; } +.absolute-width-margin { position: absolute; top: 20px; right: 20px; width: 450px; margin: .3em; padding: 10px; border: 2px solid red; box-sizing: border-box; background: #fcc; } + +.relative { position: relative; top: 10px; left: 10px; background: #cfc;} +.relative-inline { position: relative; top: 10px; left: 10px; display: inline; background: #cfc;} + +.static { position: static; top: 10px; left: 10px; background: #fcf; } +.static-size { position: static; top: 10px; left: 10px; width: 300px; height: 100px; background: #fcf; } + +#sticky-container { + margin: 50px; + height: 400px; + width: 400px; + padding: 40px; + overflow: scroll; +} +#sticky-container dl { + margin: 0; + padding: 24px 0 0 0; +} + +#sticky-container dt { + background: #ffc; + border-bottom: 1px solid #989EA4; + border-top: 1px solid #717D85; + color: #FFF; + font: bold 18px/21px Helvetica, Arial, sans-serif; + margin: 0; + padding: 2px 0 0 12px; + position: sticky; + width: 99%; + top: 0px; +} + +#sticky-container dd { + font: bold 20px/45px Helvetica, Arial, sans-serif; + margin: 0; + padding: 0 0 0 12px; + white-space: nowrap; +} + +#sticky-container dd + dd { + border-top: 1px solid #CCC +} +</style> + +<h1>Positioning playground</h1> +<p>A demo of various positioning schemes: <a href="http://dev.w3.org/csswg/css-position/#pos-sch">http://dev.w3.org/csswg/css-position/#pos-sch</a>.</p> +<p>absolute, static, fixed, relative, sticky</p> + +<h2>Absolute positioning</h2> +<div class="absolute"> + Absolute child with no relative parent +</div> +<div id="absolute-container"> + <div class="absolute"> + Absolute child with a relative parent + </div> + <div class="absolute-bottom-right"> + Absolute child with a relative parent, positioned from the bottom right + </div> + <div class="absolute-all-4"> + Absolute child with a relative parent, with all 4 positions + </div> + <div class="absolute-negative"> + Absolute child with a relative parent, with negative positions + </div> + <div class="absolute-width-margin"> + Absolute child with a relative parent, size, margin + </div> +</div> + +<h2>Relative positioning</h2> +<div id="relative-container"> + <div class="relative"> + Relative child + </div> + <div style="width: 100px;"> + <div class="relative-inline"> + Relative inline child, across multiple lines + </div> + </div> + <div style="position:relative;"> + <div class="relative"> + Relative child, in a positioned parent + </div> + </div> +</div> + +<h2>Fixed positioning</h2> +<div id="fixed-container"> + <div class="fixed"> + Fixed child + </div> + <div class="fixed-bottom-right"> + Fixed child, bottom right + </div> +</div> + +<h2>Static positioning</h2> +<div id="static-container"> + <div class="static"> + Static child with no width/height + </div> + <div class="static-size"> + Static child with width/height + </div> +</div> + +</body></html>
\ No newline at end of file diff --git a/devtools/client/inspector/test/doc_inspector_highlighter.html b/devtools/client/inspector/test/doc_inspector_highlighter.html new file mode 100644 index 000000000..376a9c714 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + div { + position:absolute; + } + + #simple-div { + padding: 5px; + border: 7px solid red; + margin: 9px; + top: 30px; + left: 150px; + } + + #rotated-div { + padding: 5px; + border: 7px solid red; + margin: 9px; + transform: rotate(45deg); + top: 30px; + left: 80px; + } + + #widthHeightZero-div { + top: 30px; + left: 10px; + width: 0; + height: 0; + } + </style> + </head> + <body> + <div id="simple-div">Gort! Klaatu barada nikto!</div> + <div id="rotated-div"></div> + <div id="widthHeightZero-div">Width & height = 0</div> + </body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_csstransform.html b/devtools/client/inspector/test/doc_inspector_highlighter_csstransform.html new file mode 100644 index 000000000..cfa2761d7 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter_csstransform.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>css transform highlighter test</title> + <style type="text/css"> + #test-node { + position: absolute; + top: 0; + left: 0; + + width: 300px; + height: 300px; + + transform: rotate(90deg) skew(13deg) scale(.8) translateX(50px); + transform-origin: 50%; + + background: linear-gradient(green, yellow); + } + </style> +</head> +<body> + <div id="test-node"></div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_dom.html b/devtools/client/inspector/test/doc_inspector_highlighter_dom.html new file mode 100644 index 000000000..fab0c8803 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter_dom.html @@ -0,0 +1,20 @@ +<!DOCTYPE html>
+<html>
+<body>
+
+<p>Hello World!</p>
+
+<div id="complex-div">
+ <div id="simple-div1">
+ <p id="useless-para">The DOM is very useful! <em>#useless-para</em></p>
+ <p id="useful-para">This example is <b id="bold">really</b> useful. <em>#useful-para</em></p>
+ </div>
+
+ <div id="simple-div2">
+ <p id="another">This is another node. You won't reach this in my test.</p>
+ <p id="ahoy">Ahoy! How you doin' Capn'? <em>#ahoy</em></p>
+ </div>
+</div>
+
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_inline.html b/devtools/client/inspector/test/doc_inspector_highlighter_inline.html new file mode 100644 index 000000000..e1aa5bb1f --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter_inline.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + html { + height: 100%; + background: #eee; + } + body { + margin: 0 auto; + padding: 1em; + box-sizing: border-box; + width: 500px; + height: 100%; + background: white; + font-family: Arial; + font-size: 15px; + line-height: 40px; + } + p span { + padding: 5px 0; + margin: 0 5px; + border: 5px solid #eee; + } + </style> + </head> + <body> + <h1>Lorem Ipsum</h1> + <h2>Lorem ipsum <em>dolor sit amet</em></h2> + <p><span>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed varius, nisl eget semper maximus, dui tellus tempor leo, at pharetra eros tortor sed odio. Nullam sagittis ex nec mi sagittis pulvinar. Pellentesque dapibus feugiat fermentum. Curabitur lacinia quis enim et tristique. Aliquam in semper massa. In ac vulputate nunc, at rutrum neque. Fusce condimentum, tellus quis placerat imperdiet, dolor tortor mattis erat, nec luctus magna diam pharetra mauris.</span></p> + <div dir="rtl"> + <span><span></span>some ltr text in an rtl container</span> + </div> + </body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_rect.html b/devtools/client/inspector/test/doc_inspector_highlighter_rect.html new file mode 100644 index 000000000..4d23d52fd --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter_rect.html @@ -0,0 +1,22 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>rect highlighter parent test page</title> + <style type="text/css"> + body { + margin: 50px; + border: 10px solid red; + } + + iframe { + border: 10px solid yellow; + padding: 0; + margin: 50px; + } + </style> +</head> +<body> + <iframe src="doc_inspector_highlighter_rect_iframe.html"></iframe> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_rect_iframe.html b/devtools/client/inspector/test/doc_inspector_highlighter_rect_iframe.html new file mode 100644 index 000000000..d59050f69 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter_rect_iframe.html @@ -0,0 +1,15 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>rect highlighter child test page</title> + <style type="text/css"> + body { + margin: 0; + } + </style> +</head> +<body> + +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_highlighter_xbl.xul b/devtools/client/inspector/test/doc_inspector_highlighter_xbl.xul new file mode 100644 index 000000000..8cbf990ea --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_highlighter_xbl.xul @@ -0,0 +1,9 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Test that the picker works correctly with XBL anonymous nodes" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<scale id="scale" style="background:red"/> + +</window> diff --git a/devtools/client/inspector/test/doc_inspector_infobar.html b/devtools/client/inspector/test/doc_inspector_infobar.html new file mode 100644 index 000000000..137b3487f --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_infobar.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + + <style> + body { + width: 100%; + height: 100%; + } + + div { + position: absolute; + height: 100px; + width: 500px; + } + + #bottom { + bottom: 0px; + } + + #vertical { + height: 100%; + } + + #farbottom { + top: 2000px; + background: red; + } + + #abovetop { + top: -123px; + }"; + </style> +</head> +<body> + <div id="abovetop"></div> + <div id="vertical"></div> + <div id="top" class="class1 class2"></div> + <div id="bottom"></div> + <div id="farbottom"></div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_infobar_01.html b/devtools/client/inspector/test/doc_inspector_infobar_01.html new file mode 100644 index 000000000..a0c42ee38 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_infobar_01.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + + <style> + body { + width: 100%; + height: 100%; + } + div { + position: absolute; + height: 100px; + width: 500px; + } + + #bottom { + bottom: 0px; + background: blue; + } + + #vertical { + height: 100%; + background: green; + } + + svg { + width: 10px; + height: 10px; + } + </style> + </head> + <body> + <div id="vertical">Vertical</div> + <div id="top" class="class1 class2">Top</div> + <div id="bottom">Bottom</div> + <svg viewBox="0 0 10 10"> + <clipPath id="clip"> + <rect x="0" y="0" width="10" height="5"></rect> + </clipPath> + <circle cx="5" cy="5" r="5" fill="blue" clip-path="url(#clip)"></circle> + </svg> + </body> + </html> diff --git a/devtools/client/inspector/test/doc_inspector_infobar_02.html b/devtools/client/inspector/test/doc_inspector_infobar_02.html new file mode 100644 index 000000000..ed1843f8d --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_infobar_02.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + + <style> + body { + width: 100%; + height: 100%; + } + + div { + position: absolute; + height: 100px; + width: 500px; + } + + #below-bottom { + bottom: -200px; + background: red; + } + + #above-top { + top: -200px; + background: black; + color: white; + }"; + </style> +</head> +<body> + <div id="above-top">Above top</div> + <div id="below-bottom">Far bottom</div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_infobar_03.html b/devtools/client/inspector/test/doc_inspector_infobar_03.html new file mode 100644 index 000000000..a9aa05fa0 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_infobar_03.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + + <style> + body { + height: 300vh; + } + </style> + </head> + <body> + </body> + </html> diff --git a/devtools/client/inspector/test/doc_inspector_infobar_textnode.html b/devtools/client/inspector/test/doc_inspector_infobar_textnode.html new file mode 100644 index 000000000..2370708f4 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_infobar_textnode.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> +</head> +<body> + <div id="textnode-container"> + text + <span>content</span> + <span>content</span> + text + </div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_long-divs.html b/devtools/client/inspector/test/doc_inspector_long-divs.html new file mode 100644 index 000000000..52d6343aa --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_long-divs.html @@ -0,0 +1,104 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Inspector Long Div Listing</title> + <style> + div { + background-color: #0002; + padding-left: 1em; + } + </style> +</head> +<body> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div id="focus-here">focus here</div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div> + <div> + <div> + <div> + <div id="zoom-here">zoom-here</div> + </div> + </div> + </div> + </div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_menu.html b/devtools/client/inspector/test/doc_inspector_menu.html new file mode 100644 index 000000000..862a34579 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_menu.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html> + <head> + <title>Inspector Tree Menu Test</title> + <meta charset="utf-8"> + </head> + <body> + <div> + <div id="paste-area"> + <h1>Inspector Tree Menu Test</h1> + <p class="inner">Unset</p> + <p class="adjacent"> + <span class="ref">3</span> + </p> + </div> + <p data-id="copy">Paragraph for testing copy</p> + <p id="sensitivity">Paragraph for sensitivity</p> + <p class="duplicate">This will be duplicated</p> + <p id="delete">This has to be deleted</p> + <img id="copyimage" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==" /> + <div id="hiddenElement" style="display: none;"> + <p id="nestedHiddenElement">Visible element nested inside a non-visible element</p> + </div> + <p id="console-var">Paragraph for testing console variables</p> + <p id="console-var-multi">Paragraph for testing multiple console variables</p> + <p id="attributes" data-edit="original" data-remove="thing">Attributes are going to be changed here</p> + </div> + </body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_outerhtml.html b/devtools/client/inspector/test/doc_inspector_outerhtml.html new file mode 100644 index 000000000..cc400674d --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_outerhtml.html @@ -0,0 +1,11 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Inspector Copy OuterHTML Test</title> +</head> +<body> + <!-- Comment --> + <div><p>Test copy OuterHTML</p></div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_remove-iframe-during-load.html b/devtools/client/inspector/test/doc_inspector_remove-iframe-during-load.html new file mode 100644 index 000000000..25454e122 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_remove-iframe-during-load.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>iframe creation/deletion test</title> +</head> +<body> + <div id="yay"></div> + <script type="text/javascript"> + "use strict"; + + var yay = document.querySelector("#yay"); + yay.textContent = "nothing"; + + // Create a custom event to let the test know when the window has finished + // loading. + var event = new Event("test-page-processing-done"); + + // Create/remove an iframe before load. + var iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + iframe.remove(); + yay.textContent = "before events"; + + // Create/remove an iframe on DOMContentLoaded. + document.addEventListener("DOMContentLoaded", function () { + let newIframe = document.createElement("iframe"); + document.body.appendChild(newIframe); + newIframe.remove(); + yay.textContent = "DOMContentLoaded"; + }); + + // Create/remove an iframe on window load. + window.addEventListener("load", function () { + let newIframe = document.createElement("iframe"); + document.body.appendChild(newIframe); + newIframe.remove(); + yay.textContent = "load"; + + // Dispatch the done event. + window.dispatchEvent(event); + }); + </script> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_search-reserved.html b/devtools/client/inspector/test/doc_inspector_search-reserved.html new file mode 100644 index 000000000..15cf8c3af --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_search-reserved.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Inspector Search Box Reserved Character Test</title> +</head> +<body> + <div id="d1.d2">Hi, I'm an id that contains a CSS reserved character</div> + <div class="c1.c2">Hi, a class that contains a CSS reserved character</div> +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_search-suggestions.html b/devtools/client/inspector/test/doc_inspector_search-suggestions.html new file mode 100644 index 000000000..a84a2e3d4 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_search-suggestions.html @@ -0,0 +1,27 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Inspector Search Box Test</title> +</head> +<body> + <div id="d1"> + <div class="l1"> + <div id="d2" class="c1">Hello, I'm nested div</div> + </div> + </div> + <span id="s1">Hello, I'm a span + <div class="l1"> + <span>Hi I am a nested span</span> + <span class="s4">Hi I am a nested classed span</span> + </div> + </span> + <span class="c1" id="s2">And me</span> + + <p class="c1" id="p1">.someclass</p> + <p id="p2">#someid</p> + <button id="b1" disabled>button[disabled]</button> + <p id="p3" class="c2"><strong>p>strong</strong></p> + +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_search-svg.html b/devtools/client/inspector/test/doc_inspector_search-svg.html new file mode 100644 index 000000000..f762b2288 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_search-svg.html @@ -0,0 +1,16 @@ +<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Inspector SVG Search Box Test</title>
+</head>
+<body>
+ <div class="class1"></div>
+ <svg>
+ <clipPath>
+ <rect x="0" y="0" width="10" height="5"></rect>
+ </clipPath>
+ <circle cx="0" cy="0" r="50" class="class2" />
+ </svg>
+</body>
+</html>
diff --git a/devtools/client/inspector/test/doc_inspector_search.html b/devtools/client/inspector/test/doc_inspector_search.html new file mode 100644 index 000000000..262eb0be6 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_search.html @@ -0,0 +1,26 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Inspector Search Box Test</title> +</head> +<body> + + <!-- This is a list of 0 h1 elements --> + + <!-- This is a list of 2 div elements --> + <div id="d1">Hello, I'm a div</div> + <div id="d2" class="c1">Hello, I'm another div</div> + + <!-- This is a list of 2 span elements --> + <span id="s1">Hello, I'm a span</span> + <span class="c1" id="s2">And me</span> + + <!-- This is a collection of various things that match only once --> + <p class="c1" id="p1">.someclass</p> + <p id="p2">#someid</p> + <button id="b1" disabled>button[disabled]</button> + <p id="p3" class="c2"><strong>p>strong</strong></p> + +</body> +</html> diff --git a/devtools/client/inspector/test/doc_inspector_select-last-selected-01.html b/devtools/client/inspector/test/doc_inspector_select-last-selected-01.html new file mode 100644 index 000000000..fbe1251cb --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_select-last-selected-01.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <title>select last selected test</title> + </head> + <body> + <div id="id1"></div> + <div id="id2"></div> + <div id="id3"> + <ul class="aList"> + <li class="item"></li> + <li class="item"></li> + <li class="item"></li> + <li class="item"> + <span id="id4"></span> + </li> + </ul> + </div> + </body> +</html>
\ No newline at end of file diff --git a/devtools/client/inspector/test/doc_inspector_select-last-selected-02.html b/devtools/client/inspector/test/doc_inspector_select-last-selected-02.html new file mode 100644 index 000000000..2fbef312c --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_select-last-selected-02.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <title>select last selected test</title> + </head> + <body> + <div id="id5"></div> + </body> +</html>
\ No newline at end of file diff --git a/devtools/client/inspector/test/doc_inspector_svg.svg b/devtools/client/inspector/test/doc_inspector_svg.svg new file mode 100644 index 000000000..75154dcf3 --- /dev/null +++ b/devtools/client/inspector/test/doc_inspector_svg.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <circle r="5"/> +</svg> diff --git a/devtools/client/inspector/test/head.js b/devtools/client/inspector/test/head.js new file mode 100644 index 000000000..f251568df --- /dev/null +++ b/devtools/client/inspector/test/head.js @@ -0,0 +1,732 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from ../../framework/test/shared-head.js */ +/* import-globals-from ../../commandline/test/helpers.js */ +/* import-globals-from ../../shared/test/test-actor-registry.js */ +/* import-globals-from ../../inspector/test/shared-head.js */ +"use strict"; + +// Load the shared-head file first. +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", + this); + +// Services.prefs.setBoolPref("devtools.debugger.log", true); +// SimpleTest.registerCleanupFunction(() => { +// Services.prefs.clearUserPref("devtools.debugger.log"); +// }); + +// Import the GCLI test helper +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/commandline/test/helpers.js", + this); + +// Import helpers registering the test-actor in remote targets +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/shared/test/test-actor-registry.js", + this); + +// Import helpers for the inspector that are also shared with others +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js", + this); + +const {LocalizationHelper} = require("devtools/shared/l10n"); +const INSPECTOR_L10N = + new LocalizationHelper("devtools/client/locales/inspector.properties"); + +flags.testing = true; +registerCleanupFunction(() => { + flags.testing = false; +}); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.inspector.activeSidebar"); +}); + +registerCleanupFunction(function* () { + // Move the mouse outside inspector. If the test happened fake a mouse event + // somewhere over inspector the pointer is considered to be there when the + // next test begins. This might cause unexpected events to be emitted when + // another test moves the mouse. + EventUtils.synthesizeMouseAtPoint(1, 1, {type: "mousemove"}, window); +}); + +var navigateTo = Task.async(function* (inspector, url) { + let markuploaded = inspector.once("markuploaded"); + let onNewRoot = inspector.once("new-root"); + let onUpdated = inspector.once("inspector-updated"); + + info("Navigating to: " + url); + let activeTab = inspector.toolbox.target.activeTab; + yield activeTab.navigateTo(url); + + info("Waiting for markup view to load after navigation."); + yield markuploaded; + + info("Waiting for new root."); + yield onNewRoot; + + info("Waiting for inspector to update after new-root event."); + yield onUpdated; +}); + +/** + * Start the element picker and focus the content window. + * @param {Toolbox} toolbox + * @param {Boolean} skipFocus - Allow tests to bypass the focus event. + */ +var startPicker = Task.async(function* (toolbox, skipFocus) { + info("Start the element picker"); + toolbox.win.focus(); + yield toolbox.highlighterUtils.startPicker(); + if (!skipFocus) { + // By default make sure the content window is focused since the picker may not focus + // the content window by default. + yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + content.focus(); + }); + } +}); + +/** + * Highlight a node and set the inspector's current selection to the node or + * the first match of the given css selector. + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox + * @return a promise that resolves when the inspector is updated with the new + * node + */ +function selectAndHighlightNode(selector, inspector) { + info("Highlighting and selecting the node " + selector); + return selectNode(selector, inspector, "test-highlight"); +} + +/** + * Select node for a given selector, make it focusable and set focus in its + * container element. + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector The current inspector-panel instance. + * @return {MarkupContainer} + */ +function* focusNode(selector, inspector) { + getContainerForNodeFront(inspector.walker.rootNode, inspector).elt.focus(); + let nodeFront = yield getNodeFront(selector, inspector); + let container = getContainerForNodeFront(nodeFront, inspector); + yield selectNode(nodeFront, inspector); + EventUtils.sendKey("return", inspector.panelWin); + return container; +} + +/** + * Set the inspector's current selection to null so that no node is selected + * + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox + * @return a promise that resolves when the inspector is updated + */ +function clearCurrentNodeSelection(inspector) { + info("Clearing the current selection"); + let updated = inspector.once("inspector-updated"); + inspector.selection.setNodeFront(null); + return updated; +} + +/** + * Open the inspector in a tab with given URL. + * @param {string} url The URL to open. + * @param {String} hostType Optional hostType, as defined in Toolbox.HostType + * @return A promise that is resolved once the tab and inspector have loaded + * with an object: { tab, toolbox, inspector }. + */ +var openInspectorForURL = Task.async(function* (url, hostType) { + let tab = yield addTab(url); + let { inspector, toolbox, testActor } = yield openInspector(hostType); + return { tab, inspector, toolbox, testActor }; +}); + +function getActiveInspector() { + let target = TargetFactory.forTab(gBrowser.selectedTab); + return gDevTools.getToolbox(target).getPanel("inspector"); +} + +/** + * Right click on a node in the test page and click on the inspect menu item. + * @param {TestActor} + * @param {String} selector The selector for the node to click on in the page. + * @return {Promise} Resolves to the inspector when it has opened and is updated + */ +var clickOnInspectMenuItem = Task.async(function* (testActor, selector) { + info("Showing the contextual menu on node " + selector); + let contentAreaContextMenu = document.querySelector( + "#contentAreaContextMenu"); + let contextOpened = once(contentAreaContextMenu, "popupshown"); + + yield testActor.synthesizeMouse({ + selector: selector, + center: true, + options: {type: "contextmenu", button: 2} + }); + + yield contextOpened; + + info("Triggering the inspect action"); + yield gContextMenu.inspectNode(); + + info("Hiding the menu"); + let contextClosed = once(contentAreaContextMenu, "popuphidden"); + contentAreaContextMenu.hidePopup(); + yield contextClosed; + + return getActiveInspector(); +}); + +/** + * Get the NodeFront for a node that matches a given css selector inside a + * given iframe. + * @param {String|NodeFront} selector + * @param {String|NodeFront} frameSelector A selector that matches the iframe + * the node is in + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {Promise} Resolves when the inspector is updated with the new node + */ +var getNodeFrontInFrame = Task.async(function* (selector, frameSelector, + inspector) { + let iframe = yield getNodeFront(frameSelector, inspector); + let {nodes} = yield inspector.walker.children(iframe); + return inspector.walker.querySelector(nodes[0], selector); +}); + +var focusSearchBoxUsingShortcut = Task.async(function* (panelWin, callback) { + info("Focusing search box"); + let searchBox = panelWin.document.getElementById("inspector-searchbox"); + let focused = once(searchBox, "focus"); + + panelWin.focus(); + + synthesizeKeyShortcut(INSPECTOR_L10N.getStr("inspector.searchHTML.key")); + + yield focused; + + if (callback) { + callback(); + } +}); + +/** + * Get the MarkupContainer object instance that corresponds to the given + * NodeFront + * @param {NodeFront} nodeFront + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {MarkupContainer} + */ +function getContainerForNodeFront(nodeFront, {markup}) { + return markup.getContainer(nodeFront); +} + +/** + * Get the MarkupContainer object instance that corresponds to the given + * selector + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {MarkupContainer} + */ +var getContainerForSelector = Task.async(function* (selector, inspector) { + info("Getting the markup-container for node " + selector); + let nodeFront = yield getNodeFront(selector, inspector); + let container = getContainerForNodeFront(nodeFront, inspector); + info("Found markup-container " + container); + return container; +}); + +/** + * Simulate a mouse-over on the markup-container (a line in the markup-view) + * that corresponds to the selector passed. + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {Promise} Resolves when the container is hovered and the higlighter + * is shown on the corresponding node + */ +var hoverContainer = Task.async(function* (selector, inspector) { + info("Hovering over the markup-container for node " + selector); + + let nodeFront = yield getNodeFront(selector, inspector); + let container = getContainerForNodeFront(nodeFront, inspector); + + let highlit = inspector.toolbox.once("node-highlight"); + EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousemove"}, + inspector.markup.doc.defaultView); + return highlit; +}); + +/** + * Simulate a click on the markup-container (a line in the markup-view) + * that corresponds to the selector passed. + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {Promise} Resolves when the node has been selected. + */ +var clickContainer = Task.async(function* (selector, inspector) { + info("Clicking on the markup-container for node " + selector); + + let nodeFront = yield getNodeFront(selector, inspector); + let container = getContainerForNodeFront(nodeFront, inspector); + + let updated = inspector.once("inspector-updated"); + EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousedown"}, + inspector.markup.doc.defaultView); + EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mouseup"}, + inspector.markup.doc.defaultView); + return updated; +}); + +/** + * Simulate the mouse leaving the markup-view area + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return a promise when done + */ +function mouseLeaveMarkupView(inspector) { + info("Leaving the markup-view area"); + let def = defer(); + + // Find another element to mouseover over in order to leave the markup-view + let btn = inspector.toolbox.doc.querySelector("#toolbox-controls"); + + EventUtils.synthesizeMouseAtCenter(btn, {type: "mousemove"}, + inspector.toolbox.win); + executeSoon(def.resolve); + + return def.promise; +} + +/** + * Dispatch the copy event on the given element + */ +function fireCopyEvent(element) { + let evt = element.ownerDocument.createEvent("Event"); + evt.initEvent("copy", true, true); + element.dispatchEvent(evt); +} + +/** + * Undo the last markup-view action and wait for the corresponding mutation to + * occur + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return a promise that resolves when the markup-mutation has been treated or + * rejects if no undo action is possible + */ +function undoChange(inspector) { + let canUndo = inspector.markup.undo.canUndo(); + ok(canUndo, "The last change in the markup-view can be undone"); + if (!canUndo) { + return promise.reject(); + } + + let mutated = inspector.once("markupmutation"); + inspector.markup.undo.undo(); + return mutated; +} + +/** + * Redo the last markup-view action and wait for the corresponding mutation to + * occur + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return a promise that resolves when the markup-mutation has been treated or + * rejects if no redo action is possible + */ +function redoChange(inspector) { + let canRedo = inspector.markup.undo.canRedo(); + ok(canRedo, "The last change in the markup-view can be redone"); + if (!canRedo) { + return promise.reject(); + } + + let mutated = inspector.once("markupmutation"); + inspector.markup.undo.redo(); + return mutated; +} + +/** + * A helper that fetches a front for a node that matches the given selector or + * doctype node if the selector is falsy. + */ +function* getNodeFrontForSelector(selector, inspector) { + if (selector) { + info("Retrieving front for selector " + selector); + return getNodeFront(selector, inspector); + } + + info("Retrieving front for doctype node"); + let {nodes} = yield inspector.walker.children(inspector.walker.rootNode); + return nodes[0]; +} + +/** + * A simple polling helper that executes a given function until it returns true. + * @param {Function} check A generator function that is expected to return true at some + * stage. + * @param {String} desc A text description to be displayed when the polling starts. + * @param {Number} attemptes Optional number of times we poll. Defaults to 10. + * @param {Number} timeBetweenAttempts Optional time to wait between each attempt. + * Defaults to 200ms. + */ +function* poll(check, desc, attempts = 10, timeBetweenAttempts = 200) { + info(desc); + + for (let i = 0; i < attempts; i++) { + if (yield check()) { + return; + } + yield new Promise(resolve => setTimeout(resolve, timeBetweenAttempts)); + } + + throw new Error(`Timeout while: ${desc}`); +} + +/** + * Encapsulate some common operations for highlighter's tests, to have + * the tests cleaner, without exposing directly `inspector`, `highlighter`, and + * `testActor` if not needed. + * + * @param {String} + * The highlighter's type + * @return + * A generator function that takes an object with `inspector` and `testActor` + * properties. (see `openInspector`) + */ +const getHighlighterHelperFor = (type) => Task.async( + function* ({inspector, testActor}) { + let front = inspector.inspector; + let highlighter = yield front.getHighlighterByType(type); + + let prefix = ""; + + // Internals for mouse events + let prevX, prevY; + + // Highlighted node + let highlightedNode = null; + + return { + set prefix(value) { + prefix = value; + }, + + get highlightedNode() { + if (!highlightedNode) { + return null; + } + + return { + getComputedStyle: function* (options = {}) { + return yield inspector.pageStyle.getComputed( + highlightedNode, options); + } + }; + }, + + show: function* (selector = ":root", options) { + highlightedNode = yield getNodeFront(selector, inspector); + return yield highlighter.show(highlightedNode, options); + }, + + hide: function* () { + yield highlighter.hide(); + }, + + isElementHidden: function* (id) { + return (yield testActor.getHighlighterNodeAttribute( + prefix + id, "hidden", highlighter)) === "true"; + }, + + getElementTextContent: function* (id) { + return yield testActor.getHighlighterNodeTextContent( + prefix + id, highlighter); + }, + + getElementAttribute: function* (id, name) { + return yield testActor.getHighlighterNodeAttribute( + prefix + id, name, highlighter); + }, + + waitForElementAttributeSet: function* (id, name) { + yield poll(function* () { + let value = yield testActor.getHighlighterNodeAttribute( + prefix + id, name, highlighter); + return !!value; + }, `Waiting for element ${id} to have attribute ${name} set`); + }, + + waitForElementAttributeRemoved: function* (id, name) { + yield poll(function* () { + let value = yield testActor.getHighlighterNodeAttribute( + prefix + id, name, highlighter); + return !value; + }, `Waiting for element ${id} to have attribute ${name} removed`); + }, + + synthesizeMouse: function* (options) { + options = Object.assign({selector: ":root"}, options); + yield testActor.synthesizeMouse(options); + }, + + // This object will synthesize any "mouse" prefixed event to the + // `testActor`, using the name of method called as suffix for the + // event's name. + // If no x, y coords are given, the previous ones are used. + // + // For example: + // mouse.down(10, 20); // synthesize "mousedown" at 10,20 + // mouse.move(20, 30); // synthesize "mousemove" at 20,30 + // mouse.up(); // synthesize "mouseup" at 20,30 + mouse: new Proxy({}, { + get: (target, name) => + function* (x = prevX, y = prevY) { + prevX = x; + prevY = y; + yield testActor.synthesizeMouse({ + selector: ":root", x, y, options: {type: "mouse" + name}}); + } + }), + + reflow: function* () { + yield testActor.reflow(); + }, + + finalize: function* () { + highlightedNode = null; + yield highlighter.finalize(); + } + }; + } +); + +// The expand all operation of the markup-view calls itself recursively and +// there's not one event we can wait for to know when it's done so use this +// helper function to wait until all recursive children updates are done. +function* waitForMultipleChildrenUpdates(inspector) { + // As long as child updates are queued up while we wait for an update already + // wait again + if (inspector.markup._queuedChildUpdates && + inspector.markup._queuedChildUpdates.size) { + yield waitForChildrenUpdated(inspector); + return yield waitForMultipleChildrenUpdates(inspector); + } + return null; +} + +/** + * Using the markupview's _waitForChildren function, wait for all queued + * children updates to be handled. + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return a promise that resolves when all queued children updates have been + * handled + */ +function waitForChildrenUpdated({markup}) { + info("Waiting for queued children updates to be handled"); + let def = defer(); + markup._waitForChildren().then(() => { + executeSoon(def.resolve); + }); + return def.promise; +} + +/** + * Wait for the toolbox to emit the styleeditor-selected event and when done + * wait for the stylesheet identified by href to be loaded in the stylesheet + * editor + * + * @param {Toolbox} toolbox + * @param {String} href + * Optional, if not provided, wait for the first editor to be ready + * @return a promise that resolves to the editor when the stylesheet editor is + * ready + */ +function waitForStyleEditor(toolbox, href) { + let def = defer(); + + info("Waiting for the toolbox to switch to the styleeditor"); + toolbox.once("styleeditor-selected").then(() => { + let panel = toolbox.getCurrentPanel(); + ok(panel && panel.UI, "Styleeditor panel switched to front"); + + // A helper that resolves the promise once it receives an editor that + // matches the expected href. Returns false if the editor was not correct. + let gotEditor = (event, editor) => { + let currentHref = editor.styleSheet.href; + if (!href || (href && currentHref.endsWith(href))) { + info("Stylesheet editor selected"); + panel.UI.off("editor-selected", gotEditor); + + editor.getSourceEditor().then(sourceEditor => { + info("Stylesheet editor fully loaded"); + def.resolve(sourceEditor); + }); + + return true; + } + + info("The editor was incorrect. Waiting for editor-selected event."); + return false; + }; + + // The expected editor may already be selected. Check the if the currently + // selected editor is the expected one and if not wait for an + // editor-selected event. + if (!gotEditor("styleeditor-selected", panel.UI.selectedEditor)) { + // The expected editor is not selected (yet). Wait for it. + panel.UI.on("editor-selected", gotEditor); + } + }); + + return def.promise; +} + +/** + * Checks if document's active element is within the given element. + * @param {HTMLDocument} doc document with active element in question + * @param {DOMNode} container element tested on focus containment + * @return {Boolean} + */ +function containsFocus(doc, container) { + let elm = doc.activeElement; + while (elm) { + if (elm === container) { + return true; + } + elm = elm.parentNode; + } + return false; +} + +/** + * Listen for a new tab to open and return a promise that resolves when one + * does and completes the load event. + * + * @return a promise that resolves to the tab object + */ +var waitForTab = Task.async(function* () { + info("Waiting for a tab to open"); + yield once(gBrowser.tabContainer, "TabOpen"); + let tab = gBrowser.selectedTab; + yield BrowserTestUtils.browserLoaded(tab.linkedBrowser); + info("The tab load completed"); + return tab; +}); + +/** + * Simulate the key input for the given input in the window. + * + * @param {String} input + * The string value to input + * @param {Window} win + * The window containing the panel + */ +function synthesizeKeys(input, win) { + for (let key of input.split("")) { + EventUtils.synthesizeKey(key, {}, win); + } +} + +/** + * Given a tooltip object instance (see Tooltip.js), checks if it is set to + * toggle and hover and if so, checks if the given target is a valid hover + * target. This won't actually show the tooltip (the less we interact with XUL + * panels during test runs, the better). + * + * @return a promise that resolves when the answer is known + */ +function isHoverTooltipTarget(tooltip, target) { + if (!tooltip._toggle._baseNode || !tooltip.panel) { + return promise.reject(new Error( + "The tooltip passed isn't set to toggle on hover or is not a tooltip")); + } + return tooltip._toggle.isValidHoverTarget(target); +} + +/** + * Same as isHoverTooltipTarget except that it will fail the test if there is no + * tooltip defined on hover of the given element + * + * @return a promise + */ +function assertHoverTooltipOn(tooltip, element) { + return isHoverTooltipTarget(tooltip, element).then(() => { + ok(true, "A tooltip is defined on hover of the given element"); + }, () => { + ok(false, "No tooltip is defined on hover of the given element"); + }); +} + +/** + * Open the inspector menu and return all of it's items in a flat array + * @param {InspectorPanel} inspector + * @param {Object} options to pass into openMenu + * @return An array of MenuItems + */ +function openContextMenuAndGetAllItems(inspector, options) { + let menu = inspector._openMenu(options); + + // Flatten all menu items into a single array to make searching through it easier + let allItems = [].concat.apply([], menu.items.map(function addItem(item) { + if (item.submenu) { + return addItem(item.submenu.items); + } + return item; + })); + + return allItems; +} + +/** + * Get the rule editor from the rule-view given its index + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {Number} childrenIndex + * The children index of the element to get + * @param {Number} nodeIndex + * The child node index of the element to get + * @return {DOMNode} The rule editor if any at this index + */ +function getRuleViewRuleEditor(view, childrenIndex, nodeIndex) { + return nodeIndex !== undefined ? + view.element.children[childrenIndex].childNodes[nodeIndex]._ruleEditor : + view.element.children[childrenIndex]._ruleEditor; +} + +/** + * Get the text displayed for a given DOM Element's textContent within the + * markup view. + * + * @param {String} selector + * @param {InspectorPanel} inspector + * @return {String} The text displayed in the markup view + */ +function* getDisplayedNodeTextContent(selector, inspector) { + // We have to ensure that the textContent is displayed, for that the DOM + // Element has to be selected in the markup view and to be expanded. + yield selectNode(selector, inspector); + + let container = yield getContainerForSelector(selector, inspector); + yield inspector.markup.expandNode(container.node); + yield waitForMultipleChildrenUpdates(inspector); + if (container) { + let textContainer = container.elt.querySelector("pre"); + return textContainer.textContent; + } + return null; +} diff --git a/devtools/client/inspector/test/shared-head.js b/devtools/client/inspector/test/shared-head.js new file mode 100644 index 000000000..13eeca0f7 --- /dev/null +++ b/devtools/client/inspector/test/shared-head.js @@ -0,0 +1,186 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* globals registerTestActor, getTestActor, Task, openToolboxForTab, gBrowser */ + +// This file contains functions related to the inspector that are also of interest to +// other test directores as well. + +/** + * Open the toolbox, with the inspector tool visible. + * @param {String} hostType Optional hostType, as defined in Toolbox.HostType + * @return a promise that resolves when the inspector is ready + */ +var openInspector = Task.async(function* (hostType) { + info("Opening the inspector"); + + let toolbox = yield openToolboxForTab(gBrowser.selectedTab, "inspector", + hostType); + let inspector = toolbox.getPanel("inspector"); + + if (inspector._updateProgress) { + info("Need to wait for the inspector to update"); + yield inspector.once("inspector-updated"); + } + + info("Waiting for actor features to be detected"); + yield inspector._detectingActorFeatures; + + yield registerTestActor(toolbox.target.client); + let testActor = yield getTestActor(toolbox); + + return {toolbox, inspector, testActor}; +}); + +/** + * Open the toolbox, with the inspector tool visible, and the one of the sidebar + * tabs selected. + * + * @param {String} id + * The ID of the sidebar tab to be opened + * @return a promise that resolves when the inspector is ready and the tab is + * visible and ready + */ +var openInspectorSidebarTab = Task.async(function* (id) { + let {toolbox, inspector, testActor} = yield openInspector(); + + info("Selecting the " + id + " sidebar"); + inspector.sidebar.select(id); + + return { + toolbox, + inspector, + testActor + }; +}); + +/** + * Open the toolbox, with the inspector tool visible, and the rule-view + * sidebar tab selected. + * + * @return a promise that resolves when the inspector is ready and the rule view + * is visible and ready + */ +function openRuleView() { + return openInspectorSidebarTab("ruleview").then(data => { + // Replace the view to use a custom throttle function that can be triggered manually + // through an additional ".flush()" property. + data.inspector.ruleview.view.throttle = manualThrottle(); + + return { + toolbox: data.toolbox, + inspector: data.inspector, + testActor: data.testActor, + view: data.inspector.ruleview.view + }; + }); +} + +/** + * Open the toolbox, with the inspector tool visible, and the computed-view + * sidebar tab selected. + * + * @return a promise that resolves when the inspector is ready and the computed + * view is visible and ready + */ +function openComputedView() { + return openInspectorSidebarTab("computedview").then(data => { + return { + toolbox: data.toolbox, + inspector: data.inspector, + testActor: data.testActor, + view: data.inspector.computedview.computedView + }; + }); +} + +/** + * Select the rule view sidebar tab on an already opened inspector panel. + * + * @param {InspectorPanel} inspector + * The opened inspector panel + * @return {CssRuleView} the rule view + */ +function selectRuleView(inspector) { + inspector.sidebar.select("ruleview"); + return inspector.ruleview.view; +} + +/** + * Select the computed view sidebar tab on an already opened inspector panel. + * + * @param {InspectorPanel} inspector + * The opened inspector panel + * @return {CssComputedView} the computed view + */ +function selectComputedView(inspector) { + inspector.sidebar.select("computedview"); + return inspector.computedview.computedView; +} + +/** + * Get the NodeFront for a node that matches a given css selector, via the + * protocol. + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {Promise} Resolves to the NodeFront instance + */ +function getNodeFront(selector, {walker}) { + if (selector._form) { + return selector; + } + return walker.querySelector(walker.rootNode, selector); +} + +/** + * Set the inspector's current selection to the first match of the given css + * selector + * @param {String|NodeFront} selector + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @param {String} reason Defaults to "test" which instructs the inspector not + * to highlight the node upon selection + * @return {Promise} Resolves when the inspector is updated with the new node + */ +var selectNode = Task.async(function* (selector, inspector, reason = "test") { + info("Selecting the node for '" + selector + "'"); + let nodeFront = yield getNodeFront(selector, inspector); + let updated = inspector.once("inspector-updated"); + inspector.selection.setNodeFront(nodeFront, reason); + yield updated; +}); + +/** + * Create a throttling function that can be manually "flushed". This is to replace the + * use of the `throttle` function from `devtools/client/inspector/shared/utils.js`, which + * has a setTimeout that can cause intermittents. + * @return {Function} This function has the same function signature as throttle, but + * the property `.flush()` has been added for flushing out any + * throttled calls. + */ +function manualThrottle() { + let calls = []; + + function throttle(func, wait, scope) { + return function () { + let existingCall = calls.find(call => call.func === func); + if (existingCall) { + existingCall.args = arguments; + } else { + calls.push({ func, wait, scope, args: arguments }); + } + }; + } + + throttle.flush = function () { + calls.forEach(({func, scope, args}) => func.apply(scope, args)); + calls = []; + }; + + return throttle; +} diff --git a/devtools/client/inspector/toolsidebar.js b/devtools/client/inspector/toolsidebar.js new file mode 100644 index 000000000..d013b7b84 --- /dev/null +++ b/devtools/client/inspector/toolsidebar.js @@ -0,0 +1,325 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EventEmitter = require("devtools/shared/event-emitter"); +var Telemetry = require("devtools/client/shared/telemetry"); +var { Task } = require("devtools/shared/task"); + +/** + * This object represents replacement for ToolSidebar + * implemented in devtools/client/framework/sidebar.js module + * + * This new component is part of devtools.html aimed at + * removing XUL and use HTML for entire DevTools UI. + * There are currently two implementation of the side bar since + * the `sidebar.js` module (mentioned above) is still used by + * other panels. + * As soon as all panels are using this HTML based + * implementation it can be removed. + */ +function ToolSidebar(tabbox, panel, uid, options = {}) { + EventEmitter.decorate(this); + + this._tabbox = tabbox; + this._uid = uid; + this._panelDoc = this._tabbox.ownerDocument; + this._toolPanel = panel; + this._options = options; + + if (!options.disableTelemetry) { + this._telemetry = new Telemetry(); + } + + this._tabs = []; + + if (this._options.hideTabstripe) { + this._tabbox.setAttribute("hidetabs", "true"); + } + + this.render(); + + this._toolPanel.emit("sidebar-created", this); +} + +exports.ToolSidebar = ToolSidebar; + +ToolSidebar.prototype = { + TABPANEL_ID_PREFIX: "sidebar-panel-", + + // React + + get React() { + return this._toolPanel.React; + }, + + get ReactDOM() { + return this._toolPanel.ReactDOM; + }, + + get browserRequire() { + return this._toolPanel.browserRequire; + }, + + get InspectorTabPanel() { + return this._toolPanel.InspectorTabPanel; + }, + + // Rendering + + render: function () { + let Tabbar = this.React.createFactory(this.browserRequire( + "devtools/client/shared/components/tabs/tabbar")); + + let sidebar = Tabbar({ + toolbox: this._toolPanel._toolbox, + showAllTabsMenu: true, + onSelect: this.handleSelectionChange.bind(this), + }); + + this._tabbar = this.ReactDOM.render(sidebar, this._tabbox); + }, + + /** + * Register a side-panel tab. + * + * @param {string} tab uniq id + * @param {string} title tab title + * @param {React.Component} panel component. See `InspectorPanelTab` as an example. + * @param {boolean} selected true if the panel should be selected + */ + addTab: function (id, title, panel, selected) { + this._tabbar.addTab(id, title, selected, panel); + this.emit("new-tab-registered", id); + }, + + /** + * Helper API for adding side-panels that use existing DOM nodes + * (defined within inspector.xhtml) as the content. + * + * @param {string} tab uniq id + * @param {string} title tab title + * @param {boolean} selected true if the panel should be selected + */ + addExistingTab: function (id, title, selected) { + let panel = this.InspectorTabPanel({ + id: id, + idPrefix: this.TABPANEL_ID_PREFIX, + key: id, + title: title, + }); + + this.addTab(id, title, panel, selected); + }, + + /** + * Helper API for adding side-panels that use existing <iframe> nodes + * (defined within inspector.xhtml) as the content. + * The document must have a title, which will be used as the name of the tab. + * + * @param {string} tab uniq id + * @param {string} title tab title + * @param {string} url + * @param {boolean} selected true if the panel should be selected + */ + addFrameTab: function (id, title, url, selected) { + let panel = this.InspectorTabPanel({ + id: id, + idPrefix: this.TABPANEL_ID_PREFIX, + key: id, + title: title, + url: url, + onMount: this.onSidePanelMounted.bind(this), + }); + + this.addTab(id, title, panel, selected); + }, + + onSidePanelMounted: function (content, props) { + let iframe = content.querySelector("iframe"); + if (!iframe || iframe.getAttribute("src")) { + return; + } + + let onIFrameLoaded = (event) => { + iframe.removeEventListener("load", onIFrameLoaded, true); + + let doc = event.target; + let win = doc.defaultView; + if ("setPanel" in win) { + win.setPanel(this._toolPanel, iframe); + } + this.emit(props.id + "-ready"); + }; + + iframe.addEventListener("load", onIFrameLoaded, true); + iframe.setAttribute("src", props.url); + }, + + /** + * Remove an existing tab. + * @param {String} tabId The ID of the tab that was used to register it, or + * the tab id attribute value if the tab existed before the sidebar + * got created. + * @param {String} tabPanelId Optional. If provided, this ID will be used + * instead of the tabId to retrieve and remove the corresponding <tabpanel> + */ + removeTab: Task.async(function* (tabId, tabPanelId) { + this._tabbar.removeTab(tabId); + + let win = this.getWindowForTab(tabId); + if (win && ("destroy" in win)) { + yield win.destroy(); + } + + this.emit("tab-unregistered", tabId); + }), + + /** + * Show or hide a specific tab. + * @param {Boolean} isVisible True to show the tab/tabpanel, False to hide it. + * @param {String} id The ID of the tab to be hidden. + */ + toggleTab: function (isVisible, id) { + this._tabbar.toggleTab(id, isVisible); + }, + + /** + * Select a specific tab. + */ + select: function (id) { + this._tabbar.select(id); + }, + + /** + * Return the id of the selected tab. + */ + getCurrentTabID: function () { + return this._currentTool; + }, + + /** + * Returns the requested tab panel based on the id. + * @param {String} id + * @return {DOMNode} + */ + getTabPanel: function (id) { + // Search with and without the ID prefix as there might have been existing + // tabpanels by the time the sidebar got created + return this._panelDoc.querySelector("#" + + this.TABPANEL_ID_PREFIX + id + ", #" + id); + }, + + /** + * Event handler. + */ + handleSelectionChange: function (id) { + if (this._destroyed) { + return; + } + + let previousTool = this._currentTool; + if (previousTool) { + if (this._telemetry) { + this._telemetry.toolClosed(previousTool); + } + this.emit(previousTool + "-unselected"); + } + + this._currentTool = id; + + if (this._telemetry) { + this._telemetry.toolOpened(this._currentTool); + } + + this.emit(this._currentTool + "-selected"); + this.emit("select", this._currentTool); + }, + + /** + * Show the sidebar. + * + * @param {String} id + * The sidebar tab id to select. + */ + show: function (id) { + this._tabbox.removeAttribute("hidden"); + + // If an id is given, select the corresponding sidebar tab and record the + // tool opened. + if (id) { + this._currentTool = id; + + if (this._telemetry) { + this._telemetry.toolOpened(this._currentTool); + } + } + + this.emit("show"); + }, + + /** + * Show the sidebar. + */ + hide: function () { + this._tabbox.setAttribute("hidden", "true"); + + this.emit("hide"); + }, + + /** + * Return the window containing the tab content. + */ + getWindowForTab: function (id) { + // Get the tabpanel and make sure it contains an iframe + let panel = this.getTabPanel(id); + if (!panel || !panel.firstElementChild || !panel.firstElementChild.contentWindow) { + return null; + } + + return panel.firstElementChild.contentWindow; + }, + + /** + * Clean-up. + */ + destroy: Task.async(function* () { + if (this._destroyed) { + return; + } + this._destroyed = true; + + this.emit("destroy"); + + // Note that we check for the existence of this._tabbox.tabpanels at each + // step as the container window may have been closed by the time one of the + // panel's destroy promise resolves. + let tabpanels = [...this._tabbox.querySelectorAll(".tab-panel-box")]; + for (let panel of tabpanels) { + let iframe = panel.querySelector("iframe"); + if (!iframe) { + continue; + } + let win = iframe.contentWindow; + if (win && ("destroy" in win)) { + yield win.destroy(); + } + panel.remove(); + } + + if (this._currentTool && this._telemetry) { + this._telemetry.toolClosed(this._currentTool); + } + + this._toolPanel.emit("sidebar-destroyed", this); + + this._tabs = null; + this._tabbox = null; + this._panelDoc = null; + this._toolPanel = null; + }) +}; |