diff options
Diffstat (limited to 'spec/frontend/behaviors/shortcuts')
-rw-r--r-- | spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js | 12 | ||||
-rw-r--r-- | spec/frontend/behaviors/shortcuts/shortcuts_spec.js | 267 |
2 files changed, 276 insertions, 3 deletions
diff --git a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js index ae7f5416c0c..6db99e796d6 100644 --- a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js +++ b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js @@ -30,14 +30,11 @@ describe('ShortcutsIssuable', () => { </div>`, ); document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); - - window.shortcut = new ShortcutsIssuable(true); }); afterEach(() => { $(FORM_SELECTOR).remove(); - delete window.shortcut; resetHTMLFixture(); }); @@ -55,6 +52,15 @@ describe('ShortcutsIssuable', () => { }); }; + it('sets up commands on instantiation', () => { + const mockShortcutsInstance = { addAll: jest.fn() }; + + // eslint-disable-next-line no-new + new ShortcutsIssuable(mockShortcutsInstance); + + expect(mockShortcutsInstance.addAll).toHaveBeenCalled(); + }); + describe('with empty selection', () => { it('does not return an error', () => { ShortcutsIssuable.replyWithSelectedText(true); diff --git a/spec/frontend/behaviors/shortcuts/shortcuts_spec.js b/spec/frontend/behaviors/shortcuts/shortcuts_spec.js new file mode 100644 index 00000000000..5f71eb24758 --- /dev/null +++ b/spec/frontend/behaviors/shortcuts/shortcuts_spec.js @@ -0,0 +1,267 @@ +import $ from 'jquery'; +import { flatten } from 'lodash'; +import htmlSnippetsShow from 'test_fixtures/snippets/show.html'; +import { Mousetrap } from '~/lib/mousetrap'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import Shortcuts, { LOCAL_MOUSETRAP_DATA_KEY } from '~/behaviors/shortcuts/shortcuts'; +import MarkdownPreview from '~/behaviors/preview_markdown'; + +describe('Shortcuts', () => { + let shortcuts; + + beforeAll(() => { + shortcuts = new Shortcuts(); + }); + + const mockSuperSidebarSearchButton = () => { + const button = document.createElement('button'); + button.id = 'super-sidebar-search'; + return button; + }; + + beforeEach(() => { + setHTMLFixture(htmlSnippetsShow); + document.body.appendChild(mockSuperSidebarSearchButton()); + + new Shortcuts(); // eslint-disable-line no-new + new MarkdownPreview(); // eslint-disable-line no-new + + jest.spyOn(HTMLElement.prototype, 'click'); + + jest.spyOn(Mousetrap.prototype, 'stopCallback'); + jest.spyOn(Mousetrap.prototype, 'bind').mockImplementation(); + jest.spyOn(Mousetrap.prototype, 'unbind').mockImplementation(); + }); + + afterEach(() => { + resetHTMLFixture(); + }); + + it('does not allow subclassing', () => { + const createSubclass = () => { + class Subclass extends Shortcuts {} + + return new Subclass(); + }; + + expect(createSubclass).toThrow(/cannot be subclassed/); + }); + + describe('markdown shortcuts', () => { + let shortcutElements; + + beforeEach(() => { + // Get all shortcuts specified with md-shortcuts attributes in the fixture. + // `shortcuts` will look something like this: + // [ + // [ 'mod+b' ], + // [ 'mod+i' ], + // [ 'mod+k' ] + // ] + shortcutElements = $('.edit-note .js-md') + .map(function getShortcutsFromToolbarBtn() { + const mdShortcuts = $(this).data('md-shortcuts'); + + // jQuery.map() automatically unwraps arrays, so we + // have to double wrap the array to counteract this + return mdShortcuts ? [mdShortcuts] : undefined; + }) + .get(); + }); + + describe('initMarkdownEditorShortcuts', () => { + let $textarea; + let localMousetrapInstance; + + beforeEach(() => { + $textarea = $('.edit-note textarea'); + Shortcuts.initMarkdownEditorShortcuts($textarea); + localMousetrapInstance = $textarea.data(LOCAL_MOUSETRAP_DATA_KEY); + }); + + it('attaches a Mousetrap handler for every markdown shortcut specified with md-shortcuts', () => { + const expectedCalls = shortcutElements.map((s) => [s, expect.any(Function)]); + + expect(Mousetrap.prototype.bind.mock.calls).toEqual(expectedCalls); + }); + + it('attaches a stopCallback that allows each markdown shortcut specified with md-shortcuts', () => { + flatten(shortcutElements).forEach((s) => { + expect( + localMousetrapInstance.stopCallback.call(localMousetrapInstance, null, null, s), + ).toBe(false); + }); + }); + }); + + describe('removeMarkdownEditorShortcuts', () => { + it('does nothing if initMarkdownEditorShortcuts was not previous called', () => { + Shortcuts.removeMarkdownEditorShortcuts($('.edit-note textarea')); + + expect(Mousetrap.prototype.unbind.mock.calls).toEqual([]); + }); + + it('removes Mousetrap handlers for every markdown shortcut specified with md-shortcuts', () => { + Shortcuts.initMarkdownEditorShortcuts($('.edit-note textarea')); + Shortcuts.removeMarkdownEditorShortcuts($('.edit-note textarea')); + + const expectedCalls = shortcutElements.map((s) => [s]); + + expect(Mousetrap.prototype.unbind.mock.calls).toEqual(expectedCalls); + }); + }); + }); + + describe('focusSearch', () => { + let event; + + beforeEach(() => { + event = new KeyboardEvent('keydown', { cancelable: true }); + Shortcuts.focusSearch(event); + }); + + it('clicks the super sidebar search button', () => { + expect(HTMLElement.prototype.click).toHaveBeenCalled(); + const thisArg = HTMLElement.prototype.click.mock.contexts[0]; + expect(thisArg.id).toBe('super-sidebar-search'); + }); + + it('cancels the default behaviour of the event', () => { + expect(event.defaultPrevented).toBe(true); + }); + }); + + describe('adding shortcuts', () => { + it('add calls Mousetrap.bind correctly', () => { + const mockCommand = { defaultKeys: ['m'] }; + const mockCallback = () => {}; + + shortcuts.add(mockCommand, mockCallback); + + expect(Mousetrap.prototype.bind).toHaveBeenCalledTimes(1); + const [callArguments] = Mousetrap.prototype.bind.mock.calls; + expect(callArguments[0]).toEqual(mockCommand.defaultKeys); + expect(callArguments[1]).toBe(mockCallback); + }); + + it('addAll calls Mousetrap.bind correctly', () => { + const mockCommandsAndCallbacks = [ + [{ defaultKeys: ['1'] }, () => {}], + [{ defaultKeys: ['2'] }, () => {}], + ]; + + shortcuts.addAll(mockCommandsAndCallbacks); + + expect(Mousetrap.prototype.bind).toHaveBeenCalledTimes(mockCommandsAndCallbacks.length); + const { calls } = Mousetrap.prototype.bind.mock; + + mockCommandsAndCallbacks.forEach(([mockCommand, mockCallback], i) => { + expect(calls[i][0]).toEqual(mockCommand.defaultKeys); + expect(calls[i][1]).toBe(mockCallback); + }); + }); + }); + + describe('addExtension', () => { + it('instantiates the given extension', () => { + const MockExtension = jest.fn(); + + const returnValue = shortcuts.addExtension(MockExtension, ['foo']); + + expect(MockExtension).toHaveBeenCalledTimes(1); + expect(MockExtension).toHaveBeenCalledWith(shortcuts, 'foo'); + expect(returnValue).toBe(MockExtension.mock.instances[0]); + }); + + it('instantiates declared dependencies', () => { + const MockDependency = jest.fn(); + const MockExtension = jest.fn(); + + MockExtension.dependencies = [MockDependency]; + + const returnValue = shortcuts.addExtension(MockExtension, ['foo']); + + expect(MockDependency).toHaveBeenCalledTimes(1); + expect(MockDependency.mock.instances).toHaveLength(1); + expect(MockDependency).toHaveBeenCalledWith(shortcuts); + + expect(returnValue).toBe(MockExtension.mock.instances[0]); + }); + + it('does not instantiate an extension more than once', () => { + const MockExtension = jest.fn(); + + const returnValue = shortcuts.addExtension(MockExtension, ['foo']); + const secondReturnValue = shortcuts.addExtension(MockExtension, ['bar']); + + expect(MockExtension).toHaveBeenCalledTimes(1); + expect(MockExtension).toHaveBeenCalledWith(shortcuts, 'foo'); + expect(returnValue).toBe(MockExtension.mock.instances[0]); + expect(secondReturnValue).toBe(MockExtension.mock.instances[0]); + }); + + it('allows extensions to redundantly depend on Shortcuts', () => { + const MockExtension = jest.fn(); + MockExtension.dependencies = [Shortcuts]; + + shortcuts.addExtension(MockExtension); + + expect(MockExtension).toHaveBeenCalledTimes(1); + expect(MockExtension).toHaveBeenCalledWith(shortcuts); + + // Ensure it wasn't instantiated + expect(shortcuts.extensions.has(Shortcuts)).toBe(false); + }); + + it('allows extensions to incorrectly depend on themselves', () => { + const A = jest.fn(); + A.dependencies = [A]; + shortcuts.addExtension(A); + expect(A).toHaveBeenCalledTimes(1); + expect(A).toHaveBeenCalledWith(shortcuts); + }); + + it('handles extensions with circular dependencies', () => { + const A = jest.fn(); + const B = jest.fn(); + const C = jest.fn(); + + A.dependencies = [B]; + B.dependencies = [C]; + C.dependencies = [A]; + + shortcuts.addExtension(A); + + expect(A).toHaveBeenCalledTimes(1); + expect(B).toHaveBeenCalledTimes(1); + expect(C).toHaveBeenCalledTimes(1); + }); + + it('handles complex (diamond) dependency graphs', () => { + const X = jest.fn(); + const A = jest.fn(); + const C = jest.fn(); + const D = jest.fn(); + const E = jest.fn(); + + // Form this dependency graph: + // + // X ───► A ───► C + // │ ▲ + // └────► D ─────┘ + // │ + // └────► E + X.dependencies = [A, D]; + A.dependencies = [C]; + D.dependencies = [C, E]; + + shortcuts.addExtension(X); + + expect(X).toHaveBeenCalledTimes(1); + expect(A).toHaveBeenCalledTimes(1); + expect(C).toHaveBeenCalledTimes(1); + expect(D).toHaveBeenCalledTimes(1); + expect(E).toHaveBeenCalledTimes(1); + }); + }); +}); |