field_test.js 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434
  1. // Copyright 2012 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 Shared tests for goog.editor.Field and ContentEditableField.
  16. * Since ContentEditableField switches many of the internal code paths in Field
  17. * (such as via usesIframe()) it's important to re-run a lot of the same tests.
  18. *
  19. * @author nicksantos@google.com (Nick Santos)
  20. * @author gboyer@google.com (Garrett Boyer)
  21. */
  22. /** @suppress {extraProvide} */
  23. goog.provide('goog.editor.field_test');
  24. goog.require('goog.array');
  25. goog.require('goog.dom');
  26. goog.require('goog.dom.Range');
  27. goog.require('goog.dom.TagName');
  28. goog.require('goog.dom.classlist');
  29. goog.require('goog.editor.BrowserFeature');
  30. goog.require('goog.editor.Field');
  31. goog.require('goog.editor.Plugin');
  32. goog.require('goog.editor.range');
  33. goog.require('goog.events');
  34. goog.require('goog.events.BrowserEvent');
  35. goog.require('goog.events.EventType');
  36. goog.require('goog.events.KeyCodes');
  37. goog.require('goog.functions');
  38. goog.require('goog.testing.LooseMock');
  39. goog.require('goog.testing.MockClock');
  40. goog.require('goog.testing.dom');
  41. goog.require('goog.testing.events');
  42. goog.require('goog.testing.events.Event');
  43. goog.require('goog.testing.recordFunction');
  44. goog.require('goog.userAgent');
  45. goog.setTestOnly('Tests for goog.editor.*Field');
  46. /** Constructor to use for creating the field. Set by the test HTML file. */
  47. var FieldConstructor;
  48. /** Hard-coded HTML for the tests. */
  49. var HTML = '<div id="testField">I am text.</div>';
  50. function setUp() {
  51. goog.dom.getElement('parent').innerHTML = HTML;
  52. assertTrue(
  53. 'FieldConstructor should be set by the test HTML file',
  54. goog.isFunction(FieldConstructor));
  55. }
  56. function tearDown() {
  57. // NOTE(nicksantos): I think IE is blowing up on this call because
  58. // it is lame. It manifests its lameness by throwing an exception.
  59. // Kudos to XT for helping me to figure this out.
  60. try {
  61. } catch (e) {
  62. }
  63. }
  64. // Tests for the plugin interface.
  65. /**
  66. * Dummy plugin for test usage.
  67. * @constructor
  68. * @extends {goog.editor.Plugin}
  69. * @final
  70. */
  71. function TestPlugin() {
  72. TestPlugin.base(this, 'constructor');
  73. this.getTrogClassId = function() { return 'TestPlugin'; };
  74. this.handleKeyDown = goog.nullFunction;
  75. this.handleKeyPress = goog.nullFunction;
  76. this.handleKeyUp = goog.nullFunction;
  77. this.handleKeyboardShortcut = goog.nullFunction;
  78. this.isSupportedCommand = goog.nullFunction;
  79. this.execCommandInternal = goog.nullFunction;
  80. this.queryCommandValue = goog.nullFunction;
  81. this.activeOnUneditableFields = goog.nullFunction;
  82. this.handleSelectionChange = goog.nullFunction;
  83. }
  84. goog.inherits(TestPlugin, goog.editor.Plugin);
  85. /**
  86. * Tests that calling registerPlugin will add the plugin to the
  87. * plugin map.
  88. */
  89. function testRegisterPlugin() {
  90. var editableField = new FieldConstructor('testField');
  91. var plugin = new TestPlugin();
  92. editableField.registerPlugin(plugin);
  93. assertEquals(
  94. 'Registered plugin must be in protected plugin map.', plugin,
  95. editableField.plugins_[plugin.getTrogClassId()]);
  96. assertEquals(
  97. 'Plugin has a keydown handler, should be in keydown map', plugin,
  98. editableField.indexedPlugins_[goog.editor.Plugin.Op.KEYDOWN][0]);
  99. assertEquals(
  100. 'Plugin has a keypress handler, should be in keypress map', plugin,
  101. editableField.indexedPlugins_[goog.editor.Plugin.Op.KEYPRESS][0]);
  102. assertEquals(
  103. 'Plugin has a keyup handler, should be in keuup map', plugin,
  104. editableField.indexedPlugins_[goog.editor.Plugin.Op.KEYUP][0]);
  105. assertEquals(
  106. 'Plugin has a selectionchange handler, should be in selectionchange map',
  107. plugin,
  108. editableField.indexedPlugins_[goog.editor.Plugin.Op.SELECTION][0]);
  109. assertEquals(
  110. 'Plugin has a shortcut handler, should be in shortcut map', plugin,
  111. editableField.indexedPlugins_[goog.editor.Plugin.Op.SHORTCUT][0]);
  112. assertEquals(
  113. 'Plugin has a execCommand, should be in execCommand map', plugin,
  114. editableField.indexedPlugins_[goog.editor.Plugin.Op.EXEC_COMMAND][0]);
  115. assertEquals(
  116. 'Plugin has a queryCommand, should be in queryCommand map', plugin,
  117. editableField.indexedPlugins_[goog.editor.Plugin.Op.QUERY_COMMAND][0]);
  118. assertEquals(
  119. 'Plugin does not have a prepareContentsHtml,' +
  120. 'should not be in prepareContentsHtml map',
  121. undefined,
  122. editableField.indexedPlugins_[goog.editor.Plugin.Op.PREPARE_CONTENTS_HTML]
  123. [0]);
  124. assertEquals(
  125. 'Plugin does not have a cleanContentsDom,' +
  126. 'should not be in cleanContentsDom map',
  127. undefined,
  128. editableField.indexedPlugins_[goog.editor.Plugin.Op.CLEAN_CONTENTS_DOM]
  129. [0]);
  130. assertEquals(
  131. 'Plugin does not have a cleanContentsHtml,' +
  132. 'should not be in cleanContentsHtml map',
  133. undefined,
  134. editableField.indexedPlugins_[goog.editor.Plugin.Op.CLEAN_CONTENTS_HTML]
  135. [0]);
  136. editableField.dispose();
  137. }
  138. /**
  139. * Tests that calling unregisterPlugin will remove the plugin from
  140. * the map.
  141. */
  142. function testUnregisterPlugin() {
  143. var editableField = new FieldConstructor('testField');
  144. var plugin = new TestPlugin();
  145. editableField.registerPlugin(plugin);
  146. editableField.unregisterPlugin(plugin);
  147. assertUndefined(
  148. 'Unregistered plugin must not be in protected plugin map.',
  149. editableField.plugins_[plugin.getTrogClassId()]);
  150. editableField.dispose();
  151. }
  152. /**
  153. * Tests that registered plugins can be fetched by their id.
  154. */
  155. function testGetPluginByClassId() {
  156. var editableField = new FieldConstructor('testField');
  157. var plugin = new TestPlugin();
  158. assertUndefined(
  159. 'Must not be able to get unregistered plugins by class id.',
  160. editableField.getPluginByClassId(plugin.getTrogClassId()));
  161. editableField.registerPlugin(plugin);
  162. assertEquals(
  163. 'Must be able to get registered plugins by class id.', plugin,
  164. editableField.getPluginByClassId(plugin.getTrogClassId()));
  165. editableField.dispose();
  166. }
  167. /**
  168. * Tests that plugins get auto disposed by default when the field is disposed.
  169. * Tests that plugins with setAutoDispose(false) do not get disposed when the
  170. * field is disposed.
  171. */
  172. function testDisposed_PluginAutoDispose() {
  173. var editableField = new FieldConstructor('testField');
  174. var plugin = new TestPlugin();
  175. var noDisposePlugin = new goog.editor.Plugin();
  176. noDisposePlugin.getTrogClassId = function() { return 'noDisposeId'; };
  177. noDisposePlugin.setAutoDispose(false);
  178. editableField.registerPlugin(plugin);
  179. editableField.registerPlugin(noDisposePlugin);
  180. editableField.dispose();
  181. assert(editableField.isDisposed());
  182. assertTrue(plugin.isDisposed());
  183. assertFalse(noDisposePlugin.isDisposed());
  184. }
  185. var STRING_KEY = String.fromCharCode(goog.events.KeyCodes.A).toLowerCase();
  186. /**
  187. * @return {goog.events.Event} Returns an event for a keyboard shortcut
  188. * for the letter 'a'.
  189. */
  190. function getBrowserEvent() {
  191. var e = new goog.events.BrowserEvent();
  192. e.ctrlKey = true;
  193. e.metaKey = true;
  194. e.charCode = goog.events.KeyCodes.A;
  195. return e;
  196. }
  197. /**
  198. * @param {boolean} followLinkInNewWindow Whether activating a hyperlink
  199. * in the editable field will open a new window or not.
  200. * @return {!goog.editor.Field} Returns an editable field after its load phase.
  201. */
  202. function createEditableFieldWithListeners(followLinkInNewWindow) {
  203. var editableField = new FieldConstructor('testField');
  204. editableField.setFollowLinkInNewWindow(followLinkInNewWindow);
  205. var originalElement = editableField.getOriginalElement();
  206. editableField.setupFieldObject(originalElement);
  207. editableField.handleFieldLoad();
  208. return editableField;
  209. }
  210. function getListenerTarget(editableField) {
  211. var elt = editableField.getElement();
  212. var listenerTarget = goog.editor.BrowserFeature.USE_DOCUMENT_FOR_KEY_EVENTS &&
  213. editableField.usesIframe() ?
  214. elt.ownerDocument :
  215. elt;
  216. return listenerTarget;
  217. }
  218. function assertClickDefaultActionIsCanceled(editableField) {
  219. var cancelClickDefaultActionListener = goog.events.getListener(
  220. getListenerTarget(editableField), goog.events.EventType.CLICK,
  221. goog.editor.Field.cancelLinkClick_, undefined, editableField);
  222. assertNotNull(cancelClickDefaultActionListener);
  223. }
  224. function assertClickDefaultActionIsNotCanceled(editableField) {
  225. var cancelClickDefaultActionListener = goog.events.getListener(
  226. getListenerTarget(editableField), goog.events.EventType.CLICK,
  227. goog.editor.Field.cancelLinkClick_, undefined, editableField);
  228. assertNull(cancelClickDefaultActionListener);
  229. }
  230. /**
  231. * Tests that plugins are disabled when the field is made uneditable.
  232. */
  233. function testMakeUneditableDisablesPlugins() {
  234. var editableField = new FieldConstructor('testField');
  235. var plugin = new TestPlugin();
  236. var calls = 0;
  237. plugin.disable = function(field) {
  238. assertEquals(editableField, field);
  239. assertTrue(field.isUneditable());
  240. calls++;
  241. };
  242. editableField.registerPlugin(plugin);
  243. editableField.makeEditable();
  244. assertEquals(0, calls);
  245. editableField.makeUneditable();
  246. assertEquals(1, calls);
  247. editableField.dispose();
  248. }
  249. /**
  250. * Test that if a browser open a new page when clicking a link in a content
  251. * editable element, a click listener is set to cancel this default action.
  252. */
  253. function testClickDefaultActionIsCanceledWhenBrowserFollowsClick() {
  254. // Simulate a browser that will open a new page when activating a link in a
  255. // content editable element.
  256. var editableField =
  257. createEditableFieldWithListeners(true /* followLinkInNewWindow */);
  258. assertClickDefaultActionIsCanceled(editableField);
  259. editableField.dispose();
  260. }
  261. /**
  262. * Test that if a browser does not open a new page when clicking a link in a
  263. * content editable element, the click default action is not canceled.
  264. */
  265. function testClickDefaultActionIsNotCanceledWhenBrowserDontFollowsClick() {
  266. // Simulate a browser that will NOT open a new page when activating a link in
  267. // a content editable element.
  268. var editableField =
  269. createEditableFieldWithListeners(false /* followLinkInNewWindow */);
  270. assertClickDefaultActionIsNotCanceled(editableField);
  271. editableField.dispose();
  272. }
  273. /**
  274. * Test that if a plugin registers keyup, it gets called.
  275. */
  276. function testPluginKeyUp() {
  277. var editableField = new FieldConstructor('testField');
  278. var plugin = new TestPlugin();
  279. var e = getBrowserEvent();
  280. var mockPlugin = new goog.testing.LooseMock(plugin);
  281. mockPlugin.getTrogClassId().$returns('mockPlugin');
  282. mockPlugin.registerFieldObject(editableField);
  283. mockPlugin.isEnabled(editableField).$anyTimes().$returns(true);
  284. mockPlugin.handleKeyUp(e);
  285. mockPlugin.$replay();
  286. editableField.registerPlugin(mockPlugin);
  287. editableField.handleKeyUp_(e);
  288. mockPlugin.$verify();
  289. }
  290. /**
  291. * Test that if a plugin registers keydown, it gets called.
  292. */
  293. function testPluginKeyDown() {
  294. var editableField = new FieldConstructor('testField');
  295. var plugin = new TestPlugin();
  296. var e = getBrowserEvent();
  297. var mockPlugin = new goog.testing.LooseMock(plugin);
  298. mockPlugin.getTrogClassId().$returns('mockPlugin');
  299. mockPlugin.registerFieldObject(editableField);
  300. mockPlugin.isEnabled(editableField).$anyTimes().$returns(true);
  301. mockPlugin.handleKeyDown(e).$returns(true);
  302. mockPlugin.$replay();
  303. editableField.registerPlugin(mockPlugin);
  304. editableField.handleKeyDown_(e);
  305. mockPlugin.$verify();
  306. }
  307. /**
  308. * Test that if a plugin registers keypress, it gets called.
  309. */
  310. function testPluginKeyPress() {
  311. var editableField = new FieldConstructor('testField');
  312. var plugin = new TestPlugin();
  313. var e = getBrowserEvent();
  314. var mockPlugin = new goog.testing.LooseMock(plugin);
  315. mockPlugin.getTrogClassId().$returns('mockPlugin');
  316. mockPlugin.registerFieldObject(editableField);
  317. mockPlugin.isEnabled(editableField).$anyTimes().$returns(true);
  318. mockPlugin.handleKeyPress(e).$returns(true);
  319. mockPlugin.$replay();
  320. editableField.registerPlugin(mockPlugin);
  321. editableField.handleKeyPress_(e);
  322. mockPlugin.$verify();
  323. }
  324. /**
  325. * If one plugin handles a key event, the rest of the plugins do not get their
  326. * key handlers invoked.
  327. */
  328. function testHandledKeyEvent() {
  329. var editableField = new FieldConstructor('testField');
  330. var plugin = new TestPlugin();
  331. var e = getBrowserEvent();
  332. var mockPlugin1 = new goog.testing.LooseMock(plugin);
  333. mockPlugin1.getTrogClassId().$returns('mockPlugin1');
  334. mockPlugin1.registerFieldObject(editableField);
  335. mockPlugin1.isEnabled(editableField).$anyTimes().$returns(true);
  336. if (goog.editor.BrowserFeature.USES_KEYDOWN) {
  337. mockPlugin1.handleKeyDown(e).$returns(true);
  338. } else {
  339. mockPlugin1.handleKeyPress(e).$returns(true);
  340. }
  341. mockPlugin1.handleKeyUp(e).$returns(true);
  342. mockPlugin1.$replay();
  343. var mockPlugin2 = new goog.testing.LooseMock(plugin);
  344. mockPlugin2.getTrogClassId().$returns('mockPlugin2');
  345. mockPlugin2.registerFieldObject(editableField);
  346. mockPlugin2.isEnabled(editableField).$anyTimes().$returns(true);
  347. mockPlugin2.$replay();
  348. editableField.registerPlugin(mockPlugin1);
  349. editableField.registerPlugin(mockPlugin2);
  350. editableField.handleKeyUp_(e);
  351. if (goog.editor.BrowserFeature.USES_KEYDOWN) {
  352. editableField.handleKeyDown_(e);
  353. } else {
  354. editableField.handleKeyPress_(e);
  355. }
  356. mockPlugin1.$verify();
  357. mockPlugin2.$verify();
  358. }
  359. /**
  360. * Tests to make sure the cut and paste events are not dispatched immediately.
  361. */
  362. function testHandleCutAndPasteEvents() {
  363. if (goog.editor.BrowserFeature.USE_MUTATION_EVENTS) {
  364. // Cut and paste events do not raise events at all in Mozilla.
  365. return;
  366. }
  367. var editableField = new FieldConstructor('testField');
  368. var clock = new goog.testing.MockClock(true);
  369. var delayedChanges = goog.testing.recordFunction();
  370. goog.events.listen(
  371. editableField, goog.editor.Field.EventType.DELAYEDCHANGE, delayedChanges);
  372. editableField.makeEditable();
  373. goog.testing.events.fireBrowserEvent(
  374. new goog.testing.events.Event('cut', editableField.getElement()));
  375. assertEquals(
  376. 'Cut event should be on a timer', 0, delayedChanges.getCallCount());
  377. clock.tick(1000);
  378. assertEquals(
  379. 'delayed change event should fire within 1s after cut', 1,
  380. delayedChanges.getCallCount());
  381. goog.testing.events.fireBrowserEvent(
  382. new goog.testing.events.Event('paste', editableField.getElement()));
  383. assertEquals(
  384. 'Paste event should be on a timer', 1, delayedChanges.getCallCount());
  385. clock.tick(1000);
  386. assertEquals(
  387. 'delayed change event should fire within 1s after paste', 2,
  388. delayedChanges.getCallCount());
  389. clock.dispose();
  390. editableField.dispose();
  391. }
  392. /**
  393. * If the first plugin does not handle the key event, the next plugin gets
  394. * a chance to handle it.
  395. */
  396. function testNotHandledKeyEvent() {
  397. var editableField = new FieldConstructor('testField');
  398. var plugin = new TestPlugin();
  399. var e = getBrowserEvent();
  400. var mockPlugin1 = new goog.testing.LooseMock(plugin);
  401. mockPlugin1.getTrogClassId().$returns('mockPlugin1');
  402. mockPlugin1.registerFieldObject(editableField);
  403. mockPlugin1.isEnabled(editableField).$anyTimes().$returns(true);
  404. if (goog.editor.BrowserFeature.USES_KEYDOWN) {
  405. mockPlugin1.handleKeyDown(e).$returns(false);
  406. } else {
  407. mockPlugin1.handleKeyPress(e).$returns(false);
  408. }
  409. mockPlugin1.handleKeyUp(e).$returns(false);
  410. mockPlugin1.$replay();
  411. var mockPlugin2 = new goog.testing.LooseMock(plugin);
  412. mockPlugin2.getTrogClassId().$returns('mockPlugin2');
  413. mockPlugin2.registerFieldObject(editableField);
  414. mockPlugin2.isEnabled(editableField).$anyTimes().$returns(true);
  415. if (goog.editor.BrowserFeature.USES_KEYDOWN) {
  416. mockPlugin2.handleKeyDown(e).$returns(true);
  417. } else {
  418. mockPlugin2.handleKeyPress(e).$returns(true);
  419. }
  420. mockPlugin2.handleKeyUp(e).$returns(true);
  421. mockPlugin2.$replay();
  422. editableField.registerPlugin(mockPlugin1);
  423. editableField.registerPlugin(mockPlugin2);
  424. editableField.handleKeyUp_(e);
  425. if (goog.editor.BrowserFeature.USES_KEYDOWN) {
  426. editableField.handleKeyDown_(e);
  427. } else {
  428. editableField.handleKeyPress_(e);
  429. }
  430. mockPlugin1.$verify();
  431. mockPlugin2.$verify();
  432. }
  433. /**
  434. * Make sure that handleKeyboardShortcut is called if other key handlers
  435. * return false.
  436. */
  437. function testKeyboardShortcutCalled() {
  438. var editableField = new FieldConstructor('testField');
  439. var plugin = new TestPlugin();
  440. var e = getBrowserEvent();
  441. var mockPlugin = new goog.testing.LooseMock(plugin);
  442. mockPlugin.getTrogClassId().$returns('mockPlugin');
  443. mockPlugin.registerFieldObject(editableField);
  444. mockPlugin.isEnabled(editableField).$anyTimes().$returns(true);
  445. if (goog.editor.BrowserFeature.USES_KEYDOWN) {
  446. mockPlugin.handleKeyDown(e).$returns(false);
  447. } else {
  448. mockPlugin.handleKeyPress(e).$returns(false);
  449. }
  450. mockPlugin.handleKeyboardShortcut(e, STRING_KEY, true).$returns(false);
  451. mockPlugin.$replay();
  452. editableField.registerPlugin(mockPlugin);
  453. if (goog.editor.BrowserFeature.USES_KEYDOWN) {
  454. editableField.handleKeyDown_(e);
  455. } else {
  456. editableField.handleKeyPress_(e);
  457. }
  458. mockPlugin.$verify();
  459. }
  460. /**
  461. * Make sure that handleKeyboardShortcut is not called if other key handlers
  462. * return true.
  463. */
  464. function testKeyboardShortcutNotCalled() {
  465. var editableField = new FieldConstructor('testField');
  466. var plugin = new TestPlugin();
  467. var e = getBrowserEvent();
  468. var mockPlugin = new goog.testing.LooseMock(plugin);
  469. mockPlugin.getTrogClassId().$returns('mockPlugin');
  470. mockPlugin.registerFieldObject(editableField);
  471. mockPlugin.isEnabled(editableField).$anyTimes().$returns(true);
  472. if (goog.editor.BrowserFeature.USES_KEYDOWN) {
  473. mockPlugin.handleKeyDown(e).$returns(true);
  474. } else {
  475. mockPlugin.handleKeyPress(e).$returns(true);
  476. }
  477. mockPlugin.$replay();
  478. editableField.registerPlugin(mockPlugin);
  479. if (goog.editor.BrowserFeature.USES_KEYDOWN) {
  480. editableField.handleKeyDown_(e);
  481. } else {
  482. editableField.handleKeyPress_(e);
  483. }
  484. mockPlugin.$verify();
  485. }
  486. /**
  487. * Make sure that handleKeyboardShortcut is not called if alt is pressed.
  488. * @bug 1363959
  489. */
  490. function testKeyHandlingAlt() {
  491. var editableField = new FieldConstructor('testField');
  492. var plugin = new TestPlugin();
  493. var e = getBrowserEvent();
  494. e.altKey = true;
  495. var mockPlugin = new goog.testing.LooseMock(plugin);
  496. mockPlugin.getTrogClassId().$returns('mockPlugin');
  497. mockPlugin.registerFieldObject(editableField);
  498. mockPlugin.isEnabled(editableField).$anyTimes().$returns(true);
  499. if (goog.editor.BrowserFeature.USES_KEYDOWN) {
  500. mockPlugin.handleKeyDown(e).$returns(false);
  501. } else {
  502. mockPlugin.handleKeyPress(e).$returns(false);
  503. }
  504. mockPlugin.$replay();
  505. editableField.registerPlugin(mockPlugin);
  506. if (goog.editor.BrowserFeature.USES_KEYDOWN) {
  507. editableField.handleKeyDown_(e);
  508. } else {
  509. editableField.handleKeyPress_(e);
  510. }
  511. mockPlugin.$verify();
  512. }
  513. /**
  514. * Test that if a plugin has an execCommand function, it gets called
  515. * but only for supported commands.
  516. */
  517. function testPluginExecCommand() {
  518. var plugin = new TestPlugin();
  519. var passedCommand, passedArg;
  520. plugin.execCommand = function(command, arg) {
  521. passedCommand = command;
  522. passedArg = arg;
  523. };
  524. var editableField = new FieldConstructor('testField');
  525. editableField.registerPlugin(plugin);
  526. plugin.enable(editableField);
  527. plugin.isSupportedCommand = goog.functions.constant(true);
  528. editableField.execCommand('+indent', true);
  529. // Verify that the plugin's execCommand was called with the correct
  530. // args.
  531. assertEquals('+indent', passedCommand);
  532. assertTrue(passedArg);
  533. passedCommand = null;
  534. passedArg = null;
  535. plugin.isSupportedCommand = goog.functions.constant(false);
  536. editableField.execCommand('+outdent', false);
  537. // Verify that a plugin's execCommand is not called if it isn't a supported
  538. // command.
  539. assertNull(passedCommand);
  540. assertNull(passedArg);
  541. editableField.dispose();
  542. plugin.dispose();
  543. }
  544. /**
  545. * Test that if one plugin supports execCommand, no other plugins
  546. * get a chance to handle the execComand.
  547. */
  548. function testSupportedExecCommand() {
  549. var editableField = new FieldConstructor('testField');
  550. var plugin = new TestPlugin();
  551. var mockPlugin1 = new goog.testing.LooseMock(plugin);
  552. mockPlugin1.getTrogClassId().$returns('mockPlugin1');
  553. mockPlugin1.registerFieldObject(editableField);
  554. mockPlugin1.isEnabled(editableField).$anyTimes().$returns(true);
  555. mockPlugin1.isSupportedCommand('+indent').$returns(true);
  556. mockPlugin1.execCommandInternal('+indent').$returns(true);
  557. mockPlugin1.execCommand('+indent').$does(function() {
  558. mockPlugin1.execCommandInternal('+indent');
  559. });
  560. mockPlugin1.$replay();
  561. var mockPlugin2 = new goog.testing.LooseMock(plugin);
  562. mockPlugin2.getTrogClassId().$returns('mockPlugin2');
  563. mockPlugin2.registerFieldObject(editableField);
  564. mockPlugin2.isEnabled(editableField).$anyTimes().$returns(true);
  565. mockPlugin2.$replay();
  566. editableField.registerPlugin(mockPlugin1);
  567. editableField.registerPlugin(mockPlugin2);
  568. editableField.execCommand('+indent');
  569. mockPlugin1.$verify();
  570. mockPlugin2.$verify();
  571. }
  572. /**
  573. * Test that if the first plugin does not support execCommand, the other
  574. * plugins get a chance to handle the execCommand.
  575. */
  576. function testNotSupportedExecCommand() {
  577. var editableField = new FieldConstructor('testField');
  578. var plugin = new TestPlugin();
  579. var mockPlugin1 = new goog.testing.LooseMock(plugin);
  580. mockPlugin1.getTrogClassId().$returns('mockPlugin1');
  581. mockPlugin1.registerFieldObject(editableField);
  582. mockPlugin1.isEnabled(editableField).$anyTimes().$returns(true);
  583. mockPlugin1.isSupportedCommand('+indent').$returns(false);
  584. mockPlugin1.$replay();
  585. var mockPlugin2 = new goog.testing.LooseMock(plugin);
  586. mockPlugin2.getTrogClassId().$returns('mockPlugin2');
  587. mockPlugin2.registerFieldObject(editableField);
  588. mockPlugin2.isEnabled(editableField).$anyTimes().$returns(true);
  589. mockPlugin2.isSupportedCommand('+indent').$returns(true);
  590. mockPlugin2.execCommandInternal('+indent').$returns(true);
  591. mockPlugin2.execCommand('+indent').$does(function() {
  592. mockPlugin2.execCommandInternal('+indent');
  593. });
  594. mockPlugin2.$replay();
  595. editableField.registerPlugin(mockPlugin1);
  596. editableField.registerPlugin(mockPlugin2);
  597. editableField.execCommand('+indent');
  598. mockPlugin1.$verify();
  599. mockPlugin2.$verify();
  600. }
  601. /**
  602. * Tests that if a plugin supports a command that its queryCommandValue
  603. * gets called and no further plugins can handle the queryCommandValue.
  604. */
  605. function testSupportedQueryCommand() {
  606. var editableField = new FieldConstructor('testField');
  607. var plugin = new TestPlugin();
  608. var mockPlugin1 = new goog.testing.LooseMock(plugin);
  609. mockPlugin1.getTrogClassId().$returns('mockPlugin1');
  610. mockPlugin1.registerFieldObject(editableField);
  611. mockPlugin1.isEnabled(editableField).$anyTimes().$returns(true);
  612. mockPlugin1.isSupportedCommand('+indent').$returns(true);
  613. mockPlugin1.queryCommandValue('+indent').$returns(true);
  614. mockPlugin1.activeOnUneditableFields().$returns(true);
  615. mockPlugin1.$replay();
  616. var mockPlugin2 = new goog.testing.LooseMock(plugin);
  617. mockPlugin2.getTrogClassId().$returns('mockPlugin2');
  618. mockPlugin2.registerFieldObject(editableField);
  619. mockPlugin2.isEnabled(editableField).$anyTimes().$returns(true);
  620. mockPlugin2.$replay();
  621. editableField.registerPlugin(mockPlugin1);
  622. editableField.registerPlugin(mockPlugin2);
  623. editableField.queryCommandValue('+indent');
  624. mockPlugin1.$verify();
  625. mockPlugin2.$verify();
  626. }
  627. /**
  628. * Tests that if the first plugin does not support a command that its
  629. * queryCommandValue do not get called and the next plugin can handle the
  630. * queryCommandValue.
  631. */
  632. function testNotSupportedQueryCommand() {
  633. var editableField = new FieldConstructor('testField');
  634. var plugin = new TestPlugin();
  635. var mockPlugin1 = new goog.testing.LooseMock(plugin);
  636. mockPlugin1.getTrogClassId().$returns('mockPlugin1');
  637. mockPlugin1.registerFieldObject(editableField);
  638. mockPlugin1.isEnabled(editableField).$anyTimes().$returns(true);
  639. mockPlugin1.isSupportedCommand('+indent').$returns(false);
  640. mockPlugin1.$replay();
  641. var mockPlugin2 = new goog.testing.LooseMock(plugin);
  642. mockPlugin2.getTrogClassId().$returns('mockPlugin2');
  643. mockPlugin2.registerFieldObject(editableField);
  644. mockPlugin2.isEnabled(editableField).$anyTimes().$returns(true);
  645. mockPlugin2.isSupportedCommand('+indent').$returns(true);
  646. mockPlugin2.queryCommandValue('+indent').$returns(true);
  647. mockPlugin2.activeOnUneditableFields().$returns(true);
  648. mockPlugin2.$replay();
  649. editableField.registerPlugin(mockPlugin1);
  650. editableField.registerPlugin(mockPlugin2);
  651. editableField.queryCommandValue('+indent');
  652. mockPlugin1.$verify();
  653. mockPlugin2.$verify();
  654. }
  655. /**
  656. * Tests that if a plugin handles selectionChange that it gets called and
  657. * no further plugins can handle the selectionChange.
  658. */
  659. function testHandledSelectionChange() {
  660. var editableField = new FieldConstructor('testField');
  661. var plugin = new TestPlugin();
  662. var e = getBrowserEvent();
  663. var mockPlugin1 = new goog.testing.LooseMock(plugin);
  664. mockPlugin1.getTrogClassId().$returns('mockPlugin1');
  665. mockPlugin1.registerFieldObject(editableField);
  666. mockPlugin1.isEnabled(editableField).$anyTimes().$returns(true);
  667. mockPlugin1.handleSelectionChange(e, undefined).$returns(true);
  668. mockPlugin1.$replay();
  669. var mockPlugin2 = new goog.testing.LooseMock(plugin);
  670. mockPlugin2.getTrogClassId().$returns('mockPlugin2');
  671. mockPlugin2.registerFieldObject(editableField);
  672. mockPlugin2.isEnabled(editableField).$anyTimes().$returns(true);
  673. mockPlugin2.$replay();
  674. editableField.registerPlugin(mockPlugin1);
  675. editableField.registerPlugin(mockPlugin2);
  676. editableField.dispatchSelectionChangeEvent(e);
  677. mockPlugin1.$verify();
  678. mockPlugin2.$verify();
  679. }
  680. /**
  681. * Tests that if the first plugin does not handle selectionChange that
  682. * the next plugin gets a chance to handle it.
  683. */
  684. function testNotHandledSelectionChange() {
  685. var editableField = new FieldConstructor('testField');
  686. var plugin = new TestPlugin();
  687. var e = getBrowserEvent();
  688. var mockPlugin1 = new goog.testing.LooseMock(plugin);
  689. mockPlugin1.getTrogClassId().$returns('mockPlugin1');
  690. mockPlugin1.registerFieldObject(editableField);
  691. mockPlugin1.isEnabled(editableField).$anyTimes().$returns(true);
  692. mockPlugin1.handleSelectionChange(e, undefined).$returns(false);
  693. mockPlugin1.$replay();
  694. var mockPlugin2 = new goog.testing.LooseMock(plugin);
  695. mockPlugin2.getTrogClassId().$returns('mockPlugin2');
  696. mockPlugin2.registerFieldObject(editableField);
  697. mockPlugin2.isEnabled(editableField).$anyTimes().$returns(true);
  698. mockPlugin2.handleSelectionChange(e, undefined).$returns(true);
  699. mockPlugin2.$replay();
  700. editableField.registerPlugin(mockPlugin1);
  701. editableField.registerPlugin(mockPlugin2);
  702. editableField.dispatchSelectionChangeEvent(e);
  703. mockPlugin1.$verify();
  704. mockPlugin2.$verify();
  705. }
  706. // Tests for goog.editor.Field internals.
  707. function testSelectionChange() {
  708. var editableField = new FieldConstructor('testField', document);
  709. var clock = new goog.testing.MockClock(true);
  710. var beforeSelectionChanges = goog.testing.recordFunction();
  711. goog.events.listen(
  712. editableField, goog.editor.Field.EventType.BEFORESELECTIONCHANGE,
  713. beforeSelectionChanges);
  714. var selectionChanges = goog.testing.recordFunction();
  715. goog.events.listen(
  716. editableField, goog.editor.Field.EventType.SELECTIONCHANGE,
  717. selectionChanges);
  718. editableField.makeEditable();
  719. // Emulate pressing left arrow key, this should result in a
  720. // BEFORESELECTIONCHANGE event immediately, and a SELECTIONCHANGE event after
  721. // a short timeout.
  722. editableField.handleKeyUp_({keyCode: goog.events.KeyCodes.LEFT});
  723. assertEquals(
  724. 'Before selection change should fire immediately', 1,
  725. beforeSelectionChanges.getCallCount());
  726. assertEquals(
  727. 'Selection change should be on a timer', 0,
  728. selectionChanges.getCallCount());
  729. clock.tick(1000);
  730. assertEquals(
  731. 'Selection change should fire within 1s', 1,
  732. selectionChanges.getCallCount());
  733. // Programically place cursor at start. SELECTIONCHANGE event should be fired.
  734. editableField.placeCursorAtStart();
  735. assertEquals(
  736. 'Selection change should fire', 2, selectionChanges.getCallCount());
  737. clock.dispose();
  738. editableField.dispose();
  739. }
  740. function testSelectionChangeOnMouseUp() {
  741. var fakeEvent =
  742. new goog.events.BrowserEvent({type: 'mouseup', target: 'fakeTarget'});
  743. var editableField = new FieldConstructor('testField', document);
  744. var clock = new goog.testing.MockClock(true);
  745. var beforeSelectionChanges = goog.testing.recordFunction();
  746. goog.events.listen(
  747. editableField, goog.editor.Field.EventType.BEFORESELECTIONCHANGE,
  748. beforeSelectionChanges);
  749. var selectionChanges = goog.testing.recordFunction();
  750. goog.events.listen(
  751. editableField, goog.editor.Field.EventType.SELECTIONCHANGE,
  752. selectionChanges);
  753. var plugin = new TestPlugin();
  754. plugin.handleSelectionChange = goog.testing.recordFunction();
  755. editableField.registerPlugin(plugin);
  756. editableField.makeEditable();
  757. // Emulate a mouseup event, this should result in immediate
  758. // BEFORESELECTIONCHANGE and SELECTIONCHANGE, plus a second SELECTIONCHANGE in
  759. // IE after a short timeout.
  760. editableField.handleMouseUp_(fakeEvent);
  761. assertEquals(
  762. 'Before selection change should fire immediately', 1,
  763. beforeSelectionChanges.getCallCount());
  764. assertEquals(
  765. 'Selection change should fire immediately', 1,
  766. selectionChanges.getCallCount());
  767. assertEquals(
  768. 'Plugin should have handled selection change immediately', 1,
  769. plugin.handleSelectionChange.getCallCount());
  770. assertEquals(
  771. 'Plugin should have received original browser event to handle', fakeEvent,
  772. plugin.handleSelectionChange.getLastCall().getArguments()[0]);
  773. // Pretend another plugin fired a SELECTIONCHANGE in the meantime.
  774. editableField.dispatchSelectionChangeEvent();
  775. assertEquals(
  776. 'Second selection change should fire immediately', 2,
  777. selectionChanges.getCallCount());
  778. assertEquals(
  779. 'Plugin should have handled second selection change immediately', 2,
  780. plugin.handleSelectionChange.getCallCount());
  781. var args = plugin.handleSelectionChange.getLastCall().getArguments();
  782. assertTrue(
  783. 'Plugin should not have received data from extra firing',
  784. args.length == 0 || !args[0] && (args.length == 1 || !args[1]));
  785. // Now check for the extra call in IE.
  786. clock.tick(1000);
  787. if (goog.userAgent.IE) {
  788. assertEquals(
  789. 'Additional selection change should fire within 1s', 3,
  790. selectionChanges.getCallCount());
  791. assertEquals(
  792. 'Plugin should have handled selection change within 1s', 3,
  793. plugin.handleSelectionChange.getCallCount());
  794. assertEquals(
  795. 'Plugin should have received target of original browser event',
  796. fakeEvent.target,
  797. plugin.handleSelectionChange.getLastCall().getArguments().pop());
  798. } else {
  799. assertEquals(
  800. 'No additional selection change should fire', 2,
  801. selectionChanges.getCallCount());
  802. assertEquals(
  803. 'Plugin should not have handled selection change again', 2,
  804. plugin.handleSelectionChange.getCallCount());
  805. }
  806. clock.dispose();
  807. editableField.dispose();
  808. }
  809. function testSelectionChangeBeforeUneditable() {
  810. var editableField = new FieldConstructor('testField', document);
  811. var clock = new goog.testing.MockClock(true);
  812. var selectionChanges = goog.testing.recordFunction();
  813. goog.events.listen(
  814. editableField, goog.editor.Field.EventType.SELECTIONCHANGE,
  815. selectionChanges);
  816. editableField.makeEditable();
  817. editableField.handleKeyUp_({keyCode: goog.events.KeyCodes.LEFT});
  818. assertEquals(
  819. 'Selection change should be on a timer', 0,
  820. selectionChanges.getCallCount());
  821. editableField.makeUneditable();
  822. assertEquals(
  823. 'Selection change should fire during make uneditable', 1,
  824. selectionChanges.getCallCount());
  825. clock.tick(1000);
  826. assertEquals(
  827. 'No additional selection change should fire', 1,
  828. selectionChanges.getCallCount());
  829. clock.dispose();
  830. editableField.dispose();
  831. }
  832. function testGetEditableDomHelper() {
  833. var editableField = new FieldConstructor('testField', document);
  834. assertNull(
  835. 'Before being made editable, we do not know the dom helper',
  836. editableField.getEditableDomHelper());
  837. editableField.makeEditable();
  838. assertNotNull(
  839. 'After being made editable, we know the dom helper',
  840. editableField.getEditableDomHelper());
  841. assertEquals(
  842. 'Document from domHelper should be the editable elements doc',
  843. goog.dom.getOwnerDocument(editableField.getElement()),
  844. editableField.getEditableDomHelper().getDocument());
  845. editableField.dispose();
  846. }
  847. function testQueryCommandValue() {
  848. var editableField = new FieldConstructor('testField', document);
  849. assertFalse(editableField.queryCommandValue('boo'));
  850. assertObjectEquals(
  851. {'boo': false, 'aieee': false},
  852. editableField.queryCommandValue(['boo', 'aieee']));
  853. editableField.makeEditable();
  854. assertFalse(editableField.queryCommandValue('boo'));
  855. focusFieldSync(editableField);
  856. assertNull(editableField.queryCommandValue('boo'));
  857. assertObjectEquals(
  858. {'boo': null, 'aieee': null},
  859. editableField.queryCommandValue(['boo', 'aieee']));
  860. editableField.dispose();
  861. }
  862. function focusFieldSync(field) {
  863. field.focus();
  864. // IE fires focus events async, so create a fake focus event
  865. // synchronously.
  866. goog.testing.events.fireFocusEvent(field.getElement());
  867. }
  868. function testSetHtml() {
  869. var editableField = new FieldConstructor('testField', document);
  870. var clock = new goog.testing.MockClock(true);
  871. try {
  872. var delayedChangeCalled = false;
  873. goog.events.listen(
  874. editableField, goog.editor.Field.EventType.DELAYEDCHANGE,
  875. function() { delayedChangeCalled = true; });
  876. editableField.makeEditable();
  877. clock.tick(1000);
  878. assertFalse(
  879. 'Make editable must not fire delayed change.', delayedChangeCalled);
  880. editableField.setHtml(false, 'bar', true /* Don't fire delayed change */);
  881. goog.testing.dom.assertHtmlContentsMatch('bar', editableField.getElement());
  882. clock.tick(1000);
  883. assertFalse(
  884. 'setHtml must not fire delayed change if so configured.',
  885. delayedChangeCalled);
  886. editableField.setHtml(false, 'foo', false /* Fire delayed change */);
  887. goog.testing.dom.assertHtmlContentsMatch('foo', editableField.getElement());
  888. clock.tick(1000);
  889. assertTrue(
  890. 'setHtml must fire delayed change by default', delayedChangeCalled);
  891. } finally {
  892. clock.dispose();
  893. editableField.dispose();
  894. }
  895. }
  896. /**
  897. * Helper to test that the cursor is placed at the beginning of the editable
  898. * field's contents.
  899. * @param {string=} opt_html Html to replace the test file default field
  900. * contents with.
  901. * @param {string=} opt_parentId Id of the parent of the node where the cursor
  902. * is expected to be placed. If omitted, will expect cursor to be placed in
  903. * the first child of the field element (or, if the field has no content, in
  904. * the field element itself).
  905. */
  906. function doTestPlaceCursorAtStart(opt_html, opt_parentId) {
  907. var editableField = new FieldConstructor('testField', document);
  908. editableField.makeEditable();
  909. // Initially place selection not at the start of the editable field.
  910. var textNode = editableField.getElement().firstChild;
  911. goog.dom.Range.createFromNodes(textNode, 1, textNode, 2).select();
  912. if (opt_html != null) {
  913. editableField.getElement().innerHTML = opt_html;
  914. }
  915. editableField.placeCursorAtStart();
  916. var range = editableField.getRange();
  917. assertNotNull(
  918. 'Placing the cursor should result in a range object being available',
  919. range);
  920. assertTrue('The range should be collapsed', range.isCollapsed());
  921. textNode = editableField.getElement().firstChild;
  922. // We check whether getAttribute exist because textNode may be an actual
  923. // TextNode, which does not have getAttribute.
  924. if (textNode && textNode.getAttribute &&
  925. textNode.getAttribute('_moz_editor_bogus_node')) {
  926. // At least in FF >= 6, assigning '' to innerHTML of a contentEditable
  927. // element will results in textNode being modified into:
  928. // <br _moz_editor_bogus_node="TRUE" _moz_dirty=""> instead of nulling
  929. // it. So we should null it ourself.
  930. textNode = null;
  931. }
  932. var startNode = opt_parentId ?
  933. editableField.getEditableDomHelper().getElement(opt_parentId).firstChild :
  934. textNode ? textNode : editableField.getElement();
  935. if (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('528')) {
  936. // Safari 3 seems to normalize the selection to the shallowest endpoint (in
  937. // this case the editable element) in all cases tested below. This is OK
  938. // because when you start typing it magically inserts the text at the
  939. // deepest endpoint, and even behaves as desired in the case tested by
  940. // testPlaceCursorAtStartNonImportantTextNode.
  941. startNode = editableField.getElement();
  942. }
  943. assertEquals(
  944. 'The range should start at the specified expected node', startNode,
  945. range.getStartNode());
  946. assertEquals(
  947. 'The range should start at the beginning of the node', 0,
  948. range.getStartOffset());
  949. }
  950. /**
  951. * Verify that restoreSavedRange() restores the range and sets the focus.
  952. */
  953. function testRestoreSavedRange() {
  954. var editableField = new FieldConstructor('testField', document);
  955. editableField.makeEditable();
  956. // Create another node to take the focus later.
  957. var doc = goog.dom.getOwnerDocument(editableField.getElement());
  958. var dom = goog.dom.getDomHelper(editableField.getElement());
  959. var otherElem = dom.createElement(goog.dom.TagName.DIV);
  960. otherElem.tabIndex = 1; // Make it focusable.
  961. editableField.getElement().parentNode.appendChild(otherElem);
  962. // Initially place selection not at the start of the editable field.
  963. var textNode = editableField.getElement().firstChild;
  964. var range = goog.dom.Range.createFromNodes(textNode, 1, textNode, 2);
  965. range.select();
  966. var savedRange = goog.editor.range.saveUsingNormalizedCarets(range);
  967. // Change range to be a simple cursor at start, and move focus away.
  968. editableField.placeCursorAtStart();
  969. otherElem.focus();
  970. editableField.restoreSavedRange(savedRange);
  971. // Verify that we have focus and the range is restored.
  972. assertEquals(
  973. 'Field should be focused', editableField.getElement(),
  974. goog.dom.getActiveElement(doc));
  975. var newRange = editableField.getRange();
  976. assertEquals('Range startNode', textNode, newRange.getStartNode());
  977. assertEquals('Range startOffset', 1, newRange.getStartOffset());
  978. assertEquals('Range endNode', textNode, newRange.getEndNode());
  979. assertEquals('Range endOffset', 2, newRange.getEndOffset());
  980. }
  981. function testPlaceCursorAtStart() {
  982. doTestPlaceCursorAtStart();
  983. }
  984. function testPlaceCursorAtStartEmptyField() {
  985. doTestPlaceCursorAtStart('');
  986. }
  987. function testPlaceCursorAtStartNonImportantTextNode() {
  988. doTestPlaceCursorAtStart(
  989. '\n<span id="important">important text</span>', 'important');
  990. }
  991. /**
  992. * Helper to test that the cursor is placed at the beginning of the editable
  993. * field's contents.
  994. * @param {string=} opt_html Html to replace the test file default field
  995. * contents with.
  996. * @param {string=} opt_parentId Id of the parent of the node where the cursor
  997. * is expected to be placed. If omitted, will expect cursor to be placed in
  998. * the first child of the field element (or, if the field has no content, in
  999. * the field element itself).
  1000. * @param {number=} opt_offset The offset to expect for the end position.
  1001. */
  1002. function doTestPlaceCursorAtEnd(opt_html, opt_parentId, opt_offset) {
  1003. var editableField = new FieldConstructor('testField', document);
  1004. editableField.makeEditable();
  1005. // Initially place selection not at the end of the editable field.
  1006. var textNode = editableField.getElement().firstChild;
  1007. goog.dom.Range.createFromNodes(textNode, 0, textNode, 1).select();
  1008. if (opt_html != null) {
  1009. editableField.getElement().innerHTML = opt_html;
  1010. }
  1011. editableField.placeCursorAtEnd();
  1012. var range = editableField.getRange();
  1013. assertNotNull(
  1014. 'Placing the cursor should result in a range object being available',
  1015. range);
  1016. assertTrue('The range should be collapsed', range.isCollapsed());
  1017. textNode = editableField.getElement().firstChild;
  1018. // We check whether getAttribute exist because textNode may be an actual
  1019. // TextNode, which does not have getAttribute.
  1020. var hasBogusNode = textNode && textNode.getAttribute &&
  1021. textNode.getAttribute('_moz_editor_bogus_node');
  1022. if (hasBogusNode) {
  1023. // At least in FF >= 6, assigning '' to innerHTML of a contentEditable
  1024. // element will results in textNode being modified into:
  1025. // <br _moz_editor_bogus_node="TRUE" _moz_dirty=""> instead of nulling
  1026. // it. So we should null it ourself.
  1027. textNode = null;
  1028. }
  1029. var endNode = opt_parentId ?
  1030. editableField.getEditableDomHelper().getElement(opt_parentId).lastChild :
  1031. textNode ? textNode : editableField.getElement();
  1032. assertEquals(
  1033. 'The range should end at the specified expected node', endNode,
  1034. range.getEndNode());
  1035. var offset = goog.isDefAndNotNull(opt_offset) ? opt_offset : textNode ?
  1036. endNode.nodeValue.length :
  1037. endNode.childNodes.length - 1;
  1038. if (hasBogusNode) {
  1039. assertEquals(
  1040. 'The range should end at the ending of the bogus node ' +
  1041. 'added by FF',
  1042. offset + 1, range.getEndOffset());
  1043. } else {
  1044. assertEquals(
  1045. 'The range should end at the ending of the node', offset,
  1046. range.getEndOffset());
  1047. }
  1048. }
  1049. function testPlaceCursorAtEnd() {
  1050. doTestPlaceCursorAtEnd();
  1051. }
  1052. function testPlaceCursorAtEndEmptyField() {
  1053. doTestPlaceCursorAtEnd('', null, 0);
  1054. }
  1055. function testPlaceCursorAtEndNonImportantTextNode() {
  1056. doTestPlaceCursorAtStart(
  1057. '\n<span id="important">important text</span>', 'important');
  1058. }
  1059. // Tests related to change/delayed change events.
  1060. function testClearDelayedChange() {
  1061. var clock = new goog.testing.MockClock(true);
  1062. var editableField = new FieldConstructor('testField', document);
  1063. editableField.makeEditable();
  1064. var delayedChangeCalled = false;
  1065. goog.events.listen(
  1066. editableField, goog.editor.Field.EventType.DELAYEDCHANGE,
  1067. function() { delayedChangeCalled = true; });
  1068. // Clears delayed change timer.
  1069. editableField.delayedChangeTimer_.start();
  1070. editableField.clearDelayedChange();
  1071. assertTrue(delayedChangeCalled);
  1072. if (editableField.changeTimerGecko_) {
  1073. assertFalse(editableField.changeTimerGecko_.isActive());
  1074. }
  1075. assertFalse(editableField.delayedChangeTimer_.isActive());
  1076. // Clears delayed changes caused by changeTimerGecko_
  1077. if (editableField.changeTimerGecko_) {
  1078. delayedChangeCalled = false;
  1079. editableField.changeTimerGecko_.start();
  1080. editableField.clearDelayedChange();
  1081. assertTrue(delayedChangeCalled);
  1082. if (editableField.changeTimerGecko_) {
  1083. assertFalse(editableField.changeTimerGecko_.isActive());
  1084. }
  1085. assertFalse(editableField.delayedChangeTimer_.isActive());
  1086. }
  1087. clock.dispose();
  1088. }
  1089. function testHandleChange() {
  1090. if (goog.editor.BrowserFeature.USE_MUTATION_EVENTS) {
  1091. var editableField = new FieldConstructor('testField', document);
  1092. editableField.makeEditable();
  1093. editableField.changeTimerGecko_.start();
  1094. editableField.handleChange();
  1095. assertFalse(editableField.changeTimerGecko_.isActive());
  1096. }
  1097. }
  1098. function testDispatchDelayedChange() {
  1099. var editableField = new FieldConstructor('testField', document);
  1100. editableField.makeEditable();
  1101. editableField.delayedChangeTimer_.start();
  1102. editableField.dispatchDelayedChange_();
  1103. assertFalse(editableField.delayedChangeTimer_.isActive());
  1104. }
  1105. function testHandleWindowLevelMouseUp() {
  1106. var editableField = new FieldConstructor('testField', document);
  1107. if (editableField.usesIframe()) {
  1108. // Only run this test if the editor does not use an iframe.
  1109. return;
  1110. }
  1111. editableField.setUseWindowMouseUp(true);
  1112. editableField.makeEditable();
  1113. var selectionHasFired = false;
  1114. goog.events.listenOnce(
  1115. editableField, goog.editor.Field.EventType.SELECTIONCHANGE,
  1116. function(e) { selectionHasFired = true; });
  1117. var editableElement = editableField.getElement();
  1118. var otherElement = goog.dom.createDom(goog.dom.TagName.DIV);
  1119. goog.dom.insertSiblingAfter(otherElement, document.body.lastChild);
  1120. goog.testing.events.fireMouseDownEvent(editableElement);
  1121. assertFalse(selectionHasFired);
  1122. goog.testing.events.fireMouseUpEvent(otherElement);
  1123. assertTrue(selectionHasFired);
  1124. }
  1125. function testNoHandleWindowLevelMouseUp() {
  1126. var editableField = new FieldConstructor('testField', document);
  1127. editableField.setUseWindowMouseUp(false);
  1128. editableField.makeEditable();
  1129. var selectionHasFired = false;
  1130. goog.events.listenOnce(
  1131. editableField, goog.editor.Field.EventType.SELECTIONCHANGE,
  1132. function(e) { selectionHasFired = true; });
  1133. var editableElement = editableField.getElement();
  1134. var otherElement = goog.dom.createDom(goog.dom.TagName.DIV);
  1135. goog.dom.insertSiblingAfter(otherElement, document.body.lastChild);
  1136. goog.testing.events.fireMouseDownEvent(editableElement);
  1137. assertFalse(selectionHasFired);
  1138. goog.testing.events.fireMouseUpEvent(otherElement);
  1139. assertFalse(selectionHasFired);
  1140. }
  1141. function testIsGeneratingKey() {
  1142. var regularKeyEvent = new goog.events.BrowserEvent();
  1143. regularKeyEvent.charCode = goog.events.KeyCodes.A;
  1144. var ctrlKeyEvent = new goog.events.BrowserEvent();
  1145. ctrlKeyEvent.ctrlKey = true;
  1146. ctrlKeyEvent.metaKey = true;
  1147. ctrlKeyEvent.charCode = goog.events.KeyCodes.A;
  1148. var imeKeyEvent = new goog.events.BrowserEvent();
  1149. imeKeyEvent.keyCode =
  1150. 229; // indicates from an IME - see KEYS_CAUSING_CHANGES
  1151. assertTrue(goog.editor.Field.isGeneratingKey_(regularKeyEvent, true));
  1152. assertFalse(goog.editor.Field.isGeneratingKey_(ctrlKeyEvent, true));
  1153. if (goog.userAgent.WINDOWS && !goog.userAgent.GECKO) {
  1154. assertTrue(goog.editor.Field.isGeneratingKey_(imeKeyEvent, false));
  1155. } else {
  1156. assertFalse(goog.editor.Field.isGeneratingKey_(imeKeyEvent, false));
  1157. }
  1158. }
  1159. function testSetEditableClassName() {
  1160. var element = goog.dom.getElement('testField');
  1161. var editableField = new FieldConstructor('testField');
  1162. assertFalse(goog.dom.classlist.contains(element, 'editable'));
  1163. editableField.makeEditable();
  1164. assertTrue(goog.dom.classlist.contains(element, 'editable'));
  1165. assertEquals(
  1166. 1,
  1167. goog.array.count(
  1168. goog.dom.classlist.get(element), goog.functions.equalTo('editable')));
  1169. // Skip restore won't reset the original element's CSS classes.
  1170. editableField.makeUneditable(true /* opt_skipRestore */);
  1171. editableField.makeEditable();
  1172. assertTrue(goog.dom.classlist.contains(element, 'editable'));
  1173. assertEquals(
  1174. 1,
  1175. goog.array.count(
  1176. goog.dom.classlist.get(element), goog.functions.equalTo('editable')));
  1177. }