abstractdialogplugin_test.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  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.AbstractDialogPluginTest');
  15. goog.setTestOnly('goog.editor.plugins.AbstractDialogPluginTest');
  16. goog.require('goog.dom');
  17. goog.require('goog.dom.SavedRange');
  18. goog.require('goog.dom.TagName');
  19. goog.require('goog.editor.Field');
  20. goog.require('goog.editor.plugins.AbstractDialogPlugin');
  21. goog.require('goog.events.Event');
  22. goog.require('goog.events.EventHandler');
  23. goog.require('goog.functions');
  24. goog.require('goog.testing.MockClock');
  25. goog.require('goog.testing.MockControl');
  26. goog.require('goog.testing.PropertyReplacer');
  27. goog.require('goog.testing.editor.FieldMock');
  28. goog.require('goog.testing.editor.TestHelper');
  29. goog.require('goog.testing.events');
  30. goog.require('goog.testing.jsunit');
  31. goog.require('goog.testing.mockmatchers.ArgumentMatcher');
  32. goog.require('goog.ui.editor.AbstractDialog');
  33. goog.require('goog.userAgent');
  34. var plugin;
  35. var mockCtrl;
  36. var mockField;
  37. var mockSavedRange;
  38. var mockOpenedHandler;
  39. var mockClosedHandler;
  40. var COMMAND = 'myCommand';
  41. var stubs = new goog.testing.PropertyReplacer();
  42. var mockClock;
  43. var fieldObj;
  44. var fieldElem;
  45. var mockHandler;
  46. function setUp() {
  47. mockCtrl = new goog.testing.MockControl();
  48. mockOpenedHandler = mockCtrl.createLooseMock(goog.events.EventHandler);
  49. mockClosedHandler = mockCtrl.createLooseMock(goog.events.EventHandler);
  50. mockField = new goog.testing.editor.FieldMock(undefined, undefined, {});
  51. mockCtrl.addMock(mockField);
  52. mockField.focus();
  53. plugin = createDialogPlugin();
  54. }
  55. function setUpMockRange() {
  56. mockSavedRange = mockCtrl.createLooseMock(goog.dom.SavedRange);
  57. mockSavedRange.restore();
  58. stubs.setPath(
  59. 'goog.editor.range.saveUsingNormalizedCarets',
  60. goog.functions.constant(mockSavedRange));
  61. }
  62. function tearDown() {
  63. stubs.reset();
  64. tearDownRealEditableField();
  65. if (mockClock) {
  66. // Crucial to letting time operations work normally in the rest of tests.
  67. mockClock.dispose();
  68. }
  69. if (plugin) {
  70. mockField.$setIgnoreUnexpectedCalls(true);
  71. plugin.dispose();
  72. }
  73. }
  74. /**
  75. * Creates a concrete instance of goog.ui.editor.AbstractDialog by adding
  76. * a plain implementation of createDialogControl().
  77. * @param {goog.dom.DomHelper} domHelper The dom helper to be used to
  78. * create the dialog.
  79. * @return {goog.ui.editor.AbstractDialog} The created dialog.
  80. */
  81. function createDialog(domHelper) {
  82. var dialog = new goog.ui.editor.AbstractDialog(domHelper);
  83. dialog.createDialogControl = function() {
  84. return new goog.ui.editor.AbstractDialog.Builder(dialog).build();
  85. };
  86. return dialog;
  87. }
  88. /**
  89. * Creates a concrete instance of the abstract class
  90. * goog.editor.plugins.AbstractDialogPlugin
  91. * and registers it with the mock editable field being used.
  92. * @return {goog.editor.plugins.AbstractDialogPlugin} The created plugin.
  93. */
  94. function createDialogPlugin() {
  95. var plugin = new goog.editor.plugins.AbstractDialogPlugin(COMMAND);
  96. plugin.createDialog = createDialog;
  97. plugin.returnControlToEditableField = plugin.restoreOriginalSelection;
  98. plugin.registerFieldObject(mockField);
  99. plugin.addEventListener(
  100. goog.editor.plugins.AbstractDialogPlugin.EventType.OPENED,
  101. mockOpenedHandler);
  102. plugin.addEventListener(
  103. goog.editor.plugins.AbstractDialogPlugin.EventType.CLOSED,
  104. mockClosedHandler);
  105. return plugin;
  106. }
  107. /**
  108. * Sets up the mock event handler to expect an OPENED event.
  109. */
  110. function expectOpened(/** number= */ opt_times) {
  111. mockOpenedHandler.handleEvent(
  112. new goog.testing.mockmatchers.ArgumentMatcher(function(arg) {
  113. return arg.type ==
  114. goog.editor.plugins.AbstractDialogPlugin.EventType.OPENED;
  115. }));
  116. mockField.dispatchSelectionChangeEvent();
  117. if (opt_times) {
  118. mockOpenedHandler.$times(opt_times);
  119. mockField.$times(opt_times);
  120. }
  121. }
  122. /**
  123. * Sets up the mock event handler to expect a CLOSED event.
  124. */
  125. function expectClosed(/** number= */ opt_times) {
  126. mockClosedHandler.handleEvent(
  127. new goog.testing.mockmatchers.ArgumentMatcher(function(arg) {
  128. return arg.type ==
  129. goog.editor.plugins.AbstractDialogPlugin.EventType.CLOSED;
  130. }));
  131. mockField.dispatchSelectionChangeEvent();
  132. if (opt_times) {
  133. mockClosedHandler.$times(opt_times);
  134. mockField.$times(opt_times);
  135. }
  136. }
  137. /**
  138. * Tests the simple flow of calling execCommand (which opens the
  139. * dialog) and immediately disposing of the plugin (which closes the dialog).
  140. * @param {boolean=} opt_reuse Whether to set the plugin to reuse its dialog.
  141. */
  142. function testExecAndDispose(opt_reuse) {
  143. setUpMockRange();
  144. expectOpened();
  145. expectClosed();
  146. mockField.debounceEvent(goog.editor.Field.EventType.SELECTIONCHANGE);
  147. mockCtrl.$replayAll();
  148. if (opt_reuse) {
  149. plugin.setReuseDialog(true);
  150. }
  151. assertFalse(
  152. 'Dialog should not be open yet',
  153. !!plugin.getDialog() && plugin.getDialog().isOpen());
  154. plugin.execCommand(COMMAND);
  155. assertTrue(
  156. 'Dialog should be open now',
  157. !!plugin.getDialog() && plugin.getDialog().isOpen());
  158. var tempDialog = plugin.getDialog();
  159. plugin.dispose();
  160. assertFalse(
  161. 'Dialog should not still be open after disposal', tempDialog.isOpen());
  162. mockCtrl.$verifyAll();
  163. }
  164. /**
  165. * Tests execCommand and dispose while reusing the dialog.
  166. */
  167. function testExecAndDisposeReuse() {
  168. testExecAndDispose(true);
  169. }
  170. /**
  171. * Tests the flow of calling execCommand (which opens the dialog) and
  172. * then hiding it (simulating that a user did somthing to cause the dialog to
  173. * close).
  174. * @param {boolean=} opt_reuse Whether to set the plugin to reuse its dialog.
  175. */
  176. function testExecAndHide(opt_reuse) {
  177. setUpMockRange();
  178. expectOpened();
  179. expectClosed();
  180. mockField.debounceEvent(goog.editor.Field.EventType.SELECTIONCHANGE);
  181. mockCtrl.$replayAll();
  182. if (opt_reuse) {
  183. plugin.setReuseDialog(true);
  184. }
  185. assertFalse(
  186. 'Dialog should not be open yet',
  187. !!plugin.getDialog() && plugin.getDialog().isOpen());
  188. plugin.execCommand(COMMAND);
  189. assertTrue(
  190. 'Dialog should be open now',
  191. !!plugin.getDialog() && plugin.getDialog().isOpen());
  192. var tempDialog = plugin.getDialog();
  193. plugin.getDialog().hide();
  194. assertFalse(
  195. 'Dialog should not still be open after hiding', tempDialog.isOpen());
  196. if (opt_reuse) {
  197. assertFalse(
  198. 'Dialog should not be disposed after hiding (will be reused)',
  199. tempDialog.isDisposed());
  200. } else {
  201. assertTrue(
  202. 'Dialog should be disposed after hiding', tempDialog.isDisposed());
  203. }
  204. plugin.dispose();
  205. mockCtrl.$verifyAll();
  206. }
  207. /**
  208. * Tests execCommand and hide while reusing the dialog.
  209. */
  210. function testExecAndHideReuse() {
  211. testExecAndHide(true);
  212. }
  213. /**
  214. * Tests the flow of calling execCommand (which opens a dialog) and
  215. * then calling it again before the first dialog is closed. This is not
  216. * something anyone should be doing since dialogs are (usually?) modal so the
  217. * user can't do another execCommand before closing the first dialog. But
  218. * since the API makes it possible, I thought it would be good to guard
  219. * against and unit test.
  220. * @param {boolean=} opt_reuse Whether to set the plugin to reuse its dialog.
  221. */
  222. function testExecTwice(opt_reuse) {
  223. setUpMockRange();
  224. if (opt_reuse) {
  225. expectOpened(2); // The second exec should cause a second OPENED event.
  226. // But the dialog was not closed between exec calls, so only one CLOSED is
  227. // expected.
  228. expectClosed();
  229. plugin.setReuseDialog(true);
  230. mockField.debounceEvent(goog.editor.Field.EventType.SELECTIONCHANGE);
  231. } else {
  232. expectOpened(2); // The second exec should cause a second OPENED event.
  233. // The first dialog will be disposed so there should be two CLOSED events.
  234. expectClosed(2);
  235. mockSavedRange.restore(); // Expected 2x, once already recorded in setup.
  236. mockField.focus(); // Expected 2x, once already recorded in setup.
  237. mockField.debounceEvent(goog.editor.Field.EventType.SELECTIONCHANGE);
  238. mockField.$times(2);
  239. }
  240. mockCtrl.$replayAll();
  241. assertFalse(
  242. 'Dialog should not be open yet',
  243. !!plugin.getDialog() && plugin.getDialog().isOpen());
  244. plugin.execCommand(COMMAND);
  245. assertTrue(
  246. 'Dialog should be open now',
  247. !!plugin.getDialog() && plugin.getDialog().isOpen());
  248. var tempDialog = plugin.getDialog();
  249. plugin.execCommand(COMMAND);
  250. if (opt_reuse) {
  251. assertTrue(
  252. 'Reused dialog should still be open after second exec',
  253. tempDialog.isOpen());
  254. assertFalse(
  255. 'Reused dialog should not be disposed after second exec',
  256. tempDialog.isDisposed());
  257. } else {
  258. assertFalse(
  259. 'First dialog should not still be open after opening second',
  260. tempDialog.isOpen());
  261. assertTrue(
  262. 'First dialog should be disposed after opening second',
  263. tempDialog.isDisposed());
  264. }
  265. plugin.dispose();
  266. mockCtrl.$verifyAll();
  267. }
  268. /**
  269. * Tests execCommand twice while reusing the dialog.
  270. */
  271. function testExecTwiceReuse() {
  272. // Test is failing with an out-of-memory error in IE7.
  273. if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('8')) {
  274. return;
  275. }
  276. testExecTwice(true);
  277. }
  278. /**
  279. * Tests that the selection is cleared when the dialog opens and is
  280. * correctly restored after it closes.
  281. */
  282. function testRestoreSelection() {
  283. setUpRealEditableField();
  284. fieldObj.setHtml(false, '12345');
  285. var elem = fieldObj.getElement();
  286. var helper = new goog.testing.editor.TestHelper(elem);
  287. helper.select('12345', 1, '12345', 4); // Selects '234'.
  288. assertEquals(
  289. 'Incorrect text selected before dialog is opened', '234',
  290. fieldObj.getRange().getText());
  291. plugin.execCommand(COMMAND);
  292. if (!goog.userAgent.IE && !goog.userAgent.OPERA) {
  293. // IE returns some bogus range when field doesn't have selection.
  294. // Opera can't remove the selection from a whitebox field.
  295. assertNull(
  296. 'There should be no selection while dialog is open',
  297. fieldObj.getRange());
  298. }
  299. plugin.getDialog().hide();
  300. assertEquals(
  301. 'Incorrect text selected after dialog is closed', '234',
  302. fieldObj.getRange().getText());
  303. }
  304. /**
  305. * Setup a real editable field (instead of a mock) and register the plugin to
  306. * it.
  307. */
  308. function setUpRealEditableField() {
  309. fieldElem = goog.dom.createElement(goog.dom.TagName.DIV);
  310. fieldElem.id = 'myField';
  311. document.body.appendChild(fieldElem);
  312. fieldObj = new goog.editor.Field('myField', document);
  313. fieldObj.makeEditable();
  314. // Register the plugin to that field.
  315. plugin.getTrogClassId = goog.functions.constant('myClassId');
  316. fieldObj.registerPlugin(plugin);
  317. }
  318. /**
  319. * Tear down the real editable field.
  320. */
  321. function tearDownRealEditableField() {
  322. if (fieldObj) {
  323. fieldObj.makeUneditable();
  324. fieldObj.dispose();
  325. fieldObj = null;
  326. }
  327. if (fieldElem && fieldElem.parentNode == document.body) {
  328. document.body.removeChild(fieldElem);
  329. }
  330. }
  331. /**
  332. * Tests that after the dialog is hidden via a keystroke, the editable field
  333. * doesn't fire an extra SELECTIONCHANGE event due to the keyup from that
  334. * keystroke.
  335. * There is also a robot test in dialog_robot.html to test debouncing the
  336. * SELECTIONCHANGE event when the dialog closes.
  337. */
  338. function testDebounceSelectionChange() {
  339. mockClock = new goog.testing.MockClock(true);
  340. // Initial time is 0 which evaluates to false in debouncing implementation.
  341. mockClock.tick(1);
  342. setUpRealEditableField();
  343. // Set up a mock event handler to make sure selection change isn't fired
  344. // more than once on close and a second time on close.
  345. var count = 0;
  346. fieldObj.addEventListener(
  347. goog.editor.Field.EventType.SELECTIONCHANGE, function(e) { count++; });
  348. assertEquals(0, count);
  349. plugin.execCommand(COMMAND);
  350. assertEquals(1, count);
  351. plugin.getDialog().hide();
  352. assertEquals(2, count);
  353. // Fake the keyup event firing on the field after the dialog closes.
  354. var e = new goog.events.Event('keyup', plugin.fieldObject.getElement());
  355. e.keyCode = 13;
  356. goog.testing.events.fireBrowserEvent(e);
  357. // Tick the mock clock so that selection change tries to fire.
  358. mockClock.tick(goog.editor.Field.SELECTION_CHANGE_FREQUENCY_ + 1);
  359. // Ensure the handler did not fire again.
  360. assertEquals(2, count);
  361. }