tooltip-manager-spec.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. const { CompositeDisposable } = require('atom');
  2. const TooltipManager = require('../src/tooltip-manager');
  3. const Tooltip = require('../src/tooltip');
  4. const _ = require('underscore-plus');
  5. describe('TooltipManager', () => {
  6. let manager, element;
  7. const ctrlX = _.humanizeKeystroke('ctrl-x');
  8. const ctrlY = _.humanizeKeystroke('ctrl-y');
  9. const hover = function(element, fn) {
  10. mouseEnter(element);
  11. advanceClock(manager.hoverDefaults.delay.show);
  12. fn();
  13. mouseLeave(element);
  14. advanceClock(manager.hoverDefaults.delay.hide);
  15. };
  16. beforeEach(function() {
  17. manager = new TooltipManager({
  18. keymapManager: atom.keymaps,
  19. viewRegistry: atom.views
  20. });
  21. element = createElement('foo');
  22. });
  23. describe('::add(target, options)', () => {
  24. describe("when the trigger is 'hover' (the default)", () => {
  25. it('creates a tooltip when hovering over the target element', () => {
  26. manager.add(element, { title: 'Title' });
  27. hover(element, () =>
  28. expect(document.body.querySelector('.tooltip')).toHaveText('Title')
  29. );
  30. });
  31. it('displays tooltips immediately when hovering over new elements once a tooltip has been displayed once', () => {
  32. const disposables = new CompositeDisposable();
  33. const element1 = createElement('foo');
  34. disposables.add(manager.add(element1, { title: 'Title' }));
  35. const element2 = createElement('bar');
  36. disposables.add(manager.add(element2, { title: 'Title' }));
  37. const element3 = createElement('baz');
  38. disposables.add(manager.add(element3, { title: 'Title' }));
  39. hover(element1, () => {});
  40. expect(document.body.querySelector('.tooltip')).toBeNull();
  41. mouseEnter(element2);
  42. expect(document.body.querySelector('.tooltip')).not.toBeNull();
  43. mouseLeave(element2);
  44. advanceClock(manager.hoverDefaults.delay.hide);
  45. expect(document.body.querySelector('.tooltip')).toBeNull();
  46. advanceClock(Tooltip.FOLLOW_THROUGH_DURATION);
  47. mouseEnter(element3);
  48. expect(document.body.querySelector('.tooltip')).toBeNull();
  49. advanceClock(manager.hoverDefaults.delay.show);
  50. expect(document.body.querySelector('.tooltip')).not.toBeNull();
  51. disposables.dispose();
  52. });
  53. it('hides the tooltip on keydown events', () => {
  54. const disposable = manager.add(element, {
  55. title: 'Title',
  56. trigger: 'hover'
  57. });
  58. hover(element, function() {
  59. expect(document.body.querySelector('.tooltip')).not.toBeNull();
  60. window.dispatchEvent(
  61. new CustomEvent('keydown', {
  62. bubbles: true
  63. })
  64. );
  65. expect(document.body.querySelector('.tooltip')).toBeNull();
  66. disposable.dispose();
  67. });
  68. });
  69. });
  70. describe("when the trigger is 'manual'", () =>
  71. it('creates a tooltip immediately and only hides it on dispose', () => {
  72. const disposable = manager.add(element, {
  73. title: 'Title',
  74. trigger: 'manual'
  75. });
  76. expect(document.body.querySelector('.tooltip')).toHaveText('Title');
  77. disposable.dispose();
  78. expect(document.body.querySelector('.tooltip')).toBeNull();
  79. }));
  80. describe("when the trigger is 'click'", () =>
  81. it('shows and hides the tooltip when the target element is clicked', () => {
  82. manager.add(element, { title: 'Title', trigger: 'click' });
  83. expect(document.body.querySelector('.tooltip')).toBeNull();
  84. element.click();
  85. expect(document.body.querySelector('.tooltip')).not.toBeNull();
  86. element.click();
  87. expect(document.body.querySelector('.tooltip')).toBeNull();
  88. // Hide the tooltip when clicking anywhere but inside the tooltip element
  89. element.click();
  90. expect(document.body.querySelector('.tooltip')).not.toBeNull();
  91. document.body.querySelector('.tooltip').click();
  92. expect(document.body.querySelector('.tooltip')).not.toBeNull();
  93. document.body.querySelector('.tooltip').firstChild.click();
  94. expect(document.body.querySelector('.tooltip')).not.toBeNull();
  95. document.body.click();
  96. expect(document.body.querySelector('.tooltip')).toBeNull();
  97. // Tooltip can show again after hiding due to clicking outside of the tooltip
  98. element.click();
  99. expect(document.body.querySelector('.tooltip')).not.toBeNull();
  100. element.click();
  101. expect(document.body.querySelector('.tooltip')).toBeNull();
  102. }));
  103. it('does not hide the tooltip on keyboard input', () => {
  104. manager.add(element, { title: 'Title', trigger: 'click' });
  105. element.click();
  106. expect(document.body.querySelector('.tooltip')).not.toBeNull();
  107. window.dispatchEvent(
  108. new CustomEvent('keydown', {
  109. bubbles: true
  110. })
  111. );
  112. expect(document.body.querySelector('.tooltip')).not.toBeNull();
  113. // click again to hide the tooltip because otherwise state leaks
  114. // into other tests.
  115. element.click();
  116. });
  117. it('allows a custom item to be specified for the content of the tooltip', () => {
  118. const tooltipElement = document.createElement('div');
  119. manager.add(element, { item: { element: tooltipElement } });
  120. hover(element, () =>
  121. expect(tooltipElement.closest('.tooltip')).not.toBeNull()
  122. );
  123. });
  124. it('allows a custom class to be specified for the tooltip', () => {
  125. manager.add(element, { title: 'Title', class: 'custom-tooltip-class' });
  126. hover(element, () =>
  127. expect(
  128. document.body
  129. .querySelector('.tooltip')
  130. .classList.contains('custom-tooltip-class')
  131. ).toBe(true)
  132. );
  133. });
  134. it('allows jQuery elements to be passed as the target', () => {
  135. const element2 = document.createElement('div');
  136. jasmine.attachToDOM(element2);
  137. const fakeJqueryWrapper = {
  138. 0: element,
  139. 1: element2,
  140. length: 2,
  141. jquery: 'any-version'
  142. };
  143. const disposable = manager.add(fakeJqueryWrapper, { title: 'Title' });
  144. hover(element, () =>
  145. expect(document.body.querySelector('.tooltip')).toHaveText('Title')
  146. );
  147. expect(document.body.querySelector('.tooltip')).toBeNull();
  148. hover(element2, () =>
  149. expect(document.body.querySelector('.tooltip')).toHaveText('Title')
  150. );
  151. expect(document.body.querySelector('.tooltip')).toBeNull();
  152. disposable.dispose();
  153. hover(element, () =>
  154. expect(document.body.querySelector('.tooltip')).toBeNull()
  155. );
  156. hover(element2, () =>
  157. expect(document.body.querySelector('.tooltip')).toBeNull()
  158. );
  159. });
  160. describe('when a keyBindingCommand is specified', () => {
  161. describe('when a title is specified', () =>
  162. it('appends the key binding corresponding to the command to the title', () => {
  163. atom.keymaps.add('test', {
  164. '.foo': { 'ctrl-x ctrl-y': 'test-command' },
  165. '.bar': { 'ctrl-x ctrl-z': 'test-command' }
  166. });
  167. manager.add(element, {
  168. title: 'Title',
  169. keyBindingCommand: 'test-command'
  170. });
  171. hover(element, function() {
  172. const tooltipElement = document.body.querySelector('.tooltip');
  173. expect(tooltipElement).toHaveText(`Title ${ctrlX} ${ctrlY}`);
  174. });
  175. }));
  176. describe('when no title is specified', () =>
  177. it('shows the key binding corresponding to the command alone', () => {
  178. atom.keymaps.add('test', {
  179. '.foo': { 'ctrl-x ctrl-y': 'test-command' }
  180. });
  181. manager.add(element, { keyBindingCommand: 'test-command' });
  182. hover(element, function() {
  183. const tooltipElement = document.body.querySelector('.tooltip');
  184. expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`);
  185. });
  186. }));
  187. describe('when a keyBindingTarget is specified', () => {
  188. it('looks up the key binding relative to the target', () => {
  189. atom.keymaps.add('test', {
  190. '.bar': { 'ctrl-x ctrl-z': 'test-command' },
  191. '.foo': { 'ctrl-x ctrl-y': 'test-command' }
  192. });
  193. manager.add(element, {
  194. keyBindingCommand: 'test-command',
  195. keyBindingTarget: element
  196. });
  197. hover(element, function() {
  198. const tooltipElement = document.body.querySelector('.tooltip');
  199. expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`);
  200. });
  201. });
  202. it('does not display the keybinding if there is nothing mapped to the specified keyBindingCommand', () => {
  203. manager.add(element, {
  204. title: 'A Title',
  205. keyBindingCommand: 'test-command',
  206. keyBindingTarget: element
  207. });
  208. hover(element, function() {
  209. const tooltipElement = document.body.querySelector('.tooltip');
  210. expect(tooltipElement.textContent).toBe('A Title');
  211. });
  212. });
  213. });
  214. });
  215. describe('when .dispose() is called on the returned disposable', () =>
  216. it('no longer displays the tooltip on hover', () => {
  217. const disposable = manager.add(element, { title: 'Title' });
  218. hover(element, () =>
  219. expect(document.body.querySelector('.tooltip')).toHaveText('Title')
  220. );
  221. disposable.dispose();
  222. hover(element, () =>
  223. expect(document.body.querySelector('.tooltip')).toBeNull()
  224. );
  225. }));
  226. describe('when the window is resized', () =>
  227. it('hides the tooltips', () => {
  228. const disposable = manager.add(element, { title: 'Title' });
  229. hover(element, function() {
  230. expect(document.body.querySelector('.tooltip')).not.toBeNull();
  231. window.dispatchEvent(new CustomEvent('resize'));
  232. expect(document.body.querySelector('.tooltip')).toBeNull();
  233. disposable.dispose();
  234. });
  235. }));
  236. describe('findTooltips', () => {
  237. it('adds and remove tooltips correctly', () => {
  238. expect(manager.findTooltips(element).length).toBe(0);
  239. const disposable1 = manager.add(element, { title: 'elem1' });
  240. expect(manager.findTooltips(element).length).toBe(1);
  241. const disposable2 = manager.add(element, { title: 'elem2' });
  242. expect(manager.findTooltips(element).length).toBe(2);
  243. disposable1.dispose();
  244. expect(manager.findTooltips(element).length).toBe(1);
  245. disposable2.dispose();
  246. expect(manager.findTooltips(element).length).toBe(0);
  247. });
  248. it('lets us hide tooltips programmatically', () => {
  249. const disposable = manager.add(element, { title: 'Title' });
  250. hover(element, function() {
  251. expect(document.body.querySelector('.tooltip')).not.toBeNull();
  252. manager.findTooltips(element)[0].hide();
  253. expect(document.body.querySelector('.tooltip')).toBeNull();
  254. disposable.dispose();
  255. });
  256. });
  257. });
  258. });
  259. });
  260. function createElement(className) {
  261. const el = document.createElement('div');
  262. el.classList.add(className);
  263. jasmine.attachToDOM(el);
  264. return el;
  265. }
  266. function mouseEnter(element) {
  267. element.dispatchEvent(new CustomEvent('mouseenter', { bubbles: false }));
  268. element.dispatchEvent(new CustomEvent('mouseover', { bubbles: true }));
  269. }
  270. function mouseLeave(element) {
  271. element.dispatchEvent(new CustomEvent('mouseleave', { bubbles: false }));
  272. element.dispatchEvent(new CustomEvent('mouseout', { bubbles: true }));
  273. }