| // Copyright 2008 The Closure Library Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS-IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| /** |
| * @fileoverview Testing utilities for DOM related tests. |
| * |
| * @author robbyw@google.com (Robby Walker) |
| */ |
| |
| goog.provide('goog.testing.dom'); |
| |
| goog.require('goog.array'); |
| goog.require('goog.asserts'); |
| goog.require('goog.dom'); |
| goog.require('goog.dom.NodeIterator'); |
| goog.require('goog.dom.NodeType'); |
| goog.require('goog.dom.TagIterator'); |
| goog.require('goog.dom.TagName'); |
| goog.require('goog.dom.classlist'); |
| goog.require('goog.iter'); |
| goog.require('goog.object'); |
| goog.require('goog.string'); |
| goog.require('goog.style'); |
| goog.require('goog.testing.asserts'); |
| goog.require('goog.userAgent'); |
| |
| |
| /** |
| * @return {!Node} A DIV node with a unique ID identifying the |
| * {@code END_TAG_MARKER_}. |
| * @private |
| */ |
| goog.testing.dom.createEndTagMarker_ = function() { |
| var marker = goog.dom.createElement(goog.dom.TagName.DIV); |
| marker.id = goog.getUid(marker); |
| return marker; |
| }; |
| |
| |
| /** |
| * A unique object to use as an end tag marker. |
| * @private {!Node} |
| * @const |
| */ |
| goog.testing.dom.END_TAG_MARKER_ = goog.testing.dom.createEndTagMarker_(); |
| |
| |
| /** |
| * Tests if the given iterator over nodes matches the given Array of node |
| * descriptors. Throws an error if any match fails. |
| * @param {goog.iter.Iterator} it An iterator over nodes. |
| * @param {Array<Node|number|string>} array Array of node descriptors to match |
| * against. Node descriptors can be any of the following: |
| * Node: Test if the two nodes are equal. |
| * number: Test node.nodeType == number. |
| * string starting with '#': Match the node's id with the text |
| * after "#". |
| * other string: Match the text node's contents. |
| */ |
| goog.testing.dom.assertNodesMatch = function(it, array) { |
| var i = 0; |
| goog.iter.forEach(it, function(node) { |
| if (array.length <= i) { |
| fail('Got more nodes than expected: ' + goog.testing.dom.describeNode_( |
| node)); |
| } |
| var expected = array[i]; |
| |
| if (goog.dom.isNodeLike(expected)) { |
| assertEquals('Nodes should match at position ' + i, expected, node); |
| } else if (goog.isNumber(expected)) { |
| assertEquals('Node types should match at position ' + i, expected, |
| node.nodeType); |
| } else if (expected.charAt(0) == '#') { |
| assertEquals('Expected element at position ' + i, |
| goog.dom.NodeType.ELEMENT, node.nodeType); |
| var expectedId = expected.substr(1); |
| assertEquals('IDs should match at position ' + i, |
| expectedId, node.id); |
| |
| } else { |
| assertEquals('Expected text node at position ' + i, |
| goog.dom.NodeType.TEXT, node.nodeType); |
| assertEquals('Node contents should match at position ' + i, |
| expected, node.nodeValue); |
| } |
| |
| i++; |
| }); |
| |
| assertEquals('Used entire match array', array.length, i); |
| }; |
| |
| |
| /** |
| * Exposes a node as a string. |
| * @param {Node} node A node. |
| * @return {string} A string representation of the node. |
| */ |
| goog.testing.dom.exposeNode = function(node) { |
| return (node.tagName || node.nodeValue) + (node.id ? '#' + node.id : '') + |
| ':"' + (node.innerHTML || '') + '"'; |
| }; |
| |
| |
| /** |
| * Exposes the nodes of a range wrapper as a string. |
| * @param {goog.dom.AbstractRange} range A range. |
| * @return {string} A string representation of the range. |
| */ |
| goog.testing.dom.exposeRange = function(range) { |
| // This is deliberately not implemented as |
| // goog.dom.AbstractRange.prototype.toString, because it is non-authoritative. |
| // Two equivalent ranges may have very different exposeRange values, and |
| // two different ranges may have equal exposeRange values. |
| // (The mapping of ranges to DOM nodes/offsets is a many-to-many mapping). |
| if (!range) { |
| return 'null'; |
| } |
| return goog.testing.dom.exposeNode(range.getStartNode()) + ':' + |
| range.getStartOffset() + ' to ' + |
| goog.testing.dom.exposeNode(range.getEndNode()) + ':' + |
| range.getEndOffset(); |
| }; |
| |
| |
| /** |
| * Determines if the current user agent matches the specified string. Returns |
| * false if the string does specify at least one user agent but does not match |
| * the running agent. |
| * @param {string} userAgents Space delimited string of user agents. |
| * @return {boolean} Whether the user agent was matched. Also true if no user |
| * agent was listed in the expectation string. |
| * @private |
| */ |
| goog.testing.dom.checkUserAgents_ = function(userAgents) { |
| if (goog.string.startsWith(userAgents, '!')) { |
| if (goog.string.contains(userAgents, ' ')) { |
| throw new Error('Only a single negative user agent may be specified'); |
| } |
| return !goog.userAgent[userAgents.substr(1)]; |
| } |
| |
| var agents = userAgents.split(' '); |
| var hasUserAgent = false; |
| for (var i = 0, len = agents.length; i < len; i++) { |
| var cls = agents[i]; |
| if (cls in goog.userAgent) { |
| hasUserAgent = true; |
| if (goog.userAgent[cls]) { |
| return true; |
| } |
| } |
| } |
| // If we got here, there was a user agent listed but we didn't match it. |
| return !hasUserAgent; |
| }; |
| |
| |
| /** |
| * Map function that converts end tags to a specific object. |
| * @param {Node} node The node to map. |
| * @param {undefined} ignore Always undefined. |
| * @param {!goog.iter.Iterator<Node>} iterator The iterator. |
| * @return {Node} The resulting iteration item. |
| * @private |
| */ |
| goog.testing.dom.endTagMap_ = function(node, ignore, iterator) { |
| return iterator.isEndTag() ? goog.testing.dom.END_TAG_MARKER_ : node; |
| }; |
| |
| |
| /** |
| * Check if the given node is important. A node is important if it is a |
| * non-empty text node, a non-annotated element, or an element annotated to |
| * match on this user agent. |
| * @param {Node} node The node to test. |
| * @return {boolean} Whether this node should be included for iteration. |
| * @private |
| */ |
| goog.testing.dom.nodeFilter_ = function(node) { |
| if (node.nodeType == goog.dom.NodeType.TEXT) { |
| // If a node is part of a string of text nodes and it has spaces in it, |
| // we allow it since it's going to affect the merging of nodes done below. |
| if (goog.string.isBreakingWhitespace(node.nodeValue) && |
| (!node.previousSibling || |
| node.previousSibling.nodeType != goog.dom.NodeType.TEXT) && |
| (!node.nextSibling || |
| node.nextSibling.nodeType != goog.dom.NodeType.TEXT)) { |
| return false; |
| } |
| // Allow optional text to be specified as [[BROWSER1 BROWSER2]]Text |
| var match = node.nodeValue.match(/^\[\[(.+)\]\]/); |
| if (match) { |
| return goog.testing.dom.checkUserAgents_(match[1]); |
| } |
| } else if (node.className && goog.isString(node.className)) { |
| return goog.testing.dom.checkUserAgents_(node.className); |
| } |
| return true; |
| }; |
| |
| |
| /** |
| * Determines the text to match from the given node, removing browser |
| * specification strings. |
| * @param {Node} node The node expected to match. |
| * @return {string} The text, stripped of browser specification strings. |
| * @private |
| */ |
| goog.testing.dom.getExpectedText_ = function(node) { |
| // Strip off the browser specifications. |
| return node.nodeValue.match(/^(\[\[.+\]\])?(.*)/)[2]; |
| }; |
| |
| |
| /** |
| * Describes the given node. |
| * @param {Node} node The node to describe. |
| * @return {string} A description of the node. |
| * @private |
| */ |
| goog.testing.dom.describeNode_ = function(node) { |
| if (node.nodeType == goog.dom.NodeType.TEXT) { |
| return '[Text: ' + node.nodeValue + ']'; |
| } else { |
| return '<' + node.tagName + (node.id ? ' #' + node.id : '') + ' .../>'; |
| } |
| }; |
| |
| |
| /** |
| * Assert that the html in {@code actual} is substantially similar to |
| * htmlPattern. This method tests for the same set of styles, for the same |
| * order of nodes, and the presence of attributes. Breaking whitespace nodes |
| * are ignored. Elements can be |
| * annotated with classnames corresponding to keys in goog.userAgent and will be |
| * expected to show up in that user agent and expected not to show up in |
| * others. |
| * @param {string} htmlPattern The pattern to match. |
| * @param {!Node} actual The element to check: its contents are matched |
| * against the HTML pattern. |
| * @param {boolean=} opt_strictAttributes If false, attributes that appear in |
| * htmlPattern must be in actual, but actual can have attributes not |
| * present in htmlPattern. If true, htmlPattern and actual must have the |
| * same set of attributes. Default is false. |
| */ |
| goog.testing.dom.assertHtmlContentsMatch = function(htmlPattern, actual, |
| opt_strictAttributes) { |
| var div = goog.dom.createDom(goog.dom.TagName.DIV); |
| div.innerHTML = htmlPattern; |
| |
| var errorSuffix = '\nExpected\n' + htmlPattern + '\nActual\n' + |
| actual.innerHTML; |
| |
| var actualIt = goog.iter.filter( |
| goog.iter.map(new goog.dom.TagIterator(actual), |
| goog.testing.dom.endTagMap_), |
| goog.testing.dom.nodeFilter_); |
| |
| var expectedIt = goog.iter.filter(new goog.dom.NodeIterator(div), |
| goog.testing.dom.nodeFilter_); |
| |
| var actualNode; |
| var preIterated = false; |
| var advanceActualNode = function() { |
| // If the iterator has already been advanced, don't advance it again. |
| if (!preIterated) { |
| actualNode = /** @type {Node} */ (goog.iter.nextOrValue(actualIt, null)); |
| } |
| preIterated = false; |
| |
| // Advance the iterator so long as it is return end tags. |
| while (actualNode == goog.testing.dom.END_TAG_MARKER_) { |
| actualNode = /** @type {Node} */ (goog.iter.nextOrValue(actualIt, null)); |
| } |
| }; |
| |
| // HACK(brenneman): IE has unique ideas about whitespace handling when setting |
| // innerHTML. This results in elision of leading whitespace in the expected |
| // nodes where doing so doesn't affect visible rendering. As a workaround, we |
| // remove the leading whitespace in the actual nodes where necessary. |
| // |
| // The collapsible variable tracks whether we should collapse the whitespace |
| // in the next Text node we encounter. |
| var IE_TEXT_COLLAPSE = |
| goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9'); |
| |
| var collapsible = true; |
| |
| var number = 0; |
| goog.iter.forEach(expectedIt, function(expectedNode) { |
| expectedNode = /** @type {Node} */ (expectedNode); |
| |
| advanceActualNode(); |
| assertNotNull('Finished actual HTML before finishing expected HTML at ' + |
| 'node number ' + number + ': ' + |
| goog.testing.dom.describeNode_(expectedNode) + errorSuffix, |
| actualNode); |
| |
| // Do no processing for expectedNode == div. |
| if (expectedNode == div) { |
| return; |
| } |
| |
| assertEquals('Should have the same node type, got ' + |
| goog.testing.dom.describeNode_(actualNode) + ' but expected ' + |
| goog.testing.dom.describeNode_(expectedNode) + '.' + errorSuffix, |
| expectedNode.nodeType, actualNode.nodeType); |
| |
| if (expectedNode.nodeType == goog.dom.NodeType.ELEMENT) { |
| var expectedElem = goog.asserts.assertElement(expectedNode); |
| var actualElem = goog.asserts.assertElement(actualNode); |
| |
| assertEquals('Tag names should match' + errorSuffix, |
| expectedElem.tagName, actualElem.tagName); |
| assertObjectEquals('Should have same styles' + errorSuffix, |
| goog.style.parseStyleAttribute(expectedElem.style.cssText), |
| goog.style.parseStyleAttribute(actualElem.style.cssText)); |
| goog.testing.dom.assertAttributesEqual_(errorSuffix, expectedElem, |
| actualElem, !!opt_strictAttributes); |
| |
| if (IE_TEXT_COLLAPSE && |
| goog.style.getCascadedStyle(actualElem, 'display') != 'inline') { |
| // Text may be collapsed after any non-inline element. |
| collapsible = true; |
| } |
| } else { |
| // Concatenate text nodes until we reach a non text node. |
| var actualText = actualNode.nodeValue; |
| preIterated = true; |
| while ((actualNode = /** @type {Node} */ |
| (goog.iter.nextOrValue(actualIt, null))) && |
| actualNode.nodeType == goog.dom.NodeType.TEXT) { |
| actualText += actualNode.nodeValue; |
| } |
| |
| if (IE_TEXT_COLLAPSE) { |
| // Collapse the leading whitespace, unless the string consists entirely |
| // of whitespace. |
| if (collapsible && !goog.string.isEmptyOrWhitespace(actualText)) { |
| actualText = goog.string.trimLeft(actualText); |
| } |
| // Prepare to collapse whitespace in the next Text node if this one does |
| // not end in a whitespace character. |
| collapsible = /\s$/.test(actualText); |
| } |
| |
| var expectedText = goog.testing.dom.getExpectedText_(expectedNode); |
| if ((actualText && !goog.string.isBreakingWhitespace(actualText)) || |
| (expectedText && !goog.string.isBreakingWhitespace(expectedText))) { |
| var normalizedActual = actualText.replace(/\s+/g, ' '); |
| var normalizedExpected = expectedText.replace(/\s+/g, ' '); |
| |
| assertEquals('Text should match' + errorSuffix, normalizedExpected, |
| normalizedActual); |
| } |
| } |
| |
| number++; |
| }); |
| |
| advanceActualNode(); |
| assertNull('Finished expected HTML before finishing actual HTML' + |
| errorSuffix, goog.iter.nextOrValue(actualIt, null)); |
| }; |
| |
| |
| /** |
| * Assert that the html in {@code actual} is substantially similar to |
| * htmlPattern. This method tests for the same set of styles, and for the same |
| * order of nodes. Breaking whitespace nodes are ignored. Elements can be |
| * annotated with classnames corresponding to keys in goog.userAgent and will be |
| * expected to show up in that user agent and expected not to show up in |
| * others. |
| * @param {string} htmlPattern The pattern to match. |
| * @param {string} actual The html to check. |
| */ |
| goog.testing.dom.assertHtmlMatches = function(htmlPattern, actual) { |
| var div = goog.dom.createDom(goog.dom.TagName.DIV); |
| div.innerHTML = actual; |
| |
| goog.testing.dom.assertHtmlContentsMatch(htmlPattern, div); |
| }; |
| |
| |
| /** |
| * Finds the first text node descendant of root with the given content. Note |
| * that this operates on a text node level, so if text nodes get split this |
| * may not match the user visible text. Using normalize() may help here. |
| * @param {string|RegExp} textOrRegexp The text to find, or a regular |
| * expression to find a match of. |
| * @param {Element} root The element to search in. |
| * @return {Node} The first text node that matches, or null if none is found. |
| */ |
| goog.testing.dom.findTextNode = function(textOrRegexp, root) { |
| var it = new goog.dom.NodeIterator(root); |
| var ret = goog.iter.nextOrValue(goog.iter.filter(it, function(node) { |
| if (node.nodeType == goog.dom.NodeType.TEXT) { |
| if (goog.isString(textOrRegexp)) { |
| return node.nodeValue == textOrRegexp; |
| } else { |
| return !!node.nodeValue.match(textOrRegexp); |
| } |
| } else { |
| return false; |
| } |
| }), null); |
| return /** @type {Node} */ (ret); |
| }; |
| |
| |
| /** |
| * Assert the end points of a range. |
| * |
| * Notice that "Are two ranges visually identical?" and "Do two ranges have |
| * the same endpoint?" are independent questions. Two visually identical ranges |
| * may have different endpoints. And two ranges with the same endpoints may |
| * be visually different. |
| * |
| * @param {Node} start The expected start node. |
| * @param {number} startOffset The expected start offset. |
| * @param {Node} end The expected end node. |
| * @param {number} endOffset The expected end offset. |
| * @param {goog.dom.AbstractRange} range The actual range. |
| */ |
| goog.testing.dom.assertRangeEquals = function(start, startOffset, end, |
| endOffset, range) { |
| assertEquals('Unexpected start node', start, range.getStartNode()); |
| assertEquals('Unexpected end node', end, range.getEndNode()); |
| assertEquals('Unexpected start offset', startOffset, range.getStartOffset()); |
| assertEquals('Unexpected end offset', endOffset, range.getEndOffset()); |
| }; |
| |
| |
| /** |
| * Gets the value of a DOM attribute in deterministic way. |
| * @param {!Node} node A node. |
| * @param {string} name Attribute name. |
| * @return {*} Attribute value. |
| * @private |
| */ |
| goog.testing.dom.getAttributeValue_ = function(node, name) { |
| // These hacks avoid nondetermistic results in the following cases: |
| // IE7: document.createElement('input').height returns a random number. |
| // FF3: getAttribute('disabled') returns different value for <div disabled=""> |
| // and <div disabled="disabled"> |
| // WebKit: Two radio buttons with the same name can't be checked at the same |
| // time, even if only one of them is in the document. |
| if (goog.userAgent.WEBKIT && node.tagName == 'INPUT' && |
| node['type'] == 'radio' && name == 'checked') { |
| return false; |
| } |
| return goog.isDef(node[name]) && |
| typeof node.getAttribute(name) != typeof node[name] ? |
| node[name] : node.getAttribute(name); |
| }; |
| |
| |
| /** |
| * Assert that the attributes of two Nodes are the same (ignoring any |
| * instances of the style attribute). |
| * @param {string} errorSuffix String to add to end of error messages. |
| * @param {!Element} expectedElem The element whose attributes we are expecting. |
| * @param {!Element} actualElem The element with the actual attributes. |
| * @param {boolean} strictAttributes If false, attributes that appear in |
| * expectedNode must also be in actualNode, but actualNode can have |
| * attributes not present in expectedNode. If true, expectedNode and |
| * actualNode must have the same set of attributes. |
| * @private |
| */ |
| goog.testing.dom.assertAttributesEqual_ = function(errorSuffix, |
| expectedElem, actualElem, strictAttributes) { |
| if (strictAttributes) { |
| goog.testing.dom.compareClassAttribute_(expectedElem, actualElem); |
| } |
| |
| var expectedAttributes = expectedElem.attributes; |
| var actualAttributes = actualElem.attributes; |
| |
| for (var i = 0, len = expectedAttributes.length; i < len; i++) { |
| var expectedName = expectedAttributes[i].name; |
| var expectedValue = goog.testing.dom.getAttributeValue_(expectedElem, |
| expectedName); |
| |
| var actualAttribute = actualAttributes[expectedName]; |
| var actualValue = goog.testing.dom.getAttributeValue_(actualElem, |
| expectedName); |
| |
| // IE enumerates attribute names in the expected node that are not present, |
| // causing an undefined actualAttribute. |
| if (!expectedValue && !actualValue) { |
| continue; |
| } |
| |
| if (expectedName == 'id' && goog.userAgent.IE) { |
| goog.testing.dom.compareIdAttributeForIe_( |
| /** @type {string} */ (expectedValue), actualAttribute, |
| strictAttributes, errorSuffix); |
| continue; |
| } |
| |
| if (goog.testing.dom.ignoreAttribute_(expectedName)) { |
| continue; |
| } |
| |
| assertNotUndefined('Expected to find attribute with name ' + |
| expectedName + ', in element ' + |
| goog.testing.dom.describeNode_(actualElem) + errorSuffix, |
| actualAttribute); |
| assertEquals('Expected attribute ' + expectedName + |
| ' has a different value ' + errorSuffix, |
| expectedValue, |
| goog.testing.dom.getAttributeValue_(actualElem, actualAttribute.name)); |
| } |
| |
| if (strictAttributes) { |
| for (i = 0; i < actualAttributes.length; i++) { |
| var actualName = actualAttributes[i].name; |
| var actualAttribute = actualAttributes.getNamedItem(actualName); |
| |
| if (!actualAttribute || goog.testing.dom.ignoreAttribute_(actualName)) { |
| continue; |
| } |
| |
| assertNotUndefined('Unexpected attribute with name ' + |
| actualName + ' in element ' + |
| goog.testing.dom.describeNode_(actualElem) + errorSuffix, |
| expectedAttributes[actualName]); |
| } |
| } |
| }; |
| |
| |
| /** |
| * Assert the class attribute of actualElem is the same as the one in |
| * expectedElem, ignoring classes that are useragents. |
| * @param {!Element} expectedElem The DOM element whose class we expect. |
| * @param {!Element} actualElem The DOM element with the actual class. |
| * @private |
| */ |
| goog.testing.dom.compareClassAttribute_ = function(expectedElem, |
| actualElem) { |
| var classes = goog.dom.classlist.get(expectedElem); |
| |
| var expectedClasses = []; |
| for (var i = 0, len = classes.length; i < len; i++) { |
| if (!(classes[i] in goog.userAgent)) { |
| expectedClasses.push(classes[i]); |
| } |
| } |
| expectedClasses.sort(); |
| |
| var actualClasses = goog.array.toArray(goog.dom.classlist.get(actualElem)); |
| actualClasses.sort(); |
| |
| assertArrayEquals( |
| 'Expected class was: ' + expectedClasses.join(' ') + |
| ', but actual class was: ' + actualElem.className, |
| expectedClasses, actualClasses); |
| }; |
| |
| |
| /** |
| * Set of attributes IE adds to elements randomly. |
| * @type {Object} |
| * @private |
| */ |
| goog.testing.dom.BAD_IE_ATTRIBUTES_ = goog.object.createSet( |
| 'methods', 'CHECKED', 'dataFld', 'dataFormatAs', 'dataSrc'); |
| |
| |
| /** |
| * Whether to ignore the attribute. |
| * @param {string} name Name of the attribute. |
| * @return {boolean} True if the attribute should be ignored. |
| * @private |
| */ |
| goog.testing.dom.ignoreAttribute_ = function(name) { |
| if (name == 'style' || name == 'class') { |
| return true; |
| } |
| return goog.userAgent.IE && goog.testing.dom.BAD_IE_ATTRIBUTES_[name]; |
| }; |
| |
| |
| /** |
| * Compare id attributes for IE. In IE, if an element lacks an id attribute |
| * in the original HTML, the element object will still have such an attribute, |
| * but its value will be the empty string. |
| * @param {string} expectedValue The expected value of the id attribute. |
| * @param {Attr} actualAttribute The actual id attribute. |
| * @param {boolean} strictAttributes Whether strict attribute checking should be |
| * done. |
| * @param {string} errorSuffix String to append to error messages. |
| * @private |
| */ |
| goog.testing.dom.compareIdAttributeForIe_ = function(expectedValue, |
| actualAttribute, strictAttributes, errorSuffix) { |
| if (expectedValue === '') { |
| if (strictAttributes) { |
| assertTrue('Unexpected attribute with name id in element ' + |
| errorSuffix, actualAttribute.value == ''); |
| } |
| } else { |
| assertNotUndefined('Expected to find attribute with name id, in element ' + |
| errorSuffix, actualAttribute); |
| assertNotEquals('Expected to find attribute with name id, in element ' + |
| errorSuffix, '', actualAttribute.value); |
| assertEquals('Expected attribute has a different value ' + errorSuffix, |
| expectedValue, actualAttribute.value); |
| } |
| }; |