dom.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. // Copyright 2009 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Testing utilities for editor specific DOM related tests.
  16. *
  17. */
  18. goog.setTestOnly('goog.testing.editor.dom');
  19. goog.provide('goog.testing.editor.dom');
  20. goog.require('goog.dom.NodeType');
  21. goog.require('goog.dom.TagIterator');
  22. goog.require('goog.dom.TagWalkType');
  23. goog.require('goog.iter');
  24. goog.require('goog.string');
  25. goog.require('goog.testing.asserts');
  26. /**
  27. * Returns the previous (in document order) node from the given node that is a
  28. * non-empty text node, or null if none is found or opt_stopAt is not an
  29. * ancestor of node. Note that if the given node has children, the search will
  30. * start from the end tag of the node, meaning all its descendants will be
  31. * included in the search, unless opt_skipDescendants is true.
  32. * @param {Node} node Node to start searching from.
  33. * @param {Node=} opt_stopAt Node to stop searching at (search will be
  34. * restricted to this node's subtree), defaults to the body of the document
  35. * containing node.
  36. * @param {boolean=} opt_skipDescendants Whether to skip searching the given
  37. * node's descentants.
  38. * @return {Text} The previous (in document order) node from the given node
  39. * that is a non-empty text node, or null if none is found.
  40. */
  41. goog.testing.editor.dom.getPreviousNonEmptyTextNode = function(
  42. node, opt_stopAt, opt_skipDescendants) {
  43. return goog.testing.editor.dom.getPreviousNextNonEmptyTextNodeHelper_(
  44. node, opt_stopAt, opt_skipDescendants, true);
  45. };
  46. /**
  47. * Returns the next (in document order) node from the given node that is a
  48. * non-empty text node, or null if none is found or opt_stopAt is not an
  49. * ancestor of node. Note that if the given node has children, the search will
  50. * start from the start tag of the node, meaning all its descendants will be
  51. * included in the search, unless opt_skipDescendants is true.
  52. * @param {Node} node Node to start searching from.
  53. * @param {Node=} opt_stopAt Node to stop searching at (search will be
  54. * restricted to this node's subtree), defaults to the body of the document
  55. * containing node.
  56. * @param {boolean=} opt_skipDescendants Whether to skip searching the given
  57. * node's descentants.
  58. * @return {Text} The next (in document order) node from the given node that
  59. * is a non-empty text node, or null if none is found or opt_stopAt is not
  60. * an ancestor of node.
  61. */
  62. goog.testing.editor.dom.getNextNonEmptyTextNode = function(
  63. node, opt_stopAt, opt_skipDescendants) {
  64. return goog.testing.editor.dom.getPreviousNextNonEmptyTextNodeHelper_(
  65. node, opt_stopAt, opt_skipDescendants, false);
  66. };
  67. /**
  68. * Helper that returns the previous or next (in document order) node from the
  69. * given node that is a non-empty text node, or null if none is found or
  70. * opt_stopAt is not an ancestor of node. Note that if the given node has
  71. * children, the search will start from the end or start tag of the node
  72. * (depending on whether it's searching for the previous or next node), meaning
  73. * all its descendants will be included in the search, unless
  74. * opt_skipDescendants is true.
  75. * @param {Node} node Node to start searching from.
  76. * @param {Node=} opt_stopAt Node to stop searching at (search will be
  77. * restricted to this node's subtree), defaults to the body of the document
  78. * containing node.
  79. * @param {boolean=} opt_skipDescendants Whether to skip searching the given
  80. * node's descentants.
  81. * @param {boolean=} opt_isPrevious Whether to search for the previous non-empty
  82. * text node instead of the next one.
  83. * @return {Text} The next (in document order) node from the given node that
  84. * is a non-empty text node, or null if none is found or opt_stopAt is not
  85. * an ancestor of node.
  86. * @private
  87. */
  88. goog.testing.editor.dom.getPreviousNextNonEmptyTextNodeHelper_ = function(
  89. node, opt_stopAt, opt_skipDescendants, opt_isPrevious) {
  90. opt_stopAt = opt_stopAt || node.ownerDocument.body;
  91. // Initializing the iterator to iterate over the children of opt_stopAt
  92. // makes it stop only when it finishes iterating through all of that
  93. // node's children, even though we will start at a different node and exit
  94. // that starting node's subtree in the process.
  95. var iter = new goog.dom.TagIterator(opt_stopAt, opt_isPrevious);
  96. // TODO(user): Move this logic to a new method in TagIterator such as
  97. // skipToNode().
  98. // Then we set the iterator to start at the given start node, not opt_stopAt.
  99. var walkType; // Let TagIterator set the initial walk type by default.
  100. var depth = goog.testing.editor.dom.getRelativeDepth_(node, opt_stopAt);
  101. if (depth == -1) {
  102. return null; // Fail because opt_stopAt is not an ancestor of node.
  103. }
  104. if (node.nodeType == goog.dom.NodeType.ELEMENT) {
  105. if (opt_skipDescendants) {
  106. // Specifically set the initial walk type so that we skip the descendant
  107. // subtree by starting at the start if going backwards or at the end if
  108. // going forwards.
  109. walkType = opt_isPrevious ? goog.dom.TagWalkType.START_TAG :
  110. goog.dom.TagWalkType.END_TAG;
  111. } else {
  112. // We're starting "inside" an element node so the depth needs to be one
  113. // deeper than the node's actual depth. That's how TagIterator works!
  114. depth++;
  115. }
  116. }
  117. iter.setPosition(node, walkType, depth);
  118. // Advance the iterator so it skips the start node.
  119. try {
  120. iter.next();
  121. } catch (e) {
  122. return null; // It could have been a leaf node.
  123. }
  124. // Now just get the first non-empty text node the iterator finds.
  125. var filter =
  126. goog.iter.filter(iter, goog.testing.editor.dom.isNonEmptyTextNode_);
  127. try {
  128. return /** @type {Text} */ (filter.next());
  129. } catch (e) { // No next item is available so return null.
  130. return null;
  131. }
  132. };
  133. /**
  134. * Returns whether the given node is a non-empty text node.
  135. * @param {Node} node Node to be checked.
  136. * @return {boolean} Whether the given node is a non-empty text node.
  137. * @private
  138. */
  139. goog.testing.editor.dom.isNonEmptyTextNode_ = function(node) {
  140. return !!node && node.nodeType == goog.dom.NodeType.TEXT && node.length > 0;
  141. };
  142. /**
  143. * Returns the depth of the given node relative to the given parent node, or -1
  144. * if the given node is not a descendant of the given parent node. E.g. if
  145. * node == parentNode returns 0, if node.parentNode == parentNode returns 1,
  146. * etc.
  147. * @param {Node} node Node whose depth to get.
  148. * @param {Node} parentNode Node relative to which the depth should be
  149. * calculated.
  150. * @return {number} The depth of the given node relative to the given parent
  151. * node, or -1 if the given node is not a descendant of the given parent
  152. * node.
  153. * @private
  154. */
  155. goog.testing.editor.dom.getRelativeDepth_ = function(node, parentNode) {
  156. var depth = 0;
  157. while (node) {
  158. if (node == parentNode) {
  159. return depth;
  160. }
  161. node = node.parentNode;
  162. depth++;
  163. }
  164. return -1;
  165. };
  166. /**
  167. * Assert that the range is surrounded by the given strings. This is useful
  168. * because different browsers can place the range endpoints inside different
  169. * nodes even when visually the range looks the same. Also, there may be empty
  170. * text nodes in the way (again depending on the browser) making it difficult to
  171. * use assertRangeEquals.
  172. * @param {string} before String that should occur immediately before the start
  173. * point of the range. If this is the empty string, assert will only succeed
  174. * if there is no text before the start point of the range.
  175. * @param {string} after String that should occur immediately after the end
  176. * point of the range. If this is the empty string, assert will only succeed
  177. * if there is no text after the end point of the range.
  178. * @param {goog.dom.AbstractRange} range The range to be tested.
  179. * @param {Node=} opt_stopAt Node to stop searching at (search will be
  180. * restricted to this node's subtree).
  181. */
  182. goog.testing.editor.dom.assertRangeBetweenText = function(
  183. before, after, range, opt_stopAt) {
  184. var previousText =
  185. goog.testing.editor.dom.getTextFollowingRange_(range, true, opt_stopAt);
  186. if (before == '') {
  187. assertNull(
  188. 'Expected nothing before range but found <' + previousText + '>',
  189. previousText);
  190. } else {
  191. assertNotNull(
  192. 'Expected <' + before + '> before range but found nothing',
  193. previousText);
  194. assertTrue(
  195. 'Expected <' + before + '> before range but found <' + previousText +
  196. '>',
  197. goog.string.endsWith(
  198. /** @type {string} */ (previousText), before));
  199. }
  200. var nextText =
  201. goog.testing.editor.dom.getTextFollowingRange_(range, false, opt_stopAt);
  202. if (after == '') {
  203. assertNull(
  204. 'Expected nothing after range but found <' + nextText + '>', nextText);
  205. } else {
  206. assertNotNull(
  207. 'Expected <' + after + '> after range but found nothing', nextText);
  208. assertTrue(
  209. 'Expected <' + after + '> after range but found <' + nextText + '>',
  210. goog.string.startsWith(
  211. /** @type {string} */ (nextText), after));
  212. }
  213. };
  214. /**
  215. * Returns the text that follows the given range, where the term "follows" means
  216. * "comes immediately before the start of the range" if isBefore is true, and
  217. * "comes immediately after the end of the range" if isBefore is false, or null
  218. * if no non-empty text node is found.
  219. * @param {goog.dom.AbstractRange} range The range to search from.
  220. * @param {boolean} isBefore Whether to search before the range instead of
  221. * after it.
  222. * @param {Node=} opt_stopAt Node to stop searching at (search will be
  223. * restricted to this node's subtree).
  224. * @return {?string} The text that follows the given range, or null if no
  225. * non-empty text node is found.
  226. * @private
  227. */
  228. goog.testing.editor.dom.getTextFollowingRange_ = function(
  229. range, isBefore, opt_stopAt) {
  230. var followingTextNode;
  231. var endpointNode = isBefore ? range.getStartNode() : range.getEndNode();
  232. var endpointOffset = isBefore ? range.getStartOffset() : range.getEndOffset();
  233. var getFollowingTextNode = isBefore ?
  234. goog.testing.editor.dom.getPreviousNonEmptyTextNode :
  235. goog.testing.editor.dom.getNextNonEmptyTextNode;
  236. if (endpointNode.nodeType == goog.dom.NodeType.TEXT) {
  237. // Range endpoint is in a text node.
  238. var endText = endpointNode.nodeValue;
  239. if (isBefore ? endpointOffset > 0 : endpointOffset < endText.length) {
  240. // There is text in this node following the endpoint so return the portion
  241. // that follows the endpoint.
  242. return isBefore ? endText.substr(0, endpointOffset) :
  243. endText.substr(endpointOffset);
  244. } else {
  245. // There is no text following the endpoint so look for the follwing text
  246. // node.
  247. followingTextNode = getFollowingTextNode(endpointNode, opt_stopAt);
  248. return followingTextNode && followingTextNode.nodeValue;
  249. }
  250. } else {
  251. // Range endpoint is in an element node.
  252. var numChildren = endpointNode.childNodes.length;
  253. if (isBefore ? endpointOffset > 0 : endpointOffset < numChildren) {
  254. // There is at least one child following the endpoint.
  255. var followingChild =
  256. endpointNode
  257. .childNodes[isBefore ? endpointOffset - 1 : endpointOffset];
  258. if (goog.testing.editor.dom.isNonEmptyTextNode_(followingChild)) {
  259. // The following child has text so return that.
  260. return followingChild.nodeValue;
  261. } else {
  262. // The following child has no text so look for the following text node.
  263. followingTextNode = getFollowingTextNode(followingChild, opt_stopAt);
  264. return followingTextNode && followingTextNode.nodeValue;
  265. }
  266. } else {
  267. // There is no child following the endpoint, so search from the endpoint
  268. // node, but don't search its children because they are not following the
  269. // endpoint!
  270. followingTextNode = getFollowingTextNode(endpointNode, opt_stopAt, true);
  271. return followingTextNode && followingTextNode.nodeValue;
  272. }
  273. }
  274. };