seamlessfield_test.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  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 Trogedit unit tests for goog.editor.SeamlessField.
  16. *
  17. * @author nicksantos@google.com (Nick Santos)
  18. * @suppress {missingProperties} There are many mocks in this unit test,
  19. * and the mocks don't fit well in the type system.
  20. */
  21. /** @suppress {extraProvide} */
  22. goog.provide('goog.editor.seamlessfield_test');
  23. goog.require('goog.dom');
  24. goog.require('goog.dom.DomHelper');
  25. goog.require('goog.dom.Range');
  26. goog.require('goog.dom.TagName');
  27. goog.require('goog.editor.BrowserFeature');
  28. goog.require('goog.editor.Field');
  29. goog.require('goog.editor.SeamlessField');
  30. goog.require('goog.events');
  31. goog.require('goog.functions');
  32. goog.require('goog.style');
  33. goog.require('goog.testing.MockClock');
  34. goog.require('goog.testing.MockRange');
  35. goog.require('goog.testing.jsunit');
  36. goog.setTestOnly('seamlessfield_test');
  37. var fieldElem;
  38. var fieldElemClone;
  39. function setUp() {
  40. fieldElem = goog.dom.getElement('field');
  41. fieldElemClone = fieldElem.cloneNode(true);
  42. }
  43. function tearDown() {
  44. fieldElem.parentNode.replaceChild(fieldElemClone, fieldElem);
  45. }
  46. // the following tests check for blended iframe positioning. They really
  47. // only make sense on browsers without contentEditable.
  48. function testBlankField() {
  49. if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
  50. assertAttachSeamlessIframeSizesCorrectly(
  51. initSeamlessField(' ', {}), createSeamlessIframe());
  52. }
  53. }
  54. function testFieldWithContent() {
  55. if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
  56. assertAttachSeamlessIframeSizesCorrectly(
  57. initSeamlessField('Hi!', {}), createSeamlessIframe());
  58. }
  59. }
  60. function testFieldWithPadding() {
  61. if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
  62. assertAttachSeamlessIframeSizesCorrectly(
  63. initSeamlessField('Hi!', {'padding': '2px 5px'}),
  64. createSeamlessIframe());
  65. }
  66. }
  67. function testFieldWithMargin() {
  68. if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
  69. assertAttachSeamlessIframeSizesCorrectly(
  70. initSeamlessField('Hi!', {'margin': '2px 5px'}),
  71. createSeamlessIframe());
  72. }
  73. }
  74. function testFieldWithBorder() {
  75. if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
  76. assertAttachSeamlessIframeSizesCorrectly(
  77. initSeamlessField('Hi!', {'border': '2px 5px'}),
  78. createSeamlessIframe());
  79. }
  80. }
  81. function testFieldWithOverflow() {
  82. if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
  83. assertAttachSeamlessIframeSizesCorrectly(
  84. initSeamlessField(
  85. ['1', '2', '3', '4', '5', '6', '7'].join('<p/>'),
  86. {'overflow': 'auto', 'position': 'relative', 'height': '20px'}),
  87. createSeamlessIframe());
  88. assertEquals(20, fieldElem.offsetHeight);
  89. }
  90. }
  91. function testFieldWithOverflowAndPadding() {
  92. if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
  93. var blendedField =
  94. initSeamlessField(['1', '2', '3', '4', '5', '6', '7'].join('<p/>'), {
  95. 'overflow': 'auto',
  96. 'position': 'relative',
  97. 'height': '20px',
  98. 'padding': '2px 3px'
  99. });
  100. var blendedIframe = createSeamlessIframe();
  101. assertAttachSeamlessIframeSizesCorrectly(blendedField, blendedIframe);
  102. assertEquals(24, fieldElem.offsetHeight);
  103. }
  104. }
  105. function testIframeHeightGrowsOnWrap() {
  106. if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
  107. var clock = new goog.testing.MockClock(true);
  108. var blendedField;
  109. try {
  110. blendedField = initSeamlessField(
  111. '', {'border': '1px solid black', 'height': '20px'});
  112. blendedField.makeEditable();
  113. blendedField.setHtml(false, 'Content that should wrap after resize.');
  114. // Ensure that the field was fully loaded and sized before measuring.
  115. clock.tick(1);
  116. // Capture starting heights.
  117. var unwrappedIframeHeight = blendedField.getEditableIframe().offsetHeight;
  118. // Resize the field such that the text should wrap.
  119. fieldElem.style.width = '200px';
  120. blendedField.doFieldSizingGecko();
  121. // Iframe should grow as a result.
  122. var wrappedIframeHeight = blendedField.getEditableIframe().offsetHeight;
  123. assertTrue(
  124. 'Wrapped text should cause iframe to grow - initial height: ' +
  125. unwrappedIframeHeight + ', wrapped height: ' +
  126. wrappedIframeHeight,
  127. wrappedIframeHeight > unwrappedIframeHeight);
  128. } finally {
  129. blendedField.dispose();
  130. clock.dispose();
  131. }
  132. }
  133. }
  134. function testDispatchIframeResizedForWrapperHeight() {
  135. if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
  136. var clock = new goog.testing.MockClock(true);
  137. var blendedField = initSeamlessField('Hi!', {'border': '2px 5px'});
  138. var iframe = createSeamlessIframe();
  139. blendedField.attachIframe(iframe);
  140. var resizeCalled = false;
  141. goog.events.listenOnce(
  142. blendedField, goog.editor.Field.EventType.IFRAME_RESIZED,
  143. function() { resizeCalled = true; });
  144. try {
  145. blendedField.makeEditable();
  146. blendedField.setHtml(false, 'Content that should wrap after resize.');
  147. // Ensure that the field was fully loaded and sized before measuring.
  148. clock.tick(1);
  149. assertFalse('Iframe resize must not be dispatched yet', resizeCalled);
  150. // Resize the field such that the text should wrap.
  151. fieldElem.style.width = '200px';
  152. blendedField.sizeIframeToWrapperGecko_();
  153. assertTrue('Iframe resize must be dispatched for Wrapper', resizeCalled);
  154. } finally {
  155. blendedField.dispose();
  156. clock.dispose();
  157. }
  158. }
  159. }
  160. function testDispatchIframeResizedForBodyHeight() {
  161. if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
  162. var clock = new goog.testing.MockClock(true);
  163. var blendedField = initSeamlessField('Hi!', {'border': '2px 5px'});
  164. var iframe = createSeamlessIframe();
  165. blendedField.attachIframe(iframe);
  166. var resizeCalled = false;
  167. goog.events.listenOnce(
  168. blendedField, goog.editor.Field.EventType.IFRAME_RESIZED,
  169. function() { resizeCalled = true; });
  170. try {
  171. blendedField.makeEditable();
  172. blendedField.setHtml(false, 'Content that should wrap after resize.');
  173. // Ensure that the field was fully loaded and sized before measuring.
  174. clock.tick(1);
  175. assertFalse('Iframe resize must not be dispatched yet', resizeCalled);
  176. // Resize the field to a different body height.
  177. var bodyHeight = blendedField.getIframeBodyHeightGecko_();
  178. blendedField.getIframeBodyHeightGecko_ = function() {
  179. return bodyHeight + 1;
  180. };
  181. blendedField.sizeIframeToBodyHeightGecko_();
  182. assertTrue('Iframe resize must be dispatched for Body', resizeCalled);
  183. } finally {
  184. blendedField.dispose();
  185. clock.dispose();
  186. }
  187. }
  188. }
  189. function testDispatchBlur() {
  190. if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE &&
  191. !goog.editor.BrowserFeature.CLEARS_SELECTION_WHEN_FOCUS_LEAVES) {
  192. var blendedField = initSeamlessField('Hi!', {'border': '2px 5px'});
  193. var iframe = createSeamlessIframe();
  194. blendedField.attachIframe(iframe);
  195. var blurCalled = false;
  196. goog.events.listenOnce(
  197. blendedField, goog.editor.Field.EventType.BLUR,
  198. function() { blurCalled = true; });
  199. var clearSelection = goog.dom.Range.clearSelection;
  200. var cleared = false;
  201. var clearedWindow;
  202. blendedField.editableDomHelper = new goog.dom.DomHelper();
  203. blendedField.editableDomHelper.getWindow =
  204. goog.functions.constant(iframe.contentWindow);
  205. var mockRange = new goog.testing.MockRange();
  206. blendedField.getRange = function() { return mockRange; };
  207. goog.dom.Range.clearSelection = function(opt_window) {
  208. clearSelection(opt_window);
  209. cleared = true;
  210. clearedWindow = opt_window;
  211. };
  212. var clock = new goog.testing.MockClock(true);
  213. mockRange.collapse(true);
  214. mockRange.select();
  215. mockRange.$replay();
  216. blendedField.dispatchBlur();
  217. clock.tick(1);
  218. assertTrue('Blur must be dispatched.', blurCalled);
  219. assertTrue('Selection must be cleared.', cleared);
  220. assertEquals(
  221. 'Selection must be cleared in iframe', iframe.contentWindow,
  222. clearedWindow);
  223. mockRange.$verify();
  224. clock.dispose();
  225. }
  226. }
  227. function testSetMinHeight() {
  228. if (!goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
  229. var clock = new goog.testing.MockClock(true);
  230. try {
  231. var field = initSeamlessField(
  232. ['1', '2', '3', '4', '5', '6', '7'].join('<p/>'),
  233. {'position': 'relative', 'height': '60px'});
  234. // Initially create and size iframe.
  235. var iframe = createSeamlessIframe();
  236. field.attachIframe(iframe);
  237. field.iframeFieldLoadHandler(iframe, '', {});
  238. // Need to process timeouts set by load handlers.
  239. clock.tick(1000);
  240. var normalHeight = goog.style.getSize(iframe).height;
  241. var delayedChangeCalled = false;
  242. goog.events.listen(
  243. field, goog.editor.Field.EventType.DELAYEDCHANGE,
  244. function() { delayedChangeCalled = true; });
  245. // Test that min height is obeyed.
  246. field.setMinHeight(30);
  247. clock.tick(1000);
  248. assertEquals(
  249. 'Iframe height must match min height.', 30,
  250. goog.style.getSize(iframe).height);
  251. assertFalse(
  252. 'Setting min height must not cause delayed change event.',
  253. delayedChangeCalled);
  254. // Test that min height doesn't shrink field.
  255. field.setMinHeight(0);
  256. clock.tick(1000);
  257. assertEquals(normalHeight, goog.style.getSize(iframe).height);
  258. assertFalse(
  259. 'Setting min height must not cause delayed change event.',
  260. delayedChangeCalled);
  261. } finally {
  262. field.dispose();
  263. clock.dispose();
  264. }
  265. }
  266. }
  267. /**
  268. * @bug 1649967 This code used to throw a Javascript error.
  269. */
  270. function testSetMinHeightWithNoIframe() {
  271. if (goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
  272. try {
  273. var field = initSeamlessField('&nbsp;', {});
  274. field.makeEditable();
  275. field.setMinHeight(30);
  276. } finally {
  277. field.dispose();
  278. }
  279. }
  280. }
  281. function testStartChangeEvents() {
  282. if (goog.editor.BrowserFeature.USE_MUTATION_EVENTS) {
  283. var clock = new goog.testing.MockClock(true);
  284. try {
  285. var field = initSeamlessField('&nbsp;', {});
  286. field.makeEditable();
  287. var changeCalled = false;
  288. goog.events.listenOnce(
  289. field, goog.editor.Field.EventType.CHANGE,
  290. function() { changeCalled = true; });
  291. var delayedChangeCalled = false;
  292. goog.events.listenOnce(
  293. field, goog.editor.Field.EventType.CHANGE,
  294. function() { delayedChangeCalled = true; });
  295. field.stopChangeEvents(true, true);
  296. if (field.changeTimerGecko_) {
  297. field.changeTimerGecko_.start();
  298. }
  299. field.startChangeEvents();
  300. clock.tick(1000);
  301. assertFalse(changeCalled);
  302. assertFalse(delayedChangeCalled);
  303. } finally {
  304. clock.dispose();
  305. field.dispose();
  306. }
  307. }
  308. }
  309. function testManipulateDom() {
  310. // Test in blended field since that is what fires change events.
  311. var editableField = initSeamlessField('&nbsp;', {});
  312. var clock = new goog.testing.MockClock(true);
  313. var delayedChangeCalled = 0;
  314. goog.events.listen(
  315. editableField, goog.editor.Field.EventType.DELAYEDCHANGE,
  316. function() { delayedChangeCalled++; });
  317. assertFalse(editableField.isLoaded());
  318. editableField.manipulateDom(goog.nullFunction);
  319. clock.tick(1000);
  320. assertEquals(
  321. 'Must not fire delayed change events if field is not loaded.', 0,
  322. delayedChangeCalled);
  323. editableField.makeEditable();
  324. var usesIframe = editableField.usesIframe();
  325. try {
  326. editableField.manipulateDom(goog.nullFunction);
  327. clock.tick(1000); // Wait for delayed change to fire.
  328. assertEquals(
  329. 'By default must fire a single delayed change event.', 1,
  330. delayedChangeCalled);
  331. editableField.manipulateDom(goog.nullFunction, true);
  332. clock.tick(1000); // Wait for delayed change to fire.
  333. assertEquals(
  334. 'Must prevent all delayed change events.', 1, delayedChangeCalled);
  335. editableField.manipulateDom(function() {
  336. this.handleChange();
  337. this.handleChange();
  338. if (this.changeTimerGecko_) {
  339. this.changeTimerGecko_.fire();
  340. }
  341. this.dispatchDelayedChange_();
  342. this.delayedChangeTimer_.fire();
  343. }, false, editableField);
  344. clock.tick(1000); // Wait for delayed change to fire.
  345. assertEquals(
  346. 'Must ignore dispatch delayed change called within func.', 2,
  347. delayedChangeCalled);
  348. } finally {
  349. // Ensure we always uninstall the mock clock and dispose of everything.
  350. editableField.dispose();
  351. clock.dispose();
  352. }
  353. }
  354. function testAttachIframe() {
  355. var blendedField = initSeamlessField('Hi!', {});
  356. var iframe = createSeamlessIframe();
  357. try {
  358. blendedField.attachIframe(iframe);
  359. } catch (err) {
  360. fail('Error occurred while attaching iframe.');
  361. }
  362. }
  363. function createSeamlessIframe() {
  364. // NOTE(nicksantos): This is a reimplementation of
  365. // TR_EditableUtil.getIframeAttributes, but untangled for tests, and
  366. // specifically with what we need for blended mode.
  367. return goog.dom.createDom(
  368. goog.dom.TagName.IFRAME, {'frameBorder': '0', 'style': 'padding:0;'});
  369. }
  370. /**
  371. * Initialize a new editable field for the field id 'field', with the given
  372. * innerHTML and styles.
  373. *
  374. * @param {string} innerHTML html for the field contents.
  375. * @param {Object} styles Key-value pairs for styles on the field.
  376. * @return {goog.editor.SeamlessField} The field.
  377. */
  378. function initSeamlessField(innerHTML, styles) {
  379. var field = new goog.editor.SeamlessField('field');
  380. fieldElem.innerHTML = innerHTML;
  381. goog.style.setStyle(fieldElem, styles);
  382. return field;
  383. }
  384. /**
  385. * Make sure that the original field element for the given goog.editor.Field has
  386. * the same size before and after attaching the given iframe. If this is not
  387. * true, then the field will fidget while we're initializing the field,
  388. * and that's not what we want.
  389. *
  390. * @param {goog.editor.Field} fieldObj The field.
  391. * @param {HTMLIFrameElement} iframe The iframe.
  392. */
  393. function assertAttachSeamlessIframeSizesCorrectly(fieldObj, iframe) {
  394. var size = goog.style.getSize(fieldObj.getOriginalElement());
  395. fieldObj.attachIframe(iframe);
  396. var newSize = goog.style.getSize(fieldObj.getOriginalElement());
  397. assertEquals(size.width, newSize.width);
  398. assertEquals(size.height, newSize.height);
  399. }