abstractbubbleplugin_test.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  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.AbstractBubblePluginTest');
  15. goog.setTestOnly('goog.editor.plugins.AbstractBubblePluginTest');
  16. goog.require('goog.dom');
  17. goog.require('goog.dom.TagName');
  18. goog.require('goog.editor.plugins.AbstractBubblePlugin');
  19. goog.require('goog.events.BrowserEvent');
  20. goog.require('goog.events.EventType');
  21. goog.require('goog.events.KeyCodes');
  22. goog.require('goog.functions');
  23. goog.require('goog.style');
  24. goog.require('goog.testing.editor.FieldMock');
  25. goog.require('goog.testing.editor.TestHelper');
  26. goog.require('goog.testing.events');
  27. goog.require('goog.testing.events.Event');
  28. goog.require('goog.testing.jsunit');
  29. goog.require('goog.ui.editor.Bubble');
  30. goog.require('goog.userAgent');
  31. var testHelper;
  32. var fieldDiv;
  33. var COMMAND = 'base';
  34. var fieldMock;
  35. var bubblePlugin;
  36. var link;
  37. var link2;
  38. function setUpPage() {
  39. fieldDiv = goog.dom.getElement('field');
  40. var viewportSize = goog.dom.getViewportSize();
  41. // Some tests depends on enough size of viewport.
  42. if (viewportSize.width < 600 || viewportSize.height < 440) {
  43. window.moveTo(0, 0);
  44. window.resizeTo(640, 480);
  45. }
  46. }
  47. function setUp() {
  48. testHelper = new goog.testing.editor.TestHelper(fieldDiv);
  49. testHelper.setUpEditableElement();
  50. fieldMock = new goog.testing.editor.FieldMock();
  51. bubblePlugin = new goog.editor.plugins.AbstractBubblePlugin(COMMAND);
  52. bubblePlugin.fieldObject = fieldMock;
  53. fieldDiv.innerHTML = '<a href="http://www.google.com">Google</a>' +
  54. '<a href="http://www.google.com">Google2</a>';
  55. link = fieldDiv.firstChild;
  56. link2 = fieldDiv.lastChild;
  57. window.scrollTo(0, 0);
  58. goog.style.setStyle(document.body, 'direction', 'ltr');
  59. goog.style.setStyle(document.getElementById('field'), 'position', 'static');
  60. }
  61. function tearDown() {
  62. bubblePlugin.closeBubble();
  63. testHelper.tearDownEditableElement();
  64. }
  65. /**
  66. * This is a helper function for setting up the targetElement with a
  67. * given direction.
  68. *
  69. * @param {string} dir The direction of the targetElement, 'ltr' or 'rtl'.
  70. */
  71. function prepareTargetWithGivenDirection(dir) {
  72. goog.style.setStyle(document.body, 'direction', dir);
  73. fieldDiv.style.direction = dir;
  74. fieldDiv.innerHTML = '<a href="http://www.google.com">Google</a>';
  75. link = fieldDiv.firstChild;
  76. fieldMock.$replay();
  77. bubblePlugin.createBubbleContents = function(bubbleContainer) {
  78. bubbleContainer.innerHTML = '<div style="border:1px solid blue;">B</div>';
  79. goog.style.setStyle(bubbleContainer, 'border', '1px solid white');
  80. };
  81. bubblePlugin.registerFieldObject(fieldMock);
  82. bubblePlugin.enable(fieldMock);
  83. bubblePlugin.createBubble(link);
  84. }
  85. /**
  86. * Similar in intent to mock reset, but implemented by recreating the mock
  87. * variable. $reset() can't work because it will reset general any-time
  88. * expectations done in the fieldMock constructor.
  89. */
  90. function resetFieldMock() {
  91. fieldMock = new goog.testing.editor.FieldMock();
  92. bubblePlugin.fieldObject = fieldMock;
  93. }
  94. function helpTestCreateBubble(opt_fn) {
  95. fieldMock.$replay();
  96. var numCalled = 0;
  97. bubblePlugin.createBubbleContents = function(bubbleContainer) {
  98. numCalled++;
  99. assertNotNull('bubbleContainer should not be null', bubbleContainer);
  100. };
  101. if (opt_fn) {
  102. opt_fn();
  103. }
  104. bubblePlugin.createBubble(link);
  105. assertEquals('createBubbleContents should be called', 1, numCalled);
  106. fieldMock.$verify();
  107. }
  108. function testCreateBubble(opt_fn) {
  109. helpTestCreateBubble(opt_fn);
  110. assertTrue(bubblePlugin.getSharedBubble_() instanceof goog.ui.editor.Bubble);
  111. assertTrue('Bubble should be visible', bubblePlugin.isVisible());
  112. }
  113. function testOpeningBubbleCallsOnShow() {
  114. var numCalled = 0;
  115. testCreateBubble(function() {
  116. bubblePlugin.onShow = function() { numCalled++; };
  117. });
  118. assertEquals('onShow should be called', 1, numCalled);
  119. fieldMock.$verify();
  120. }
  121. function testCloseBubble() {
  122. testCreateBubble();
  123. bubblePlugin.closeBubble();
  124. assertFalse('Bubble should not be visible', bubblePlugin.isVisible());
  125. fieldMock.$verify();
  126. }
  127. function testZindexBehavior() {
  128. // Don't use the default return values.
  129. fieldMock.$reset();
  130. fieldMock.getAppWindow().$anyTimes().$returns(window);
  131. fieldMock.getEditableDomHelper().$anyTimes().$returns(
  132. goog.dom.getDomHelper(document));
  133. fieldMock.getBaseZindex().$returns(2);
  134. bubblePlugin.createBubbleContents = goog.nullFunction;
  135. fieldMock.$replay();
  136. bubblePlugin.createBubble(link);
  137. assertEquals(
  138. '2', '' + bubblePlugin.getSharedBubble_().bubbleContainer_.style.zIndex);
  139. fieldMock.$verify();
  140. }
  141. function testNoTwoBubblesOpenAtSameTime() {
  142. fieldMock.$replay();
  143. var origClose = goog.bind(bubblePlugin.closeBubble, bubblePlugin);
  144. var numTimesCloseCalled = 0;
  145. bubblePlugin.closeBubble = function() {
  146. numTimesCloseCalled++;
  147. origClose();
  148. };
  149. bubblePlugin.getBubbleTargetFromSelection = goog.functions.identity;
  150. bubblePlugin.createBubbleContents = goog.nullFunction;
  151. bubblePlugin.handleSelectionChangeInternal(link);
  152. assertEquals(0, numTimesCloseCalled);
  153. assertEquals(link, bubblePlugin.targetElement_);
  154. fieldMock.$verify();
  155. bubblePlugin.handleSelectionChangeInternal(link2);
  156. assertEquals(1, numTimesCloseCalled);
  157. assertEquals(link2, bubblePlugin.targetElement_);
  158. fieldMock.$verify();
  159. }
  160. function testHandleSelectionChangeWithEvent() {
  161. fieldMock.$replay();
  162. var fakeEvent = new goog.events.BrowserEvent({type: 'mouseup', target: link});
  163. bubblePlugin.getBubbleTargetFromSelection = goog.functions.identity;
  164. bubblePlugin.createBubbleContents = goog.nullFunction;
  165. bubblePlugin.handleSelectionChange(fakeEvent);
  166. assertTrue('Bubble should have been opened', bubblePlugin.isVisible());
  167. assertEquals(
  168. 'Bubble target should be provided event\'s target', link,
  169. bubblePlugin.targetElement_);
  170. }
  171. function testHandleSelectionChangeWithTarget() {
  172. fieldMock.$replay();
  173. bubblePlugin.getBubbleTargetFromSelection = goog.functions.identity;
  174. bubblePlugin.createBubbleContents = goog.nullFunction;
  175. bubblePlugin.handleSelectionChange(undefined, link2);
  176. assertTrue('Bubble should have been opened', bubblePlugin.isVisible());
  177. assertEquals(
  178. 'Bubble target should be provided target', link2,
  179. bubblePlugin.targetElement_);
  180. }
  181. /**
  182. * Regression test for @bug 2945341
  183. */
  184. function testSelectOneTextCharacterNoError() {
  185. fieldMock.$replay();
  186. bubblePlugin.getBubbleTargetFromSelection = goog.functions.identity;
  187. bubblePlugin.createBubbleContents = goog.nullFunction;
  188. // Select first char of first link's text node.
  189. testHelper.select(link.firstChild, 0, link.firstChild, 1);
  190. // This should execute without js errors.
  191. bubblePlugin.handleSelectionChange();
  192. assertTrue('Bubble should have been opened', bubblePlugin.isVisible());
  193. fieldMock.$verify();
  194. }
  195. function testTabKeyEvents() {
  196. fieldMock.$replay();
  197. bubblePlugin.enableKeyboardNavigation(true);
  198. bubblePlugin.getBubbleTargetFromSelection = goog.functions.identity;
  199. var nonTabbable1, tabbable1, tabbable2, nonTabbable2;
  200. bubblePlugin.createBubbleContents = function(container) {
  201. nonTabbable1 = goog.dom.createDom(goog.dom.TagName.DIV);
  202. tabbable1 = goog.dom.createDom(goog.dom.TagName.DIV);
  203. tabbable2 = goog.dom.createDom(goog.dom.TagName.DIV);
  204. nonTabbable2 = goog.dom.createDom(goog.dom.TagName.DIV);
  205. goog.dom.append(
  206. container, nonTabbable1, tabbable1, tabbable2, nonTabbable2);
  207. bubblePlugin.setTabbable(tabbable1);
  208. bubblePlugin.setTabbable(tabbable2);
  209. };
  210. bubblePlugin.handleSelectionChangeInternal(link);
  211. assertTrue('Bubble should be visible', bubblePlugin.isVisible());
  212. var tabHandledByBubble = simulateTabKeyOnBubble();
  213. assertTrue('The action should be handled by the plugin', tabHandledByBubble);
  214. assertFocused(tabbable1);
  215. // Tab on the first tabbable. The test framework doesn't easily let us verify
  216. // the desired behavior - namely, that the second tabbable gets focused - but
  217. // we verify that the field doesn't get the focus.
  218. goog.testing.events.fireKeySequence(tabbable1, goog.events.KeyCodes.TAB);
  219. fieldMock.$verify();
  220. // Tabbing on the last tabbable should trigger focus() of the target field.
  221. resetFieldMock();
  222. fieldMock.focus();
  223. fieldMock.$replay();
  224. goog.testing.events.fireKeySequence(tabbable2, goog.events.KeyCodes.TAB);
  225. fieldMock.$verify();
  226. }
  227. function testTabKeyEventsWithShiftKey() {
  228. fieldMock.$replay();
  229. bubblePlugin.enableKeyboardNavigation(true);
  230. bubblePlugin.getBubbleTargetFromSelection = goog.functions.identity;
  231. var nonTabbable, tabbable1, tabbable2;
  232. bubblePlugin.createBubbleContents = function(container) {
  233. nonTabbable = goog.dom.createDom(goog.dom.TagName.DIV);
  234. tabbable1 = goog.dom.createDom(goog.dom.TagName.DIV);
  235. // The test acts only on one tabbable, but we give another one to make sure
  236. // that the tabbable we act on is not also the last.
  237. tabbable2 = goog.dom.createDom(goog.dom.TagName.DIV);
  238. goog.dom.append(container, nonTabbable, tabbable1, tabbable2);
  239. bubblePlugin.setTabbable(tabbable1);
  240. bubblePlugin.setTabbable(tabbable2);
  241. };
  242. bubblePlugin.handleSelectionChangeInternal(link);
  243. assertTrue('Bubble should be visible', bubblePlugin.isVisible());
  244. var tabHandledByBubble = simulateTabKeyOnBubble();
  245. assertTrue('The action should be handled by the plugin', tabHandledByBubble);
  246. assertFocused(tabbable1);
  247. fieldMock.$verify();
  248. // Shift-tabbing on the first tabbable should trigger focus() of the target
  249. // field.
  250. resetFieldMock();
  251. fieldMock.focus();
  252. fieldMock.$replay();
  253. goog.testing.events.fireKeySequence(
  254. tabbable1, goog.events.KeyCodes.TAB, {shiftKey: true});
  255. fieldMock.$verify();
  256. }
  257. function testLinksAreTabbable() {
  258. fieldMock.$replay();
  259. bubblePlugin.enableKeyboardNavigation(true);
  260. bubblePlugin.getBubbleTargetFromSelection = goog.functions.identity;
  261. var nonTabbable1, link1, link2, nonTabbable2;
  262. bubblePlugin.createBubbleContents = function(container) {
  263. nonTabbable1 = goog.dom.createDom(goog.dom.TagName.DIV);
  264. goog.dom.appendChild(container, nonTabbable1);
  265. bubbleLink1 = this.createLink('linkInBubble1', 'Foo', false, container);
  266. bubbleLink2 = this.createLink('linkInBubble2', 'Bar', false, container);
  267. nonTabbable2 = goog.dom.createDom(goog.dom.TagName.DIV);
  268. goog.dom.appendChild(container, nonTabbable2);
  269. };
  270. bubblePlugin.handleSelectionChangeInternal(link);
  271. assertTrue('Bubble should be visible', bubblePlugin.isVisible());
  272. var tabHandledByBubble = simulateTabKeyOnBubble();
  273. assertTrue('The action should be handled by the plugin', tabHandledByBubble);
  274. assertFocused(bubbleLink1);
  275. fieldMock.$verify();
  276. // Tabbing on the last link should trigger focus() of the target field.
  277. resetFieldMock();
  278. fieldMock.focus();
  279. fieldMock.$replay();
  280. goog.testing.events.fireKeySequence(bubbleLink2, goog.events.KeyCodes.TAB);
  281. fieldMock.$verify();
  282. }
  283. function testTabKeyNoEffectKeyboardNavDisabled() {
  284. fieldMock.$replay();
  285. bubblePlugin.getBubbleTargetFromSelection = goog.functions.identity;
  286. var bubbleLink;
  287. bubblePlugin.createBubbleContents = function(container) {
  288. bubbleLink = this.createLink('linkInBubble', 'Foo', false, container);
  289. };
  290. bubblePlugin.handleSelectionChangeInternal(link);
  291. assertTrue('Bubble should be visible', bubblePlugin.isVisible());
  292. var tabHandledByBubble = simulateTabKeyOnBubble();
  293. assertFalse(
  294. 'The action should not be handled by the plugin', tabHandledByBubble);
  295. assertNotFocused(bubbleLink);
  296. // Verify that tabbing the link doesn't cause focus of the field.
  297. goog.testing.events.fireKeySequence(bubbleLink, goog.events.KeyCodes.TAB);
  298. fieldMock.$verify();
  299. }
  300. function testOtherKeyEventNoEffectKeyboardNavEnabled() {
  301. fieldMock.$replay();
  302. bubblePlugin.enableKeyboardNavigation(true);
  303. bubblePlugin.getBubbleTargetFromSelection = goog.functions.identity;
  304. var bubbleLink;
  305. bubblePlugin.createBubbleContents = function(container) {
  306. bubbleLink = this.createLink('linkInBubble', 'Foo', false, container);
  307. };
  308. bubblePlugin.handleSelectionChangeInternal(link);
  309. assertTrue('Bubble should be visible', bubblePlugin.isVisible());
  310. // Test pressing CTRL + B: this should not have any effect.
  311. var keyHandledByBubble =
  312. simulateKeyDownOnBubble(goog.events.KeyCodes.B, true);
  313. assertFalse(
  314. 'The action should not be handled by the plugin', keyHandledByBubble);
  315. assertNotFocused(bubbleLink);
  316. fieldMock.$verify();
  317. }
  318. function testSetTabbableSetsTabIndex() {
  319. var element1 = goog.dom.createDom(goog.dom.TagName.DIV);
  320. var element2 = goog.dom.createDom(goog.dom.TagName.DIV);
  321. element1.setAttribute('tabIndex', '1');
  322. bubblePlugin.setTabbable(element1);
  323. bubblePlugin.setTabbable(element2);
  324. assertEquals('1', element1.getAttribute('tabIndex'));
  325. assertEquals('0', element2.getAttribute('tabIndex'));
  326. }
  327. function testDisable() {
  328. testCreateBubble();
  329. fieldMock.setUneditable(true);
  330. bubblePlugin.disable(fieldMock);
  331. bubblePlugin.closeBubble();
  332. }
  333. /**
  334. * Sends a tab key event to the bubble.
  335. * @return {boolean} whether the bubble hanlded the event.
  336. */
  337. function simulateTabKeyOnBubble() {
  338. return simulateKeyDownOnBubble(goog.events.KeyCodes.TAB, false);
  339. }
  340. /**
  341. * Sends a key event to the bubble.
  342. * @param {number} keyCode
  343. * @param {boolean} isCtrl
  344. * @return {boolean} whether the bubble hanlded the event.
  345. */
  346. function simulateKeyDownOnBubble(keyCode, isCtrl) {
  347. // In some browsers (e.g. FireFox) the editable field is marked with
  348. // designMode on. In the test setting (and not in production setting), the
  349. // bubble element shares the same window and hence the designMode. In this
  350. // mode, activeElement remains the <body> and isn't changed along with the
  351. // focus as a result of tab key.
  352. if (goog.userAgent.GECKO) {
  353. bubblePlugin.getSharedBubble_()
  354. .getContentElement()
  355. .ownerDocument.designMode = 'off';
  356. }
  357. var event =
  358. new goog.testing.events.Event(goog.events.EventType.KEYDOWN, null);
  359. event.keyCode = keyCode;
  360. event.ctrlKey = isCtrl;
  361. return bubblePlugin.handleKeyDown(event);
  362. }
  363. function assertFocused(element) {
  364. // The activeElement assertion below doesn't work in IE7. At this time IE7 is
  365. // no longer supported by any client product, so we don't care.
  366. if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(8)) {
  367. return;
  368. }
  369. assertEquals('unexpected focus', element, document.activeElement);
  370. }
  371. function assertNotFocused(element) {
  372. assertNotEquals('unexpected focus', element, document.activeElement);
  373. }