enterhandler_test.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770
  1. // Copyright 2008 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. goog.provide('goog.editor.plugins.EnterHandlerTest');
  15. goog.setTestOnly('goog.editor.plugins.EnterHandlerTest');
  16. goog.require('goog.dom');
  17. goog.require('goog.dom.NodeType');
  18. goog.require('goog.dom.Range');
  19. goog.require('goog.dom.TagName');
  20. goog.require('goog.editor.BrowserFeature');
  21. goog.require('goog.editor.Field');
  22. goog.require('goog.editor.Plugin');
  23. goog.require('goog.editor.plugins.Blockquote');
  24. goog.require('goog.editor.plugins.EnterHandler');
  25. goog.require('goog.editor.range');
  26. goog.require('goog.events');
  27. goog.require('goog.events.KeyCodes');
  28. goog.require('goog.testing.ExpectedFailures');
  29. goog.require('goog.testing.MockClock');
  30. goog.require('goog.testing.dom');
  31. goog.require('goog.testing.editor.TestHelper');
  32. goog.require('goog.testing.events');
  33. goog.require('goog.testing.jsunit');
  34. goog.require('goog.userAgent');
  35. var savedHtml;
  36. var field1;
  37. var field2;
  38. var firedDelayedChange;
  39. var firedBeforeChange;
  40. var clock;
  41. var container;
  42. var EXPECTEDFAILURES;
  43. function setUpPage() {
  44. container = goog.dom.getElement('container');
  45. }
  46. function setUp() {
  47. EXPECTEDFAILURES = new goog.testing.ExpectedFailures();
  48. savedHtml = goog.dom.getElement('root').innerHTML;
  49. clock = new goog.testing.MockClock(true);
  50. }
  51. function setUpFields(classnameRequiredToSplitBlockquote) {
  52. field1 = makeField('field1', classnameRequiredToSplitBlockquote);
  53. field2 = makeField('field2', classnameRequiredToSplitBlockquote);
  54. field1.makeEditable();
  55. field2.makeEditable();
  56. }
  57. function tearDown() {
  58. clock.dispose();
  59. EXPECTEDFAILURES.handleTearDown();
  60. goog.dom.getElement('root').innerHTML = savedHtml;
  61. }
  62. function testEnterInNonSetupBlockquote() {
  63. setUpFields(true);
  64. resetChangeFlags();
  65. var prevented = !selectNodeAndHitEnter(field1, 'field1cursor');
  66. waitForChangeEvents();
  67. assertChangeFlags();
  68. // make sure there's just one blockquote, and that the text has been deleted.
  69. var elem = field1.getElement();
  70. var dom = field1.getEditableDomHelper();
  71. EXPECTEDFAILURES.expectFailureFor(
  72. goog.userAgent.OPERA,
  73. 'The blockquote is overwritten with DIV due to CORE-22104 -- Opera ' +
  74. 'overwrites the BLOCKQUOTE ancestor with DIV when doing FormatBlock ' +
  75. 'for DIV');
  76. try {
  77. assertEquals(
  78. 'Blockquote should not be split', 1,
  79. dom.getElementsByTagNameAndClass(
  80. goog.dom.TagName.BLOCKQUOTE, null, elem)
  81. .length);
  82. } catch (e) {
  83. EXPECTEDFAILURES.handleException(e);
  84. }
  85. assert(
  86. 'Selection should be deleted', -1 == elem.innerHTML.indexOf('selection'));
  87. assertEquals(
  88. 'The event should have been prevented only on webkit', prevented,
  89. goog.userAgent.WEBKIT);
  90. }
  91. function testEnterInSetupBlockquote() {
  92. setUpFields(true);
  93. resetChangeFlags();
  94. var prevented = !selectNodeAndHitEnter(field2, 'field2cursor');
  95. waitForChangeEvents();
  96. assertChangeFlags();
  97. // make sure there are two blockquotes, and a DIV with nbsp in the middle.
  98. var elem = field2.getElement();
  99. var dom = field2.getEditableDomHelper();
  100. assertEquals(
  101. 'Blockquote should be split', 2,
  102. dom.getElementsByTagNameAndClass(goog.dom.TagName.BLOCKQUOTE, null, elem)
  103. .length);
  104. assert(
  105. 'Selection should be deleted', -1 == elem.innerHTML.indexOf('selection'));
  106. assert(
  107. 'should have div with  ',
  108. -1 != elem.innerHTML.indexOf('>' + getNbsp() + '<'));
  109. assert('event should have been prevented', prevented);
  110. }
  111. function testEnterInNonSetupBlockquoteWhenClassnameIsNotRequired() {
  112. setUpFields(false);
  113. resetChangeFlags();
  114. var prevented = !selectNodeAndHitEnter(field1, 'field1cursor');
  115. waitForChangeEvents();
  116. assertChangeFlags();
  117. // make sure there are two blockquotes, and a DIV with nbsp in the middle.
  118. var elem = field1.getElement();
  119. var dom = field1.getEditableDomHelper();
  120. assertEquals(
  121. 'Blockquote should be split', 2,
  122. dom.getElementsByTagNameAndClass(goog.dom.TagName.BLOCKQUOTE, null, elem)
  123. .length);
  124. assert(
  125. 'Selection should be deleted', -1 == elem.innerHTML.indexOf('selection'));
  126. assert(
  127. 'should have div with &nbsp;',
  128. -1 != elem.innerHTML.indexOf('>' + getNbsp() + '<'));
  129. assert('event should have been prevented', prevented);
  130. }
  131. function testEnterInBlockquoteCreatesDivInBrMode() {
  132. setUpFields(true);
  133. selectNodeAndHitEnter(field2, 'field2cursor');
  134. var elem = field2.getElement();
  135. var dom = field2.getEditableDomHelper();
  136. var firstBlockquote = dom.getElementsByTagNameAndClass(
  137. goog.dom.TagName.BLOCKQUOTE, null, elem)[0];
  138. var div = dom.getNextElementSibling(firstBlockquote);
  139. assertEquals('Element after blockquote should be a div', 'DIV', div.tagName);
  140. assertEquals(
  141. 'Element after div should be second blockquote', 'BLOCKQUOTE',
  142. dom.getNextElementSibling(div).tagName);
  143. }
  144. /**
  145. * Tests that breaking after a BR doesn't result in unnecessary newlines.
  146. * @bug 1471047
  147. */
  148. function testEnterInBlockquoteRemovesUnnecessaryBrWithCursorAfterBr() {
  149. setUpFields(true);
  150. // Assume the following HTML snippet:-
  151. // <blockquote>one<br>|two<br></blockquote>
  152. //
  153. // After enter on the cursor position without the fix, the resulting HTML
  154. // after the blockquote split was:-
  155. // <blockquote>one</blockquote>
  156. // <div>&nbsp;</div>
  157. // <blockquote><br>two<br></blockquote>
  158. //
  159. // This creates the impression on an unnecessary newline. The resulting HTML
  160. // after the fix is:-
  161. //
  162. // <blockquote>one<br></blockquote>
  163. // <div>&nbsp;</div>
  164. // <blockquote>two<br></blockquote>
  165. field1.setHtml(
  166. false, '<blockquote id="quote" class="tr_bq">one<br>' +
  167. 'two<br></blockquote>');
  168. var dom = field1.getEditableDomHelper();
  169. goog.dom.Range.createCaret(dom.getElement('quote'), 2).select();
  170. goog.testing.events.fireKeySequence(
  171. field1.getElement(), goog.events.KeyCodes.ENTER);
  172. var elem = field1.getElement();
  173. var secondBlockquote = dom.getElementsByTagNameAndClass(
  174. goog.dom.TagName.BLOCKQUOTE, null, elem)[1];
  175. assertHTMLEquals('two<br>', secondBlockquote.innerHTML);
  176. // Verifies that a blockquote split doesn't happen if it doesn't need to.
  177. field1.setHtml(
  178. false, '<blockquote class="tr_bq">one<br id="brcursor"></blockquote>');
  179. selectNodeAndHitEnter(field1, 'brcursor');
  180. assertEquals(
  181. 1,
  182. dom.getElementsByTagNameAndClass(goog.dom.TagName.BLOCKQUOTE, null, elem)
  183. .length);
  184. }
  185. /**
  186. * Tests that breaking in a text node before a BR doesn't result in unnecessary
  187. * newlines.
  188. * @bug 1471047
  189. */
  190. function testEnterInBlockquoteRemovesUnnecessaryBrWithCursorBeforeBr() {
  191. setUpFields(true);
  192. // Assume the following HTML snippet:-
  193. // <blockquote>one|<br>two<br></blockquote>
  194. //
  195. // After enter on the cursor position, the resulting HTML should be.
  196. // <blockquote>one<br></blockquote>
  197. // <div>&nbsp;</div>
  198. // <blockquote>two<br></blockquote>
  199. field1.setHtml(
  200. false, '<blockquote id="quote" class="tr_bq">one<br>' +
  201. 'two<br></blockquote>');
  202. var dom = field1.getEditableDomHelper();
  203. var cursor = dom.getElement('quote').firstChild;
  204. goog.dom.Range.createCaret(cursor, 3).select();
  205. goog.testing.events.fireKeySequence(
  206. field1.getElement(), goog.events.KeyCodes.ENTER);
  207. var elem = field1.getElement();
  208. var secondBlockquote = dom.getElementsByTagNameAndClass(
  209. goog.dom.TagName.BLOCKQUOTE, null, elem)[1];
  210. assertHTMLEquals('two<br>', secondBlockquote.innerHTML);
  211. // Ensures that standard text node split works as expected with the new
  212. // change.
  213. field1.setHtml(
  214. false, '<blockquote id="quote" class="tr_bq">one<b>two</b><br>');
  215. cursor = dom.getElement('quote').firstChild;
  216. goog.dom.Range.createCaret(cursor, 3).select();
  217. goog.testing.events.fireKeySequence(
  218. field1.getElement(), goog.events.KeyCodes.ENTER);
  219. secondBlockquote = dom.getElementsByTagNameAndClass(
  220. goog.dom.TagName.BLOCKQUOTE, null, elem)[1];
  221. assertHTMLEquals('<b>two</b><br>', secondBlockquote.innerHTML);
  222. }
  223. /**
  224. * Tests that pressing enter in a blockquote doesn't create unnecessary
  225. * DOM subtrees.
  226. *
  227. * @bug 1991539
  228. * @bug 1991392
  229. */
  230. function testEnterInBlockquoteRemovesExtraNodes() {
  231. setUpFields(true);
  232. // Let's assume we have the following DOM structure and the
  233. // cursor is placed after the first numbered list item "one".
  234. //
  235. // <blockquote class="tr_bq">
  236. // <div><div>a</div><ol><li>one|</li></div>
  237. // <div>two</div>
  238. // </blockquote>
  239. //
  240. // After pressing enter, we have the following structure.
  241. //
  242. // <blockquote class="tr_bq">
  243. // <div><div>a</div><ol><li>one|</li></div>
  244. // </blockquote>
  245. // <div>&nbsp;</div>
  246. // <blockquote class="tr_bq">
  247. // <div><ol><li><span id=""></span></li></ol></div>
  248. // <div>two</div>
  249. // </blockquote>
  250. //
  251. // This appears to the user as an empty list. After the fix, the HTML
  252. // will be
  253. //
  254. // <blockquote class="tr_bq">
  255. // <div><div>a</div><ol><li>one|</li></div>
  256. // </blockquote>
  257. // <div>&nbsp;</div>
  258. // <blockquote class="tr_bq">
  259. // <div>two</div>
  260. // </blockquote>
  261. //
  262. field1.setHtml(
  263. false, '<blockquote class="tr_bq">' +
  264. '<div><div>a</div><ol><li id="cursor">one</li></div>' +
  265. '<div>b</div>' +
  266. '</blockquote>');
  267. var dom = field1.getEditableDomHelper();
  268. goog.dom.Range.createCaret(dom.getElement('cursor').firstChild, 3).select();
  269. goog.testing.events.fireKeySequence(
  270. field1.getElement(), goog.events.KeyCodes.ENTER);
  271. var elem = field1.getElement();
  272. var secondBlockquote = dom.getElementsByTagNameAndClass(
  273. goog.dom.TagName.BLOCKQUOTE, null, elem)[1];
  274. assertHTMLEquals('<div>b</div>', secondBlockquote.innerHTML);
  275. // Ensure that we remove only unnecessary subtrees.
  276. field1.setHtml(
  277. false, '<blockquote class="tr_bq">' +
  278. '<div><span>a</span><div id="cursor">one</div><div>two</div></div>' +
  279. '<div><span>c</span></div>' +
  280. '</blockquote>');
  281. goog.dom.Range.createCaret(dom.getElement('cursor').firstChild, 3).select();
  282. goog.testing.events.fireKeySequence(
  283. field1.getElement(), goog.events.KeyCodes.ENTER);
  284. secondBlockquote = dom.getElementsByTagNameAndClass(
  285. goog.dom.TagName.BLOCKQUOTE, null, elem)[1];
  286. var expectedHTML = '<div><div>two</div></div>' +
  287. '<div><span>c</span></div>';
  288. assertHTMLEquals(expectedHTML, secondBlockquote.innerHTML);
  289. // Place the cursor in the middle of a line.
  290. field1.setHtml(
  291. false, '<blockquote id="quote" class="tr_bq">' +
  292. '<div>one</div><div>two</div>' +
  293. '</blockquote>');
  294. goog.dom.Range.createCaret(dom.getElement('quote').firstChild.firstChild, 1)
  295. .select();
  296. goog.testing.events.fireKeySequence(
  297. field1.getElement(), goog.events.KeyCodes.ENTER);
  298. var blockquotes =
  299. dom.getElementsByTagNameAndClass(goog.dom.TagName.BLOCKQUOTE, null, elem);
  300. assertEquals(2, blockquotes.length);
  301. assertHTMLEquals('<div>o</div>', blockquotes[0].innerHTML);
  302. assertHTMLEquals('<div>ne</div><div>two</div>', blockquotes[1].innerHTML);
  303. }
  304. function testEnterInList() {
  305. setUpFields(true);
  306. // <enter> in a list should *never* be handled by custom code. Lists are
  307. // just way too complicated to get right.
  308. field1.setHtml(false, '<ol><li>hi!<span id="field1cursor"></span></li></ol>');
  309. if (goog.userAgent.OPERA) {
  310. // Opera doesn't actually place the selection in the empty span
  311. // unless we add a text node first.
  312. var dom = field1.getEditableDomHelper();
  313. dom.getElement('field1cursor').appendChild(dom.createTextNode(''));
  314. }
  315. var prevented = !selectNodeAndHitEnter(field1, 'field1cursor');
  316. assertFalse('<enter> in a list should not be prevented', prevented);
  317. }
  318. function testEnterAtEndOfBlockInWebkit() {
  319. setUpFields(true);
  320. if (goog.userAgent.WEBKIT) {
  321. field1.setHtml(
  322. false, '<blockquote>hi!<span id="field1cursor"></span></blockquote>');
  323. var cursor = field1.getEditableDomHelper().getElement('field1cursor');
  324. goog.editor.range.placeCursorNextTo(cursor, false);
  325. goog.dom.removeNode(cursor);
  326. var prevented = !goog.testing.events.fireKeySequence(
  327. field1.getElement(), goog.events.KeyCodes.ENTER);
  328. waitForChangeEvents();
  329. assertChangeFlags();
  330. assert('event should have been prevented', prevented);
  331. // Make sure that the block now has two brs.
  332. var elem = field1.getElement();
  333. assertEquals(
  334. 'should have inserted two br tags: ' + elem.innerHTML, 2,
  335. goog.dom.getElementsByTagNameAndClass(goog.dom.TagName.BR, null, elem)
  336. .length);
  337. }
  338. }
  339. /**
  340. * Tests that deleting a BR that comes right before a block element works.
  341. * @bug 1471096
  342. * @bug 2056376
  343. */
  344. function testDeleteBrBeforeBlock() {
  345. setUpFields(true);
  346. // This test only works on Gecko, because it's testing for manual deletion of
  347. // BR tags, which is done only for Gecko. For other browsers we fall through
  348. // and let the browser do the delete, which can only be tested with a robot
  349. // test (see javascript/apps/editor/tests/delete_br_robot.html).
  350. if (goog.userAgent.GECKO) {
  351. field1.setHtml(false, 'one<br><br><div>two</div>');
  352. var helper = new goog.testing.editor.TestHelper(field1.getElement());
  353. helper.select(field1.getElement(), 2); // Between the two BR's.
  354. goog.testing.events.fireKeySequence(
  355. field1.getElement(), goog.events.KeyCodes.DELETE);
  356. assertEquals(
  357. 'Should have deleted exactly one <br>', 'one<br><div>two</div>',
  358. field1.getElement().innerHTML);
  359. // We test the case where the BR has a previous sibling which is not
  360. // a block level element.
  361. field1.setHtml(false, 'one<br><ul><li>two</li></ul>');
  362. helper.select(field1.getElement(), 1); // Between one and BR.
  363. goog.testing.events.fireKeySequence(
  364. field1.getElement(), goog.events.KeyCodes.DELETE);
  365. assertEquals(
  366. 'Should have deleted the <br>', 'one<ul><li>two</li></ul>',
  367. field1.getElement().innerHTML);
  368. // Verify that the cursor is placed at the end of the text node "one".
  369. var range = field1.getRange();
  370. var focusNode = range.getFocusNode();
  371. assertTrue('The selected range should be collapsed', range.isCollapsed());
  372. assertTrue(
  373. 'The focus node should be the text node "one"',
  374. focusNode.nodeType == goog.dom.NodeType.TEXT &&
  375. focusNode.data == 'one');
  376. assertEquals(
  377. 'The focus offset should be at the end of the text node "one"',
  378. focusNode.length, range.getFocusOffset());
  379. assertTrue(
  380. 'The next sibling of the focus node should be the UL',
  381. focusNode.nextSibling &&
  382. focusNode.nextSibling.tagName == goog.dom.TagName.UL);
  383. // We test the case where the previous sibling of the BR is a block
  384. // level element.
  385. field1.setHtml(false, '<div>foo</div><br><div><span>bar</span></div>');
  386. helper.select(field1.getElement(), 1); // Before the BR.
  387. goog.testing.events.fireKeySequence(
  388. field1.getElement(), goog.events.KeyCodes.DELETE);
  389. assertEquals(
  390. 'Should have deleted the <br>',
  391. '<div>foo</div><div><span>bar</span></div>',
  392. field1.getElement().innerHTML);
  393. range = field1.getRange();
  394. assertEquals(
  395. 'The selected range should be contained within the <span>',
  396. String(goog.dom.TagName.SPAN), range.getContainerElement().tagName);
  397. assertTrue('The selected range should be collapsed', range.isCollapsed());
  398. // Verify that the cursor is placed inside the span at the beginning of bar.
  399. focusNode = range.getFocusNode();
  400. assertTrue(
  401. 'The focus node should be the text node "bar"',
  402. focusNode.nodeType == goog.dom.NodeType.TEXT &&
  403. focusNode.data == 'bar');
  404. assertEquals(
  405. 'The focus offset should be at the beginning ' +
  406. 'of the text node "bar"',
  407. 0, range.getFocusOffset());
  408. // We test the case where the BR does not have a previous sibling.
  409. field1.setHtml(false, '<br><ul><li>one</li></ul>');
  410. helper.select(field1.getElement(), 0); // Before the BR.
  411. goog.testing.events.fireKeySequence(
  412. field1.getElement(), goog.events.KeyCodes.DELETE);
  413. assertEquals(
  414. 'Should have deleted the <br>', '<ul><li>one</li></ul>',
  415. field1.getElement().innerHTML);
  416. range = field1.getRange();
  417. // Verify that the cursor is placed inside the LI at the text node "one".
  418. assertEquals(
  419. 'The selected range should be contained within the <li>',
  420. String(goog.dom.TagName.LI), range.getContainerElement().tagName);
  421. assertTrue('The selected range should be collapsed', range.isCollapsed());
  422. focusNode = range.getFocusNode();
  423. assertTrue(
  424. 'The focus node should be the text node "one"',
  425. (focusNode.nodeType == goog.dom.NodeType.TEXT &&
  426. focusNode.data == 'one'));
  427. assertEquals(
  428. 'The focus offset should be at the beginning of ' +
  429. 'the text node "one"',
  430. 0, range.getFocusOffset());
  431. // Testing deleting a BR followed by a block level element and preceded
  432. // by a BR.
  433. field1.setHtml(false, '<br><br><ul><li>one</li></ul>');
  434. helper.select(field1.getElement(), 1); // Between the BR's.
  435. goog.testing.events.fireKeySequence(
  436. field1.getElement(), goog.events.KeyCodes.DELETE);
  437. assertEquals(
  438. 'Should have deleted the <br>', '<br><ul><li>one</li></ul>',
  439. field1.getElement().innerHTML);
  440. // Verify that the cursor is placed inside the LI at the text node "one".
  441. range = field1.getRange();
  442. assertEquals(
  443. 'The selected range should be contained within the <li>',
  444. String(goog.dom.TagName.LI), range.getContainerElement().tagName);
  445. assertTrue('The selected range should be collapsed', range.isCollapsed());
  446. focusNode = range.getFocusNode();
  447. assertTrue(
  448. 'The focus node should be the text node "one"',
  449. (focusNode.nodeType == goog.dom.NodeType.TEXT &&
  450. focusNode.data == 'one'));
  451. assertEquals(
  452. 'The focus offset should be at the beginning of ' +
  453. 'the text node "one"',
  454. 0, range.getFocusOffset());
  455. } // End if GECKO
  456. }
  457. /**
  458. * Tests that deleting a BR before a blockquote doesn't remove quoted text.
  459. * @bug 1471075
  460. */
  461. function testDeleteBeforeBlockquote() {
  462. setUpFields(true);
  463. if (goog.userAgent.GECKO) {
  464. field1.setHtml(
  465. false, '<br><br><div><br><blockquote>foo</blockquote></div>');
  466. var helper = new goog.testing.editor.TestHelper(field1.getElement());
  467. helper.select(field1.getElement(), 0); // Before the first BR.
  468. // Fire three deletes in quick succession.
  469. goog.testing.events.fireKeySequence(
  470. field1.getElement(), goog.events.KeyCodes.DELETE);
  471. goog.testing.events.fireKeySequence(
  472. field1.getElement(), goog.events.KeyCodes.DELETE);
  473. goog.testing.events.fireKeySequence(
  474. field1.getElement(), goog.events.KeyCodes.DELETE);
  475. assertEquals(
  476. 'Should have deleted all the <br>\'s and the blockquote ' +
  477. 'isn\'t affected',
  478. '<div><blockquote>foo</blockquote></div>',
  479. field1.getElement().innerHTML);
  480. var range = field1.getRange();
  481. assertEquals(
  482. 'The selected range should be contained within the ' +
  483. '<blockquote>',
  484. String(goog.dom.TagName.BLOCKQUOTE), range.getContainerElement().tagName);
  485. assertTrue('The selected range should be collapsed', range.isCollapsed());
  486. var focusNode = range.getFocusNode();
  487. assertTrue(
  488. 'The focus node should be the text node "foo"',
  489. (focusNode.nodeType == goog.dom.NodeType.TEXT &&
  490. focusNode.data == 'foo'));
  491. assertEquals(
  492. 'The focus offset should be at the ' +
  493. 'beginning of the text node "foo"',
  494. 0, range.getFocusOffset());
  495. }
  496. }
  497. /**
  498. * Tests that deleting a BR is working normally (that the workaround for the
  499. * bug is not causing double deletes).
  500. * @bug 1471096
  501. */
  502. function testDeleteBrNormal() {
  503. setUpFields(true);
  504. // This test only works on Gecko, because it's testing for manual deletion of
  505. // BR tags, which is done only for Gecko. For other browsers we fall through
  506. // and let the browser do the delete, which can only be tested with a robot
  507. // test (see javascript/apps/editor/tests/delete_br_robot.html).
  508. if (goog.userAgent.GECKO) {
  509. field1.setHtml(false, 'one<br><br><br>two');
  510. var helper = new goog.testing.editor.TestHelper(field1.getElement());
  511. helper.select(
  512. field1.getElement(), 2); // Between the first and second BR's.
  513. field1.getElement().focus();
  514. goog.testing.events.fireKeySequence(
  515. field1.getElement(), goog.events.KeyCodes.DELETE);
  516. assertEquals(
  517. 'Should have deleted exactly one <br>', 'one<br><br>two',
  518. field1.getElement().innerHTML);
  519. } // End if GECKO
  520. }
  521. /**
  522. * Tests that deleteCursorSelectionW3C_ correctly recognizes visually
  523. * collapsed selections in Opera even if they contain a <br>.
  524. * See the deleteCursorSelectionW3C_ comment in enterhandler.js.
  525. */
  526. function testCollapsedSelectionKeepsBrOpera() {
  527. setUpFields(true);
  528. if (goog.userAgent.OPERA) {
  529. field1.setHtml(false, '<div><br id="pleasedontdeleteme"></div>');
  530. field1.focus();
  531. goog.testing.events.fireKeySequence(
  532. field1.getElement(), goog.events.KeyCodes.ENTER);
  533. assertNotNull(
  534. 'The <br> must not have been deleted',
  535. goog.dom.getElement('pleasedontdeleteme'));
  536. }
  537. }
  538. /**
  539. * Selects the node at the given id, and simulates an ENTER keypress.
  540. * @param {goog.editor.Field} field The field with the node.
  541. * @param {string} id A DOM id.
  542. * @return {boolean} Whether preventDefault was called on the event.
  543. */
  544. function selectNodeAndHitEnter(field, id) {
  545. var dom = field.getEditableDomHelper();
  546. var cursor = dom.getElement(id);
  547. goog.dom.Range.createFromNodeContents(cursor).select();
  548. return goog.testing.events.fireKeySequence(
  549. cursor, goog.events.KeyCodes.ENTER);
  550. }
  551. /**
  552. * Creates a field with only the enter handler plugged in, for testing.
  553. * @param {string} id A DOM id.
  554. * @return {goog.editor.Field} A field.
  555. */
  556. function makeField(id, classnameRequiredToSplitBlockquote) {
  557. var field = new goog.editor.Field(id);
  558. field.registerPlugin(new goog.editor.plugins.EnterHandler());
  559. field.registerPlugin(
  560. new goog.editor.plugins.Blockquote(classnameRequiredToSplitBlockquote));
  561. goog.events.listen(
  562. field, goog.editor.Field.EventType.BEFORECHANGE, function() {
  563. // set the global flag that beforechange was fired.
  564. firedBeforeChange = true;
  565. });
  566. goog.events.listen(
  567. field, goog.editor.Field.EventType.DELAYEDCHANGE, function() {
  568. // set the global flag that delayed change was fired.
  569. firedDelayedChange = true;
  570. });
  571. return field;
  572. }
  573. /**
  574. * Reset all the global flags related to change events.
  575. */
  576. function resetChangeFlags() {
  577. waitForChangeEvents();
  578. firedBeforeChange = firedDelayedChange = false;
  579. }
  580. /**
  581. * Asserts that both change flags were fired since the last reset.
  582. */
  583. function assertChangeFlags() {
  584. assert('Beforechange should have fired', firedBeforeChange);
  585. assert('Delayedchange should have fired', firedDelayedChange);
  586. }
  587. /**
  588. * Wait for delayedchange to propagate.
  589. */
  590. function waitForChangeEvents() {
  591. clock.tick(
  592. goog.editor.Field.DELAYED_CHANGE_FREQUENCY +
  593. goog.editor.Field.CHANGE_FREQUENCY);
  594. }
  595. function getNbsp() {
  596. // On WebKit (pre-528) and Opera, &nbsp; shows up as its unicode character in
  597. // innerHTML under some circumstances.
  598. return (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('528')) ||
  599. goog.userAgent.OPERA ?
  600. '\u00a0' :
  601. '&nbsp;';
  602. }
  603. function testPrepareContent() {
  604. setUpFields(true);
  605. assertPreparedContents('hi', 'hi');
  606. assertPreparedContents(
  607. goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES ? '<br>' : '', ' ');
  608. }
  609. /**
  610. * Assert that the prepared contents matches the expected.
  611. */
  612. function assertPreparedContents(expected, original) {
  613. assertEquals(
  614. expected,
  615. field1.reduceOp_(goog.editor.Plugin.Op.PREPARE_CONTENTS_HTML, original));
  616. }
  617. // UTILITY FUNCTION TESTS.
  618. function testDeleteW3CSimple() {
  619. if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
  620. container.innerHTML = '<div>abcd</div>';
  621. var range = goog.dom.Range.createFromNodes(
  622. container.firstChild.firstChild, 1, container.firstChild.firstChild, 3);
  623. range.select();
  624. goog.editor.plugins.EnterHandler.deleteW3cRange_(range);
  625. goog.testing.dom.assertHtmlContentsMatch('<div>ad</div>', container);
  626. }
  627. }
  628. function testDeleteW3CAll() {
  629. if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
  630. container.innerHTML = '<div>abcd</div>';
  631. var range = goog.dom.Range.createFromNodes(
  632. container.firstChild.firstChild, 0, container.firstChild.firstChild, 4);
  633. range.select();
  634. goog.editor.plugins.EnterHandler.deleteW3cRange_(range);
  635. goog.testing.dom.assertHtmlContentsMatch('<div>&nbsp;</div>', container);
  636. }
  637. }
  638. function testDeleteW3CPartialEnd() {
  639. if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
  640. container.innerHTML = '<div>ab</div><div>cd</div>';
  641. var range = goog.dom.Range.createFromNodes(
  642. container.firstChild.firstChild, 1, container.lastChild.firstChild, 1);
  643. range.select();
  644. goog.editor.plugins.EnterHandler.deleteW3cRange_(range);
  645. goog.testing.dom.assertHtmlContentsMatch('<div>ad</div>', container);
  646. }
  647. }
  648. function testDeleteW3CNonPartialEnd() {
  649. if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
  650. container.innerHTML = '<div>ab</div><div>cd</div>';
  651. var range = goog.dom.Range.createFromNodes(
  652. container.firstChild.firstChild, 1, container.lastChild.firstChild, 2);
  653. range.select();
  654. goog.editor.plugins.EnterHandler.deleteW3cRange_(range);
  655. goog.testing.dom.assertHtmlContentsMatch('<div>a</div>', container);
  656. }
  657. }
  658. function testIsInOneContainer() {
  659. if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
  660. container.innerHTML = '<div><br></div>';
  661. var div = container.firstChild;
  662. var range = goog.dom.Range.createFromNodes(div, 0, div, 1);
  663. range.select();
  664. assertTrue(
  665. 'Selection must be recognized as being in one container',
  666. goog.editor.plugins.EnterHandler.isInOneContainerW3c_(range));
  667. }
  668. }
  669. function testDeletingEndNodesWithNoNewLine() {
  670. if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
  671. container.innerHTML =
  672. 'a<div>b</div><div><br></div><div>c</div><div>d</div>';
  673. var range = goog.dom.Range.createFromNodes(
  674. container.childNodes[2], 0, container.childNodes[4].childNodes[0], 1);
  675. range.select();
  676. var newRange = goog.editor.plugins.EnterHandler.deleteW3cRange_(range);
  677. goog.testing.dom.assertHtmlContentsMatch('a<div>b</div>', container);
  678. assertTrue(newRange.isCollapsed());
  679. assertEquals(container, newRange.getStartNode());
  680. assertEquals(2, newRange.getStartOffset());
  681. }
  682. }