| // 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.setTestOnly('goog.testing.dom'); | 
 | goog.provide('goog.testing.dom'); | 
 |  | 
 | goog.require('goog.array'); | 
 | goog.require('goog.asserts'); | 
 | goog.require('goog.dom'); | 
 | goog.require('goog.dom.InputType'); | 
 | 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'); | 
 |  | 
 | goog.forwardDeclare('goog.dom.AbstractRange'); | 
 |  | 
 |  | 
 | /** | 
 |  * @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(/^(\[\[.+\]\])?([\s\S]*)/)[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' + div.innerHTML + '\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 = 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 = 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) { | 
 |     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 = 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. | 
 |  * @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.assertHtmlMatches = function( | 
 |     htmlPattern, actual, opt_strictAttributes) { | 
 |   var div = goog.dom.createDom(goog.dom.TagName.DIV); | 
 |   div.innerHTML = actual; | 
 |  | 
 |   goog.testing.dom.assertHtmlContentsMatch( | 
 |       htmlPattern, div, opt_strictAttributes); | 
 | }; | 
 |  | 
 |  | 
 | /** | 
 |  * 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 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: goog.dom.createElement(goog.dom.TagName.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 == goog.dom.TagName.INPUT && | 
 |       node['type'] == goog.dom.InputType.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, | 
 |         String(expectedValue), String( | 
 |                                    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 + ' in node ' + | 
 |           goog.testing.dom.describeNode_(actualElem), | 
 |       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); | 
 |   } | 
 | }; |