const {CompositeDisposable} = require('atom') const TooltipManager = require('../src/tooltip-manager') const Tooltip = require('../src/tooltip') const _ = require('underscore-plus') describe('TooltipManager', () => { let manager, element const ctrlX = _.humanizeKeystroke('ctrl-x') const ctrlY = _.humanizeKeystroke('ctrl-y') const hover = function (element, fn) { mouseEnter(element) advanceClock(manager.hoverDefaults.delay.show) fn() mouseLeave(element) advanceClock(manager.hoverDefaults.delay.hide) } beforeEach(function () { manager = new TooltipManager({keymapManager: atom.keymaps, viewRegistry: atom.views}) element = createElement('foo') }) describe('::add(target, options)', () => { describe("when the trigger is 'hover' (the default)", () => { it('creates a tooltip when hovering over the target element', () => { manager.add(element, {title: 'Title'}) hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) }) it('displays tooltips immediately when hovering over new elements once a tooltip has been displayed once', () => { const disposables = new CompositeDisposable() const element1 = createElement('foo') disposables.add(manager.add(element1, {title: 'Title'})) const element2 = createElement('bar') disposables.add(manager.add(element2, {title: 'Title'})) const element3 = createElement('baz') disposables.add(manager.add(element3, {title: 'Title'})) hover(element1, () => {}) expect(document.body.querySelector('.tooltip')).toBeNull() mouseEnter(element2) expect(document.body.querySelector('.tooltip')).not.toBeNull() mouseLeave(element2) advanceClock(manager.hoverDefaults.delay.hide) expect(document.body.querySelector('.tooltip')).toBeNull() advanceClock(Tooltip.FOLLOW_THROUGH_DURATION) mouseEnter(element3) expect(document.body.querySelector('.tooltip')).toBeNull() advanceClock(manager.hoverDefaults.delay.show) expect(document.body.querySelector('.tooltip')).not.toBeNull() disposables.dispose() }) }) describe("when the trigger is 'manual'", () => it('creates a tooltip immediately and only hides it on dispose', () => { const disposable = manager.add(element, {title: 'Title', trigger: 'manual'}) expect(document.body.querySelector('.tooltip')).toHaveText('Title') disposable.dispose() expect(document.body.querySelector('.tooltip')).toBeNull() }) ) describe("when the trigger is 'click'", () => it('shows and hides the tooltip when the target element is clicked', () => { manager.add(element, {title: 'Title', trigger: 'click'}) expect(document.body.querySelector('.tooltip')).toBeNull() element.click() expect(document.body.querySelector('.tooltip')).not.toBeNull() element.click() expect(document.body.querySelector('.tooltip')).toBeNull() // Hide the tooltip when clicking anywhere but inside the tooltip element element.click() expect(document.body.querySelector('.tooltip')).not.toBeNull() document.body.querySelector('.tooltip').click() expect(document.body.querySelector('.tooltip')).not.toBeNull() document.body.querySelector('.tooltip').firstChild.click() expect(document.body.querySelector('.tooltip')).not.toBeNull() document.body.click() expect(document.body.querySelector('.tooltip')).toBeNull() // Tooltip can show again after hiding due to clicking outside of the tooltip element.click() expect(document.body.querySelector('.tooltip')).not.toBeNull() element.click() expect(document.body.querySelector('.tooltip')).toBeNull() }) ) it('allows a custom item to be specified for the content of the tooltip', () => { const tooltipElement = document.createElement('div') manager.add(element, {item: {element: tooltipElement}}) hover(element, () => expect(tooltipElement.closest('.tooltip')).not.toBeNull()) }) it('allows a custom class to be specified for the tooltip', () => { manager.add(element, {title: 'Title', class: 'custom-tooltip-class'}) hover(element, () => expect(document.body.querySelector('.tooltip').classList.contains('custom-tooltip-class')).toBe(true)) }) it('allows jQuery elements to be passed as the target', () => { const element2 = document.createElement('div') jasmine.attachToDOM(element2) const fakeJqueryWrapper = { 0: element, 1: element2, length: 2, jquery: 'any-version' } const disposable = manager.add(fakeJqueryWrapper, {title: 'Title'}) hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) expect(document.body.querySelector('.tooltip')).toBeNull() hover(element2, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) expect(document.body.querySelector('.tooltip')).toBeNull() disposable.dispose() hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull()) hover(element2, () => expect(document.body.querySelector('.tooltip')).toBeNull()) }) describe('when a keyBindingCommand is specified', () => { describe('when a title is specified', () => it('appends the key binding corresponding to the command to the title', () => { atom.keymaps.add('test', { '.foo': { 'ctrl-x ctrl-y': 'test-command' }, '.bar': { 'ctrl-x ctrl-z': 'test-command' } } ) manager.add(element, {title: 'Title', keyBindingCommand: 'test-command'}) hover(element, function () { const tooltipElement = document.body.querySelector('.tooltip') expect(tooltipElement).toHaveText(`Title ${ctrlX} ${ctrlY}`) }) }) ) describe('when no title is specified', () => it('shows the key binding corresponding to the command alone', () => { atom.keymaps.add('test', {'.foo': {'ctrl-x ctrl-y': 'test-command'}}) manager.add(element, {keyBindingCommand: 'test-command'}) hover(element, function () { const tooltipElement = document.body.querySelector('.tooltip') expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`) }) }) ) describe('when a keyBindingTarget is specified', () => { it('looks up the key binding relative to the target', () => { atom.keymaps.add('test', { '.bar': { 'ctrl-x ctrl-z': 'test-command' }, '.foo': { 'ctrl-x ctrl-y': 'test-command' } } ) manager.add(element, {keyBindingCommand: 'test-command', keyBindingTarget: element}) hover(element, function () { const tooltipElement = document.body.querySelector('.tooltip') expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`) }) }) it('does not display the keybinding if there is nothing mapped to the specified keyBindingCommand', () => { manager.add(element, {title: 'A Title', keyBindingCommand: 'test-command', keyBindingTarget: element}) hover(element, function () { const tooltipElement = document.body.querySelector('.tooltip') expect(tooltipElement.textContent).toBe('A Title') }) }) }) }) describe('when .dispose() is called on the returned disposable', () => it('no longer displays the tooltip on hover', () => { const disposable = manager.add(element, {title: 'Title'}) hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) disposable.dispose() hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull()) }) ) describe('when the window is resized', () => it('hides the tooltips', () => { const disposable = manager.add(element, {title: 'Title'}) hover(element, function () { expect(document.body.querySelector('.tooltip')).not.toBeNull() window.dispatchEvent(new CustomEvent('resize')) expect(document.body.querySelector('.tooltip')).toBeNull() disposable.dispose() }) }) ) describe('findTooltips', () => { it('adds and remove tooltips correctly', () => { expect(manager.findTooltips(element).length).toBe(0) const disposable1 = manager.add(element, {title: 'elem1'}) expect(manager.findTooltips(element).length).toBe(1) const disposable2 = manager.add(element, {title: 'elem2'}) expect(manager.findTooltips(element).length).toBe(2) disposable1.dispose() expect(manager.findTooltips(element).length).toBe(1) disposable2.dispose() expect(manager.findTooltips(element).length).toBe(0) }) it('lets us hide tooltips programmatically', () => { const disposable = manager.add(element, {title: 'Title'}) hover(element, function () { expect(document.body.querySelector('.tooltip')).not.toBeNull() manager.findTooltips(element)[0].hide() expect(document.body.querySelector('.tooltip')).toBeNull() disposable.dispose() }) }) }) }) }) function createElement (className) { const el = document.createElement('div') el.classList.add(className) jasmine.attachToDOM(el) return el } function mouseEnter (element) { element.dispatchEvent(new CustomEvent('mouseenter', {bubbles: false})) element.dispatchEvent(new CustomEvent('mouseover', {bubbles: true})) } function mouseLeave (element) { element.dispatchEvent(new CustomEvent('mouseleave', {bubbles: false})) element.dispatchEvent(new CustomEvent('mouseout', {bubbles: true})) }