diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-08 18:08:27 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-08 18:08:27 +0000 |
commit | 99c01aa6867b91b8d2279eb8d32794ea90d5dcdc (patch) | |
tree | ae36b06dd962230381080d9e2a31385cea5f8609 /spec/frontend/behaviors | |
parent | 5693fb6ba7d21ba7b79775543a3f195eb989664b (diff) | |
download | gitlab-ce-99c01aa6867b91b8d2279eb8d32794ea90d5dcdc.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend/behaviors')
-rw-r--r-- | spec/frontend/behaviors/autosize_spec.js | 20 | ||||
-rw-r--r-- | spec/frontend/behaviors/bind_in_out_spec.js | 10 | ||||
-rw-r--r-- | spec/frontend/behaviors/copy_as_gfm_spec.js | 125 | ||||
-rw-r--r-- | spec/frontend/behaviors/gl_emoji/unicode_support_map_spec.js | 52 | ||||
-rw-r--r-- | spec/frontend/behaviors/markdown/highlight_current_user_spec.js | 55 | ||||
-rw-r--r-- | spec/frontend/behaviors/requires_input_spec.js | 62 | ||||
-rw-r--r-- | spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js | 322 |
7 files changed, 638 insertions, 8 deletions
diff --git a/spec/frontend/behaviors/autosize_spec.js b/spec/frontend/behaviors/autosize_spec.js new file mode 100644 index 00000000000..59abae479d4 --- /dev/null +++ b/spec/frontend/behaviors/autosize_spec.js @@ -0,0 +1,20 @@ +import $ from 'jquery'; +import '~/behaviors/autosize'; + +function load() { + $(document).trigger('load'); +} + +describe('Autosize behavior', () => { + beforeEach(() => { + setFixtures('<textarea class="js-autosize" style="resize: vertical"></textarea>'); + }); + + it('does not overwrite the resize property', () => { + load(); + + expect($('textarea')).toHaveCss({ + resize: 'vertical', + }); + }); +}); diff --git a/spec/frontend/behaviors/bind_in_out_spec.js b/spec/frontend/behaviors/bind_in_out_spec.js index 923b6d372dd..92a68ddd387 100644 --- a/spec/frontend/behaviors/bind_in_out_spec.js +++ b/spec/frontend/behaviors/bind_in_out_spec.js @@ -163,14 +163,8 @@ describe('BindInOut', () => { describe('init', () => { beforeEach(() => { - // eslint-disable-next-line func-names - jest.spyOn(BindInOut.prototype, 'addEvents').mockImplementation(function() { - return this; - }); - // eslint-disable-next-line func-names - jest.spyOn(BindInOut.prototype, 'updateOut').mockImplementation(function() { - return this; - }); + jest.spyOn(BindInOut.prototype, 'addEvents').mockReturnThis(); + jest.spyOn(BindInOut.prototype, 'updateOut').mockReturnThis(); testContext.init = BindInOut.init({}, {}); }); diff --git a/spec/frontend/behaviors/copy_as_gfm_spec.js b/spec/frontend/behaviors/copy_as_gfm_spec.js new file mode 100644 index 00000000000..cf96ac488a8 --- /dev/null +++ b/spec/frontend/behaviors/copy_as_gfm_spec.js @@ -0,0 +1,125 @@ +import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; + +describe('CopyAsGFM', () => { + describe('CopyAsGFM.pasteGFM', () => { + function callPasteGFM() { + const e = { + originalEvent: { + clipboardData: { + getData(mimeType) { + // When GFM code is copied, we put the regular plain text + // on the clipboard as `text/plain`, and the GFM as `text/x-gfm`. + // This emulates the behavior of `getData` with that data. + if (mimeType === 'text/plain') { + return 'code'; + } + if (mimeType === 'text/x-gfm') { + return '`code`'; + } + return null; + }, + }, + }, + preventDefault() {}, + }; + + CopyAsGFM.pasteGFM(e); + } + + it('wraps pasted code when not already in code tags', () => { + jest.spyOn(window.gl.utils, 'insertText').mockImplementation((el, textFunc) => { + const insertedText = textFunc('This is code: ', ''); + + expect(insertedText).toEqual('`code`'); + }); + + callPasteGFM(); + }); + + it('does not wrap pasted code when already in code tags', () => { + jest.spyOn(window.gl.utils, 'insertText').mockImplementation((el, textFunc) => { + const insertedText = textFunc('This is code: `', '`'); + + expect(insertedText).toEqual('code'); + }); + + callPasteGFM(); + }); + }); + + describe('CopyAsGFM.copyGFM', () => { + // Stub getSelection to return a purpose-built object. + const stubSelection = (html, parentNode) => ({ + getRangeAt: () => ({ + commonAncestorContainer: { tagName: parentNode }, + cloneContents: () => { + const fragment = document.createDocumentFragment(); + const node = document.createElement('div'); + node.innerHTML = html; + Array.from(node.childNodes).forEach(item => fragment.appendChild(item)); + return fragment; + }, + }), + rangeCount: 1, + }); + + const clipboardData = { + setData() {}, + }; + + const simulateCopy = () => { + const e = { + originalEvent: { + clipboardData, + }, + preventDefault() {}, + stopPropagation() {}, + }; + CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); + return clipboardData; + }; + + beforeAll(done => { + initCopyAsGFM(); + + // Fake call to nodeToGfm so the import of lazy bundle happened + CopyAsGFM.nodeToGFM(document.createElement('div')) + .then(() => { + done(); + }) + .catch(done.fail); + }); + + beforeEach(() => jest.spyOn(clipboardData, 'setData')); + + describe('list handling', () => { + it('uses correct gfm for unordered lists', done => { + const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'UL'); + + window.getSelection = jest.fn(() => selection); + simulateCopy(); + + setImmediate(() => { + const expectedGFM = '* List Item1\n* List Item2'; + + expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM); + done(); + }); + }); + + it('uses correct gfm for ordered lists', done => { + const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'OL'); + + window.getSelection = jest.fn(() => selection); + simulateCopy(); + + setImmediate(() => { + const expectedGFM = '1. List Item1\n1. List Item2'; + + expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM); + done(); + }); + }); + }); + }); +}); diff --git a/spec/frontend/behaviors/gl_emoji/unicode_support_map_spec.js b/spec/frontend/behaviors/gl_emoji/unicode_support_map_spec.js new file mode 100644 index 00000000000..aaee9c30cac --- /dev/null +++ b/spec/frontend/behaviors/gl_emoji/unicode_support_map_spec.js @@ -0,0 +1,52 @@ +import getUnicodeSupportMap from '~/emoji/support/unicode_support_map'; +import AccessorUtilities from '~/lib/utils/accessor'; +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; + +describe('Unicode Support Map', () => { + useLocalStorageSpy(); + describe('getUnicodeSupportMap', () => { + const stringSupportMap = 'stringSupportMap'; + + beforeEach(() => { + jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockImplementation(() => {}); + jest.spyOn(JSON, 'parse').mockImplementation(() => {}); + jest.spyOn(JSON, 'stringify').mockReturnValue(stringSupportMap); + }); + + describe('if isLocalStorageAvailable is `true`', () => { + beforeEach(() => { + jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(true); + + getUnicodeSupportMap(); + }); + + it('should call .getItem and .setItem', () => { + const getArgs = window.localStorage.getItem.mock.calls; + const setArgs = window.localStorage.setItem.mock.calls; + + expect(getArgs[0][0]).toBe('gl-emoji-version'); + expect(getArgs[1][0]).toBe('gl-emoji-user-agent'); + + expect(setArgs[0][0]).toBe('gl-emoji-version'); + expect(setArgs[0][1]).toBe('0.2.0'); + expect(setArgs[1][0]).toBe('gl-emoji-user-agent'); + expect(setArgs[1][1]).toBe(navigator.userAgent); + expect(setArgs[2][0]).toBe('gl-emoji-unicode-support-map'); + expect(setArgs[2][1]).toBe(stringSupportMap); + }); + }); + + describe('if isLocalStorageAvailable is `false`', () => { + beforeEach(() => { + jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(false); + + getUnicodeSupportMap(); + }); + + it('should not call .getItem or .setItem', () => { + expect(window.localStorage.getItem.mock.calls.length).toBe(1); + expect(window.localStorage.setItem).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/spec/frontend/behaviors/markdown/highlight_current_user_spec.js b/spec/frontend/behaviors/markdown/highlight_current_user_spec.js new file mode 100644 index 00000000000..3305ddc412d --- /dev/null +++ b/spec/frontend/behaviors/markdown/highlight_current_user_spec.js @@ -0,0 +1,55 @@ +import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user'; + +describe('highlightCurrentUser', () => { + let rootElement; + let elements; + + beforeEach(() => { + setFixtures(` + <div id="dummy-root-element"> + <div data-user="1">@first</div> + <div data-user="2">@second</div> + </div> + `); + rootElement = document.getElementById('dummy-root-element'); + elements = rootElement.querySelectorAll('[data-user]'); + }); + + describe('without current user', () => { + beforeEach(() => { + window.gon = window.gon || {}; + window.gon.current_user_id = null; + }); + + afterEach(() => { + delete window.gon.current_user_id; + }); + + it('does not highlight the user', () => { + const initialHtml = rootElement.outerHTML; + + highlightCurrentUser(elements); + + expect(rootElement.outerHTML).toBe(initialHtml); + }); + }); + + describe('with current user', () => { + beforeEach(() => { + window.gon = window.gon || {}; + window.gon.current_user_id = 2; + }); + + afterEach(() => { + delete window.gon.current_user_id; + }); + + it('highlights current user', () => { + highlightCurrentUser(elements); + + expect(elements.length).toBe(2); + expect(elements[0]).not.toHaveClass('current-user'); + expect(elements[1]).toHaveClass('current-user'); + }); + }); +}); diff --git a/spec/frontend/behaviors/requires_input_spec.js b/spec/frontend/behaviors/requires_input_spec.js new file mode 100644 index 00000000000..617fe49b059 --- /dev/null +++ b/spec/frontend/behaviors/requires_input_spec.js @@ -0,0 +1,62 @@ +import $ from 'jquery'; +import '~/behaviors/requires_input'; + +describe('requiresInput', () => { + let submitButton; + preloadFixtures('branches/new_branch.html'); + + beforeEach(() => { + loadFixtures('branches/new_branch.html'); + submitButton = $('button[type="submit"]'); + }); + + it('disables submit when any field is required', () => { + $('.js-requires-input').requiresInput(); + + expect(submitButton).toBeDisabled(); + }); + + it('enables submit when no field is required', () => { + $('*[required=required]').prop('required', false); + $('.js-requires-input').requiresInput(); + + expect(submitButton).not.toBeDisabled(); + }); + + it('enables submit when all required fields are pre-filled', () => { + $('*[required=required]').remove(); + $('.js-requires-input').requiresInput(); + + expect($('.submit')).not.toBeDisabled(); + }); + + it('enables submit when all required fields receive input', () => { + $('.js-requires-input').requiresInput(); + $('#required1') + .val('input1') + .change(); + + expect(submitButton).toBeDisabled(); + + $('#optional1') + .val('input1') + .change(); + + expect(submitButton).toBeDisabled(); + + $('#required2') + .val('input2') + .change(); + $('#required3') + .val('input3') + .change(); + $('#required4') + .val('input4') + .change(); + $('#required5') + .val('1') + .change(); + + expect($('.submit')).not.toBeDisabled(); + }); +}); diff --git a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js new file mode 100644 index 00000000000..6391a544985 --- /dev/null +++ b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js @@ -0,0 +1,322 @@ +import $ from 'jquery'; +import 'mousetrap'; +import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; +import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; +import { getSelectedFragment } from '~/lib/utils/common_utils'; + +const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form'; + +jest.mock('~/lib/utils/common_utils', () => ({ + ...jest.requireActual('~/lib/utils/common_utils'), + getSelectedFragment: jest.fn().mockName('getSelectedFragment'), +})); + +describe('ShortcutsIssuable', () => { + const fixtureName = 'snippets/show.html'; + + preloadFixtures(fixtureName); + + beforeAll(done => { + initCopyAsGFM(); + + // Fake call to nodeToGfm so the import of lazy bundle happened + CopyAsGFM.nodeToGFM(document.createElement('div')) + .then(() => { + done(); + }) + .catch(done.fail); + }); + + beforeEach(() => { + loadFixtures(fixtureName); + $('body').append( + `<div class="js-main-target-form"> + <textarea class="js-vue-comment-form"></textarea> + </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; + }); + + describe('replyWithSelectedText', () => { + // Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML. + const stubSelection = (html, invalidNode) => { + getSelectedFragment.mockImplementation(() => { + const documentFragment = document.createDocumentFragment(); + const node = document.createElement('div'); + + node.innerHTML = html; + if (!invalidNode) node.className = 'md'; + + documentFragment.appendChild(node); + return documentFragment; + }); + }; + + describe('with empty selection', () => { + it('does not return an error', () => { + ShortcutsIssuable.replyWithSelectedText(true); + + expect($(FORM_SELECTOR).val()).toBe(''); + }); + + it('triggers `focus`', () => { + const spy = jest.spyOn(document.querySelector(FORM_SELECTOR), 'focus'); + ShortcutsIssuable.replyWithSelectedText(true); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('with any selection', () => { + beforeEach(() => { + stubSelection('<p>Selected text.</p>'); + }); + + it('leaves existing input intact', done => { + $(FORM_SELECTOR).val('This text was already here.'); + + expect($(FORM_SELECTOR).val()).toBe('This text was already here.'); + + ShortcutsIssuable.replyWithSelectedText(true); + + setImmediate(() => { + expect($(FORM_SELECTOR).val()).toBe( + 'This text was already here.\n\n> Selected text.\n\n', + ); + done(); + }); + }); + + it('triggers `input`', done => { + let triggered = false; + $(FORM_SELECTOR).on('input', () => { + triggered = true; + }); + + ShortcutsIssuable.replyWithSelectedText(true); + + setImmediate(() => { + expect(triggered).toBe(true); + done(); + }); + }); + + it('triggers `focus`', done => { + const spy = jest.spyOn(document.querySelector(FORM_SELECTOR), 'focus'); + ShortcutsIssuable.replyWithSelectedText(true); + + setImmediate(() => { + expect(spy).toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('with a one-line selection', () => { + it('quotes the selection', done => { + stubSelection('<p>This text has been selected.</p>'); + ShortcutsIssuable.replyWithSelectedText(true); + + setImmediate(() => { + expect($(FORM_SELECTOR).val()).toBe('> This text has been selected.\n\n'); + done(); + }); + }); + }); + + describe('with a multi-line selection', () => { + it('quotes the selected lines as a group', done => { + stubSelection( + '<p>Selected line one.</p>\n<p>Selected line two.</p>\n<p>Selected line three.</p>', + ); + ShortcutsIssuable.replyWithSelectedText(true); + + setImmediate(() => { + expect($(FORM_SELECTOR).val()).toBe( + '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n', + ); + done(); + }); + }); + }); + + describe('with an invalid selection', () => { + beforeEach(() => { + stubSelection('<p>Selected text.</p>', true); + }); + + it('does not add anything to the input', done => { + ShortcutsIssuable.replyWithSelectedText(true); + + setImmediate(() => { + expect($(FORM_SELECTOR).val()).toBe(''); + done(); + }); + }); + + it('triggers `focus`', done => { + const spy = jest.spyOn(document.querySelector(FORM_SELECTOR), 'focus'); + ShortcutsIssuable.replyWithSelectedText(true); + + setImmediate(() => { + expect(spy).toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('with a semi-valid selection', () => { + beforeEach(() => { + stubSelection('<div class="md">Selected text.</div><p>Invalid selected text.</p>', true); + }); + + it('only adds the valid part to the input', done => { + ShortcutsIssuable.replyWithSelectedText(true); + + setImmediate(() => { + expect($(FORM_SELECTOR).val()).toBe('> Selected text.\n\n'); + done(); + }); + }); + + it('triggers `focus`', done => { + const spy = jest.spyOn(document.querySelector(FORM_SELECTOR), 'focus'); + ShortcutsIssuable.replyWithSelectedText(true); + + setImmediate(() => { + expect(spy).toHaveBeenCalled(); + done(); + }); + }); + + it('triggers `input`', done => { + let triggered = false; + $(FORM_SELECTOR).on('input', () => { + triggered = true; + }); + + ShortcutsIssuable.replyWithSelectedText(true); + + setImmediate(() => { + expect(triggered).toBe(true); + done(); + }); + }); + }); + + describe('with a selection in a valid block', () => { + beforeEach(() => { + getSelectedFragment.mockImplementation(() => { + const documentFragment = document.createDocumentFragment(); + const node = document.createElement('div'); + const originalNode = document.createElement('body'); + originalNode.innerHTML = `<div class="issue"> + <div class="otherElem">Text...</div> + <div class="md"><p><em>Selected text.</em></p></div> + </div>`; + documentFragment.originalNodes = [originalNode.querySelector('em')]; + + node.innerHTML = '<em>Selected text.</em>'; + + documentFragment.appendChild(node); + + return documentFragment; + }); + }); + + it('adds the quoted selection to the input', done => { + ShortcutsIssuable.replyWithSelectedText(true); + + setImmediate(() => { + expect($(FORM_SELECTOR).val()).toBe('> *Selected text.*\n\n'); + done(); + }); + }); + + it('triggers `focus`', done => { + const spy = jest.spyOn(document.querySelector(FORM_SELECTOR), 'focus'); + ShortcutsIssuable.replyWithSelectedText(true); + + setImmediate(() => { + expect(spy).toHaveBeenCalled(); + done(); + }); + }); + + it('triggers `input`', done => { + let triggered = false; + $(FORM_SELECTOR).on('input', () => { + triggered = true; + }); + + ShortcutsIssuable.replyWithSelectedText(true); + + setImmediate(() => { + expect(triggered).toBe(true); + done(); + }); + }); + }); + + describe('with a selection in an invalid block', () => { + beforeEach(() => { + getSelectedFragment.mockImplementation(() => { + const documentFragment = document.createDocumentFragment(); + const node = document.createElement('div'); + const originalNode = document.createElement('body'); + originalNode.innerHTML = `<div class="issue"> + <div class="otherElem"><div><b>Selected text.</b></div></div> + <div class="md"><p><em>Valid text</em></p></div> + </div>`; + documentFragment.originalNodes = [originalNode.querySelector('b')]; + + node.innerHTML = '<b>Selected text.</b>'; + + documentFragment.appendChild(node); + + return documentFragment; + }); + }); + + it('does not add anything to the input', done => { + ShortcutsIssuable.replyWithSelectedText(true); + + setImmediate(() => { + expect($(FORM_SELECTOR).val()).toBe(''); + done(); + }); + }); + + it('triggers `focus`', done => { + const spy = jest.spyOn(document.querySelector(FORM_SELECTOR), 'focus'); + ShortcutsIssuable.replyWithSelectedText(true); + + setImmediate(() => { + expect(spy).toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('with a valid selection with no text content', () => { + it('returns the proper markdown', done => { + stubSelection('<img src="https://gitlab.com/logo.png" alt="logo" />'); + ShortcutsIssuable.replyWithSelectedText(true); + + setImmediate(() => { + expect($(FORM_SELECTOR).val()).toBe('> ![logo](https://gitlab.com/logo.png)\n\n'); + + done(); + }); + }); + }); + }); +}); |