diff options
Diffstat (limited to 'spec/javascripts')
35 files changed, 1423 insertions, 371 deletions
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js index 9d55c615450..1e9470970ff 100644 --- a/spec/javascripts/api_spec.js +++ b/spec/javascripts/api_spec.js @@ -49,6 +49,22 @@ describe('Api', () => { }); }); + describe('groupMembers', () => { + it('fetches group members', done => { + const groupId = '54321'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/members`; + const expectedData = [{ id: 7 }]; + mock.onGet(expectedUrl).reply(200, expectedData); + + Api.groupMembers(groupId) + .then(({ data }) => { + expect(data).toEqual(expectedData); + }) + .then(done) + .catch(done.fail); + }); + }); + describe('groups', () => { it('fetches groups', done => { const query = 'dummy query'; diff --git a/spec/javascripts/behaviors/copy_as_gfm_spec.js b/spec/javascripts/behaviors/copy_as_gfm_spec.js index 6179a02ce16..ca849f75860 100644 --- a/spec/javascripts/behaviors/copy_as_gfm_spec.js +++ b/spec/javascripts/behaviors/copy_as_gfm_spec.js @@ -1,4 +1,4 @@ -import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; +import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; describe('CopyAsGFM', () => { describe('CopyAsGFM.pasteGFM', () => { @@ -79,27 +79,46 @@ describe('CopyAsGFM', () => { 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(() => spyOn(clipboardData, 'setData')); describe('list handling', () => { - it('uses correct gfm for unordered lists', () => { + it('uses correct gfm for unordered lists', done => { const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'UL'); + spyOn(window, 'getSelection').and.returnValue(selection); simulateCopy(); - const expectedGFM = '* List Item1\n\n* List Item2'; + setTimeout(() => { + const expectedGFM = '* List Item1\n\n* List Item2'; - expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM); + expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM); + done(); + }); }); - it('uses correct gfm for ordered lists', () => { + it('uses correct gfm for ordered lists', done => { const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'OL'); + spyOn(window, 'getSelection').and.returnValue(selection); simulateCopy(); - const expectedGFM = '1. List Item1\n\n1. List Item2'; + setTimeout(() => { + const expectedGFM = '1. List Item1\n\n1. List Item2'; - expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM); + expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM); + done(); + }); }); }); }); diff --git a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js index fe827bb1e18..4843a0386b5 100644 --- a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js +++ b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js @@ -3,17 +3,26 @@ */ import $ from 'jquery'; -import initCopyAsGFM from '~/behaviors/markdown/copy_as_gfm'; +import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; -initCopyAsGFM(); - const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form'; describe('ShortcutsIssuable', function() { const fixtureName = 'snippets/show.html.raw'; 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( @@ -63,17 +72,22 @@ describe('ShortcutsIssuable', function() { stubSelection('<p>Selected text.</p>'); }); - it('leaves existing input intact', () => { + 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); - expect($(FORM_SELECTOR).val()).toBe('This text was already here.\n\n> Selected text.\n\n'); + setTimeout(() => { + expect($(FORM_SELECTOR).val()).toBe( + 'This text was already here.\n\n> Selected text.\n\n', + ); + done(); + }); }); - it('triggers `input`', () => { + it('triggers `input`', done => { let triggered = false; $(FORM_SELECTOR).on('input', () => { triggered = true; @@ -81,36 +95,48 @@ describe('ShortcutsIssuable', function() { ShortcutsIssuable.replyWithSelectedText(true); - expect(triggered).toBe(true); + setTimeout(() => { + expect(triggered).toBe(true); + done(); + }); }); - it('triggers `focus`', () => { + it('triggers `focus`', done => { const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus'); ShortcutsIssuable.replyWithSelectedText(true); - expect(spy).toHaveBeenCalled(); + setTimeout(() => { + expect(spy).toHaveBeenCalled(); + done(); + }); }); }); describe('with a one-line selection', () => { - it('quotes the selection', () => { + it('quotes the selection', done => { stubSelection('<p>This text has been selected.</p>'); ShortcutsIssuable.replyWithSelectedText(true); - expect($(FORM_SELECTOR).val()).toBe('> This text has been selected.\n\n'); + setTimeout(() => { + 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', () => { + 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); - expect($(FORM_SELECTOR).val()).toBe( - '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n', - ); + setTimeout(() => { + expect($(FORM_SELECTOR).val()).toBe( + '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n', + ); + done(); + }); }); }); @@ -119,17 +145,23 @@ describe('ShortcutsIssuable', function() { stubSelection('<p>Selected text.</p>', true); }); - it('does not add anything to the input', () => { + it('does not add anything to the input', done => { ShortcutsIssuable.replyWithSelectedText(true); - expect($(FORM_SELECTOR).val()).toBe(''); + setTimeout(() => { + expect($(FORM_SELECTOR).val()).toBe(''); + done(); + }); }); - it('triggers `focus`', () => { + it('triggers `focus`', done => { const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus'); ShortcutsIssuable.replyWithSelectedText(true); - expect(spy).toHaveBeenCalled(); + setTimeout(() => { + expect(spy).toHaveBeenCalled(); + done(); + }); }); }); @@ -138,20 +170,26 @@ describe('ShortcutsIssuable', function() { stubSelection('<div class="md">Selected text.</div><p>Invalid selected text.</p>', true); }); - it('only adds the valid part to the input', () => { + it('only adds the valid part to the input', done => { ShortcutsIssuable.replyWithSelectedText(true); - expect($(FORM_SELECTOR).val()).toBe('> Selected text.\n\n'); + setTimeout(() => { + expect($(FORM_SELECTOR).val()).toBe('> Selected text.\n\n'); + done(); + }); }); - it('triggers `focus`', () => { + it('triggers `focus`', done => { const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus'); ShortcutsIssuable.replyWithSelectedText(true); - expect(spy).toHaveBeenCalled(); + setTimeout(() => { + expect(spy).toHaveBeenCalled(); + done(); + }); }); - it('triggers `input`', () => { + it('triggers `input`', done => { let triggered = false; $(FORM_SELECTOR).on('input', () => { triggered = true; @@ -159,7 +197,10 @@ describe('ShortcutsIssuable', function() { ShortcutsIssuable.replyWithSelectedText(true); - expect(triggered).toBe(true); + setTimeout(() => { + expect(triggered).toBe(true); + done(); + }); }); }); @@ -183,20 +224,26 @@ describe('ShortcutsIssuable', function() { }); }); - it('adds the quoted selection to the input', () => { + it('adds the quoted selection to the input', done => { ShortcutsIssuable.replyWithSelectedText(true); - expect($(FORM_SELECTOR).val()).toBe('> *Selected text.*\n\n'); + setTimeout(() => { + expect($(FORM_SELECTOR).val()).toBe('> *Selected text.*\n\n'); + done(); + }); }); - it('triggers `focus`', () => { + it('triggers `focus`', done => { const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus'); ShortcutsIssuable.replyWithSelectedText(true); - expect(spy).toHaveBeenCalled(); + setTimeout(() => { + expect(spy).toHaveBeenCalled(); + done(); + }); }); - it('triggers `input`', () => { + it('triggers `input`', done => { let triggered = false; $(FORM_SELECTOR).on('input', () => { triggered = true; @@ -204,7 +251,10 @@ describe('ShortcutsIssuable', function() { ShortcutsIssuable.replyWithSelectedText(true); - expect(triggered).toBe(true); + setTimeout(() => { + expect(triggered).toBe(true); + done(); + }); }); }); @@ -228,17 +278,23 @@ describe('ShortcutsIssuable', function() { }); }); - it('does not add anything to the input', () => { + it('does not add anything to the input', done => { ShortcutsIssuable.replyWithSelectedText(true); - expect($(FORM_SELECTOR).val()).toBe(''); + setTimeout(() => { + expect($(FORM_SELECTOR).val()).toBe(''); + done(); + }); }); - it('triggers `focus`', () => { + it('triggers `focus`', done => { const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus'); ShortcutsIssuable.replyWithSelectedText(true); - expect(spy).toHaveBeenCalled(); + setTimeout(() => { + expect(spy).toHaveBeenCalled(); + done(); + }); }); }); }); diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js index 2f0385454d7..e886f962d2f 100644 --- a/spec/javascripts/diffs/components/compare_versions_spec.js +++ b/spec/javascripts/diffs/components/compare_versions_spec.js @@ -10,6 +10,10 @@ describe('CompareVersions', () => { const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 }; beforeEach(() => { + store.state.diffs.addedLines = 10; + store.state.diffs.removedLines = 20; + store.state.diffs.diffFiles.push('test'); + vm = createComponentWithStore(Vue.extend(CompareVersionsComponent), store, { mergeRequestDiffs: diffsMockData, mergeRequestDiff: diffsMockData[0], diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index b77907ff26f..787a81fd88f 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -24,6 +24,10 @@ describe('diff_file_header', () => { beforeEach(() => { const diffFile = diffDiscussionMock.diff_file; + + diffFile.added_lines = 2; + diffFile.removed_lines = 1; + props = { diffFile: { ...diffFile }, canCurrentUserFork: false, diff --git a/spec/javascripts/diffs/components/diff_stats_spec.js b/spec/javascripts/diffs/components/diff_stats_spec.js new file mode 100644 index 00000000000..984b3026209 --- /dev/null +++ b/spec/javascripts/diffs/components/diff_stats_spec.js @@ -0,0 +1,33 @@ +import { shallowMount } from '@vue/test-utils'; +import DiffStats from '~/diffs/components/diff_stats.vue'; + +describe('diff_stats', () => { + it('does not render a group if diffFileLengths is not passed in', () => { + const wrapper = shallowMount(DiffStats, { + propsData: { + addedLines: 1, + removedLines: 2, + }, + }); + const groups = wrapper.findAll('.diff-stats-group'); + + expect(groups.length).toBe(2); + }); + + it('shows amount of files changed, lines added and lines removed when passed all props', () => { + const wrapper = shallowMount(DiffStats, { + propsData: { + addedLines: 100, + removedLines: 200, + diffFilesLength: 300, + }, + }); + const additions = wrapper.find('icon-stub[name="file-addition"]').element.parentNode; + const deletions = wrapper.find('icon-stub[name="file-deletion"]').element.parentNode; + const filesChanged = wrapper.find('icon-stub[name="doc-code"]').element.parentNode; + + expect(additions.textContent).toContain('100'); + expect(deletions.textContent).toContain('200'); + expect(filesChanged.textContent).toContain('300'); + }); +}); diff --git a/spec/javascripts/diffs/components/tree_list_spec.js b/spec/javascripts/diffs/components/tree_list_spec.js index 08b0b4f9e45..9e556698f34 100644 --- a/spec/javascripts/diffs/components/tree_list_spec.js +++ b/spec/javascripts/diffs/components/tree_list_spec.js @@ -35,12 +35,6 @@ describe('Diffs tree list component', () => { vm.$destroy(); }); - it('renders diff stats', () => { - expect(vm.$el.textContent).toContain('1 changed file'); - expect(vm.$el.textContent).toContain('10 additions'); - expect(vm.$el.textContent).toContain('20 deletions'); - }); - it('renders empty text', () => { expect(vm.$el.textContent).toContain('No files found'); }); @@ -83,17 +77,6 @@ describe('Diffs tree list component', () => { expect(vm.$el.querySelectorAll('.file-row')[1].textContent).toContain('app'); }); - it('filters tree list to blobs matching search', done => { - vm.search = 'app/index'; - - vm.$nextTick(() => { - expect(vm.$el.querySelectorAll('.file-row').length).toBe(1); - expect(vm.$el.querySelectorAll('.file-row')[0].textContent).toContain('index.js'); - - done(); - }); - }); - it('calls toggleTreeOpen when clicking folder', () => { spyOn(vm.$store, 'dispatch').and.stub(); @@ -130,14 +113,4 @@ describe('Diffs tree list component', () => { }); }); }); - - describe('clearSearch', () => { - it('resets search', () => { - vm.search = 'test'; - - vm.$el.querySelector('.tree-list-clear-icon').click(); - - expect(vm.search).toBe(''); - }); - }); }); diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js index 190ca1230ca..4f69dc92ab8 100644 --- a/spec/javascripts/diffs/store/getters_spec.js +++ b/spec/javascripts/diffs/store/getters_spec.js @@ -242,7 +242,11 @@ describe('Diffs Module Getters', () => { }, }; - expect(getters.allBlobs(localState)).toEqual([ + expect( + getters.allBlobs(localState, { + flatBlobsList: getters.flatBlobsList(localState), + }), + ).toEqual([ { isHeader: true, path: '/', diff --git a/spec/javascripts/helpers/vue_test_utils_helper.js b/spec/javascripts/helpers/vue_test_utils_helper.js new file mode 100644 index 00000000000..19e27388eeb --- /dev/null +++ b/spec/javascripts/helpers/vue_test_utils_helper.js @@ -0,0 +1,19 @@ +/* eslint-disable import/prefer-default-export */ + +const vNodeContainsText = (vnode, text) => + (vnode.text && vnode.text.includes(text)) || + (vnode.children && vnode.children.filter(child => vNodeContainsText(child, text)).length); + +/** + * Determines whether a `shallowMount` Wrapper contains text + * within one of it's slots. This will also work on Wrappers + * acquired with `find()`, but only if it's parent Wrapper + * was shallowMounted. + * NOTE: Prefer checking the rendered output of a component + * wherever possible using something like `text()` instead. + * @param {Wrapper} shallowWrapper - Vue test utils wrapper (shallowMounted) + * @param {String} slotName + * @param {String} text + */ +export const shallowWrapperContainsSlotText = (shallowWrapper, slotName, text) => + !!shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length; diff --git a/spec/javascripts/helpers/vue_test_utils_helper_spec.js b/spec/javascripts/helpers/vue_test_utils_helper_spec.js new file mode 100644 index 00000000000..41714066da5 --- /dev/null +++ b/spec/javascripts/helpers/vue_test_utils_helper_spec.js @@ -0,0 +1,48 @@ +import { shallowMount } from '@vue/test-utils'; +import { shallowWrapperContainsSlotText } from './vue_test_utils_helper'; + +describe('Vue test utils helpers', () => { + describe('shallowWrapperContainsSlotText', () => { + const mockText = 'text'; + const mockSlot = `<div>${mockText}</div>`; + let mockComponent; + + beforeEach(() => { + mockComponent = shallowMount( + { + render(h) { + h(`<div>mockedComponent</div>`); + }, + }, + { + slots: { + default: mockText, + namedSlot: mockSlot, + }, + }, + ); + }); + + it('finds text within shallowWrapper default slot', () => { + expect(shallowWrapperContainsSlotText(mockComponent, 'default', mockText)).toBe(true); + }); + + it('finds text within shallowWrapper named slot', () => { + expect(shallowWrapperContainsSlotText(mockComponent, 'namedSlot', mockText)).toBe(true); + }); + + it('returns false when text is not present', () => { + const searchText = 'absent'; + + expect(shallowWrapperContainsSlotText(mockComponent, 'default', searchText)).toBe(false); + expect(shallowWrapperContainsSlotText(mockComponent, 'namedSlot', searchText)).toBe(false); + }); + + it('searches with case-sensitivity', () => { + const searchText = mockText.toUpperCase(); + + expect(shallowWrapperContainsSlotText(mockComponent, 'default', searchText)).toBe(false); + expect(shallowWrapperContainsSlotText(mockComponent, 'namedSlot', searchText)).toBe(false); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js index 55f40be0e4e..dc5790f6562 100644 --- a/spec/javascripts/ide/components/ide_spec.js +++ b/spec/javascripts/ide/components/ide_spec.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import Mousetrap from 'mousetrap'; import store from '~/ide/stores'; import ide from '~/ide/components/ide.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; @@ -72,73 +71,6 @@ describe('ide component', () => { }); }); - describe('file finder', () => { - beforeEach(done => { - spyOn(vm, 'toggleFileFinder'); - - vm.$store.state.fileFindVisible = true; - - vm.$nextTick(done); - }); - - it('calls toggleFileFinder on `t` key press', done => { - Mousetrap.trigger('t'); - - vm.$nextTick() - .then(() => { - expect(vm.toggleFileFinder).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - - it('calls toggleFileFinder on `command+p` key press', done => { - Mousetrap.trigger('command+p'); - - vm.$nextTick() - .then(() => { - expect(vm.toggleFileFinder).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - - it('calls toggleFileFinder on `ctrl+p` key press', done => { - Mousetrap.trigger('ctrl+p'); - - vm.$nextTick() - .then(() => { - expect(vm.toggleFileFinder).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - - it('always allows `command+p` to trigger toggleFileFinder', () => { - expect( - vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'command+p'), - ).toBe(false); - }); - - it('always allows `ctrl+p` to trigger toggleFileFinder', () => { - expect( - vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'ctrl+p'), - ).toBe(false); - }); - - it('onlys handles `t` when focused in input-field', () => { - expect( - vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'), - ).toBe(true); - }); - - it('stops callback in monaco editor', () => { - setFixtures('<div class="inputarea"></div>'); - - expect(vm.mousetrapStopCallback(null, document.querySelector('.inputarea'), 't')).toBe(true); - }); - }); - it('shows error message when set', done => { expect(vm.$el.querySelector('.flash-container')).toBe(null); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index e3fd9604474..3eff3f655ee 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -232,6 +232,21 @@ describe('common_utils', () => { }); }); + describe('debounceByAnimationFrame', () => { + it('debounces a function to allow a maximum of one call per animation frame', done => { + const spy = jasmine.createSpy('spy'); + const debouncedSpy = commonUtils.debounceByAnimationFrame(spy); + window.requestAnimationFrame(() => { + debouncedSpy(); + debouncedSpy(); + window.requestAnimationFrame(() => { + expect(spy).toHaveBeenCalledTimes(1); + done(); + }); + }); + }); + }); + describe('getParameterByName', () => { beforeEach(() => { window.history.pushState({}, null, '?scope=all&p=2'); diff --git a/spec/javascripts/lib/utils/file_upload_spec.js b/spec/javascripts/lib/utils/file_upload_spec.js index 92c9cc70aaf..8f7092f63de 100644 --- a/spec/javascripts/lib/utils/file_upload_spec.js +++ b/spec/javascripts/lib/utils/file_upload_spec.js @@ -9,28 +9,56 @@ describe('File upload', () => { <span class="js-filename"></span> </form> `); + }); + + describe('when there is a matching button and input', () => { + beforeEach(() => { + fileUpload('.js-button', '.js-input'); + }); + + it('clicks file input after clicking button', () => { + const btn = document.querySelector('.js-button'); + const input = document.querySelector('.js-input'); + + spyOn(input, 'click'); + + btn.click(); + + expect(input.click).toHaveBeenCalled(); + }); + + it('updates file name text', () => { + const input = document.querySelector('.js-input'); - fileUpload('.js-button', '.js-input'); + input.value = 'path/to/file/index.js'; + + input.dispatchEvent(new CustomEvent('change')); + + expect(document.querySelector('.js-filename').textContent).toEqual('index.js'); + }); }); - it('clicks file input after clicking button', () => { - const btn = document.querySelector('.js-button'); + it('fails gracefully when there is no matching button', () => { const input = document.querySelector('.js-input'); + const btn = document.querySelector('.js-button'); + fileUpload('.js-not-button', '.js-input'); spyOn(input, 'click'); btn.click(); - expect(input.click).toHaveBeenCalled(); + expect(input.click).not.toHaveBeenCalled(); }); - it('updates file name text', () => { + it('fails gracefully when there is no matching input', () => { const input = document.querySelector('.js-input'); + const btn = document.querySelector('.js-button'); + fileUpload('.js-button', '.js-not-input'); - input.value = 'path/to/file/index.js'; + spyOn(input, 'click'); - input.dispatchEvent(new CustomEvent('change')); + btn.click(); - expect(document.querySelector('.js-filename').textContent).toEqual('index.js'); + expect(input.click).not.toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/lib/utils/grammar_spec.js b/spec/javascripts/lib/utils/grammar_spec.js new file mode 100644 index 00000000000..377b2ffb48c --- /dev/null +++ b/spec/javascripts/lib/utils/grammar_spec.js @@ -0,0 +1,35 @@ +import * as grammar from '~/lib/utils/grammar'; + +describe('utils/grammar', () => { + describe('toNounSeriesText', () => { + it('with empty items returns empty string', () => { + expect(grammar.toNounSeriesText([])).toBe(''); + }); + + it('with single item returns item', () => { + const items = ['Lorem Ipsum']; + + expect(grammar.toNounSeriesText(items)).toBe(items[0]); + }); + + it('with 2 items returns item1 and item2', () => { + const items = ['Dolar', 'Sit Amit']; + + expect(grammar.toNounSeriesText(items)).toBe(`${items[0]} and ${items[1]}`); + }); + + it('with 3 items returns comma separated series', () => { + const items = ['Lorem', 'Ipsum', 'dolar']; + const expected = 'Lorem, Ipsum, and dolar'; + + expect(grammar.toNounSeriesText(items)).toBe(expected); + }); + + it('with 6 items returns comma separated series', () => { + const items = ['Lorem', 'ipsum', 'dolar', 'sit', 'amit', 'consectetur']; + const expected = 'Lorem, ipsum, dolar, sit, amit, and consectetur'; + + expect(grammar.toNounSeriesText(items)).toBe(expected); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/icon_utils_spec.js b/spec/javascripts/lib/utils/icon_utils_spec.js new file mode 100644 index 00000000000..3fd3940efe8 --- /dev/null +++ b/spec/javascripts/lib/utils/icon_utils_spec.js @@ -0,0 +1,67 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import * as iconUtils from '~/lib/utils/icon_utils'; + +describe('Icon utils', () => { + describe('getSvgIconPathContent', () => { + let spriteIcons; + + beforeAll(() => { + spriteIcons = gon.sprite_icons; + gon.sprite_icons = 'mockSpriteIconsEndpoint'; + }); + + afterAll(() => { + gon.sprite_icons = spriteIcons; + }); + + let axiosMock; + let mockEndpoint; + let getIcon; + const mockName = 'mockIconName'; + const mockPath = 'mockPath'; + + beforeEach(() => { + axiosMock = new MockAdapter(axios); + mockEndpoint = axiosMock.onGet(gon.sprite_icons); + getIcon = iconUtils.getSvgIconPathContent(mockName); + }); + + afterEach(() => { + axiosMock.restore(); + }); + + it('extracts svg icon path content from sprite icons', done => { + mockEndpoint.replyOnce( + 200, + `<svg><symbol id="${mockName}"><path d="${mockPath}"/></symbol></svg>`, + ); + getIcon + .then(path => { + expect(path).toBe(mockPath); + done(); + }) + .catch(done.fail); + }); + + it('returns null if icon path content does not exist', done => { + mockEndpoint.replyOnce(200, ``); + getIcon + .then(path => { + expect(path).toBe(null); + done(); + }) + .catch(done.fail); + }); + + it('returns null if an http error occurs', done => { + mockEndpoint.replyOnce(500); + getIcon + .then(path => { + expect(path).toBe(null); + done(); + }) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js new file mode 100644 index 00000000000..0b36fc9f5f7 --- /dev/null +++ b/spec/javascripts/monitoring/charts/area_spec.js @@ -0,0 +1,220 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlAreaChart } from '@gitlab/ui/dist/charts'; +import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper'; +import Area from '~/monitoring/components/charts/area.vue'; +import MonitoringStore from '~/monitoring/stores/monitoring_store'; +import MonitoringMock, { deploymentData } from '../mock_data'; + +describe('Area component', () => { + const mockWidgets = 'mockWidgets'; + let mockGraphData; + let areaChart; + let spriteSpy; + + beforeEach(() => { + const store = new MonitoringStore(); + store.storeMetrics(MonitoringMock.data); + store.storeDeploymentData(deploymentData); + + [mockGraphData] = store.groups[0].metrics; + + areaChart = shallowMount(Area, { + propsData: { + graphData: mockGraphData, + containerWidth: 0, + deploymentData: store.deploymentData, + }, + slots: { + default: mockWidgets, + }, + }); + + spriteSpy = spyOnDependency(Area, 'getSvgIconPathContent').and.callFake( + () => new Promise(resolve => resolve()), + ); + }); + + afterEach(() => { + areaChart.destroy(); + }); + + it('renders chart title', () => { + expect(areaChart.find({ ref: 'graphTitle' }).text()).toBe(mockGraphData.title); + }); + + it('contains graph widgets from slot', () => { + expect(areaChart.find({ ref: 'graphWidgets' }).text()).toBe(mockWidgets); + }); + + describe('wrapped components', () => { + describe('GitLab UI area chart', () => { + let glAreaChart; + + beforeEach(() => { + glAreaChart = areaChart.find(GlAreaChart); + }); + + it('is a Vue instance', () => { + expect(glAreaChart.isVueInstance()).toBe(true); + }); + + it('receives data properties needed for proper chart render', () => { + const props = glAreaChart.props(); + + expect(props.data).toBe(areaChart.vm.chartData); + expect(props.option).toBe(areaChart.vm.chartOptions); + expect(props.formatTooltipText).toBe(areaChart.vm.formatTooltipText); + expect(props.thresholds).toBe(areaChart.props('alertData')); + }); + + it('recieves a tooltip title', () => { + const mockTitle = 'mockTitle'; + areaChart.vm.tooltip.title = mockTitle; + + expect(shallowWrapperContainsSlotText(glAreaChart, 'tooltipTitle', mockTitle)).toBe(true); + }); + + it('recieves tooltip content', () => { + const mockContent = 'mockContent'; + areaChart.vm.tooltip.content = mockContent; + + expect(shallowWrapperContainsSlotText(glAreaChart, 'tooltipContent', mockContent)).toBe( + true, + ); + }); + + describe('when tooltip is showing deployment data', () => { + beforeEach(() => { + areaChart.vm.tooltip.isDeployment = true; + }); + + it('uses deployment title', () => { + expect(shallowWrapperContainsSlotText(glAreaChart, 'tooltipTitle', 'Deployed')).toBe( + true, + ); + }); + + it('renders commit sha in tooltip content', () => { + const mockSha = 'mockSha'; + areaChart.vm.tooltip.sha = mockSha; + + expect(shallowWrapperContainsSlotText(glAreaChart, 'tooltipContent', mockSha)).toBe(true); + }); + }); + }); + }); + + describe('methods', () => { + describe('formatTooltipText', () => { + const mockDate = deploymentData[0].created_at; + const generateSeriesData = type => ({ + seriesData: [ + { + componentSubType: type, + value: [mockDate, 5.55555], + }, + ], + value: mockDate, + }); + + describe('series is of line type', () => { + beforeEach(() => { + areaChart.vm.formatTooltipText(generateSeriesData('line')); + }); + + it('formats tooltip title', () => { + expect(areaChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM'); + }); + + it('formats tooltip content', () => { + expect(areaChart.vm.tooltip.content).toBe('CPU (Cores) 5.556'); + }); + }); + + describe('series is of scatter type', () => { + beforeEach(() => { + areaChart.vm.formatTooltipText(generateSeriesData('scatter')); + }); + + it('formats tooltip title', () => { + expect(areaChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM'); + }); + + it('formats tooltip sha', () => { + expect(areaChart.vm.tooltip.sha).toBe('f5bcd1d9'); + }); + }); + }); + + describe('getScatterSymbol', () => { + beforeEach(() => { + areaChart.vm.getScatterSymbol(); + }); + + it('gets rocket svg path content for use as deployment data symbol', () => { + expect(spriteSpy).toHaveBeenCalledWith('rocket'); + }); + }); + + describe('onResize', () => { + const mockWidth = 233; + const mockHeight = 144; + + beforeEach(() => { + spyOn(Element.prototype, 'getBoundingClientRect').and.callFake(() => ({ + width: mockWidth, + height: mockHeight, + })); + areaChart.vm.onResize(); + }); + + it('sets area chart width', () => { + expect(areaChart.vm.width).toBe(mockWidth); + }); + + it('sets area chart height', () => { + expect(areaChart.vm.height).toBe(mockHeight); + }); + }); + }); + + describe('computed', () => { + describe('chartData', () => { + it('utilizes all data points', () => { + expect(Object.keys(areaChart.vm.chartData)).toEqual(['Cores']); + expect(areaChart.vm.chartData.Cores.length).toBe(297); + }); + + it('creates valid data', () => { + const data = areaChart.vm.chartData.Cores; + + expect( + data.filter(([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number') + .length, + ).toBe(data.length); + }); + }); + + describe('scatterSeries', () => { + it('utilizes deployment data', () => { + expect(areaChart.vm.scatterSeries.data).toEqual([ + ['2017-05-31T21:23:37.881Z', 0], + ['2017-05-30T20:08:04.629Z', 0], + ['2017-05-30T17:42:38.409Z', 0], + ]); + }); + }); + + describe('xAxisLabel', () => { + it('constructs a label for the chart x-axis', () => { + expect(areaChart.vm.xAxisLabel).toBe('Core Usage'); + }); + }); + + describe('yAxisLabel', () => { + it('constructs a label for the chart y-axis', () => { + expect(areaChart.vm.yAxisLabel).toBe('CPU (Cores)'); + }); + }); + }); +}); diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index 565b87de248..b1778029a77 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -25,15 +25,22 @@ export default propsData; describe('Dashboard', () => { let DashboardComponent; + let mock; beforeEach(() => { setFixtures(` <div class="prometheus-graphs"></div> - <div class="nav-sidebar"></div> + <div class="layout-page"></div> `); + + mock = new MockAdapter(axios); DashboardComponent = Vue.extend(Dashboard); }); + afterEach(() => { + mock.restore(); + }); + describe('no metrics are available yet', () => { it('shows a getting started empty state when no metrics are present', () => { const component = new DashboardComponent({ @@ -47,16 +54,10 @@ describe('Dashboard', () => { }); describe('requests information to the server', () => { - let mock; beforeEach(() => { - mock = new MockAdapter(axios); mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); }); - afterEach(() => { - mock.restore(); - }); - it('shows up a loading state', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), @@ -152,28 +153,25 @@ describe('Dashboard', () => { }); describe('when the window resizes', () => { - let mock; beforeEach(() => { - mock = new MockAdapter(axios); mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); jasmine.clock().install(); }); afterEach(() => { - mock.restore(); jasmine.clock().uninstall(); }); - it('rerenders the dashboard when the sidebar is resized', done => { + it('sets elWidth to page width when the sidebar is resized', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), propsData: { ...propsData, hasMetrics: true, showPanels: false }, }); - expect(component.forceRedraw).toEqual(0); + expect(component.elWidth).toEqual(0); - const navSidebarEl = document.querySelector('.nav-sidebar'); - navSidebarEl.classList.add('nav-sidebar-collapsed'); + const pageLayoutEl = document.querySelector('.layout-page'); + pageLayoutEl.classList.add('page-with-icon-sidebar'); Vue.nextTick() .then(() => { @@ -181,7 +179,7 @@ describe('Dashboard', () => { return Vue.nextTick(); }) .then(() => { - expect(component.forceRedraw).toEqual(component.elWidth); + expect(component.elWidth).toEqual(pageLayoutEl.clientWidth); done(); }) .catch(done.fail); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index b4e2cd75d47..ffc7148fde2 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -326,6 +326,7 @@ export const metricsGroupsAPIResponse = { { id: 6, title: 'CPU usage', + y_label: 'CPU', weight: 1, queries: [ { diff --git a/spec/javascripts/notes/components/discussion_reply_placeholder_spec.js b/spec/javascripts/notes/components/discussion_reply_placeholder_spec.js new file mode 100644 index 00000000000..07a366cf339 --- /dev/null +++ b/spec/javascripts/notes/components/discussion_reply_placeholder_spec.js @@ -0,0 +1,34 @@ +import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; + +const localVue = createLocalVue(); + +describe('ReplyPlaceholder', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(ReplyPlaceholder, { + localVue, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('emits onClick even on button click', () => { + const button = wrapper.find({ ref: 'button' }); + + button.trigger('click'); + + expect(wrapper.emitted()).toEqual({ + onClick: [[]], + }); + }); + + it('should render reply button', () => { + const button = wrapper.find({ ref: 'button' }); + + expect(button.text()).toEqual('Reply...'); + }); +}); diff --git a/spec/javascripts/notes/components/note_actions/reply_button_spec.js b/spec/javascripts/notes/components/note_actions/reply_button_spec.js new file mode 100644 index 00000000000..11e1664a3f4 --- /dev/null +++ b/spec/javascripts/notes/components/note_actions/reply_button_spec.js @@ -0,0 +1,46 @@ +import Vuex from 'vuex'; +import { createLocalVue, mount } from '@vue/test-utils'; +import ReplyButton from '~/notes/components/note_actions/reply_button.vue'; + +describe('ReplyButton', () => { + const noteId = 'dummy-note-id'; + + let wrapper; + let convertToDiscussion; + + beforeEach(() => { + const localVue = createLocalVue(); + convertToDiscussion = jasmine.createSpy('convertToDiscussion'); + + localVue.use(Vuex); + const store = new Vuex.Store({ + actions: { + convertToDiscussion, + }, + }); + + wrapper = mount(ReplyButton, { + propsData: { + noteId, + }, + store, + sync: false, + localVue, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('dispatches convertToDiscussion with note ID on click', () => { + const button = wrapper.find({ ref: 'button' }); + + button.trigger('click'); + + expect(convertToDiscussion).toHaveBeenCalledTimes(1); + const [, payload] = convertToDiscussion.calls.argsFor(0); + + expect(payload).toBe(noteId); + }); +}); diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js index b102b7aecf7..0c1962912b4 100644 --- a/spec/javascripts/notes/components/note_actions_spec.js +++ b/spec/javascripts/notes/components/note_actions_spec.js @@ -2,14 +2,38 @@ import Vue from 'vue'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import createStore from '~/notes/stores'; import noteActions from '~/notes/components/note_actions.vue'; +import { TEST_HOST } from 'spec/test_constants'; import { userDataMock } from '../mock_data'; describe('noteActions', () => { let wrapper; let store; + let props; + + const createWrapper = propsData => { + const localVue = createLocalVue(); + return shallowMount(noteActions, { + store, + propsData, + localVue, + sync: false, + }); + }; beforeEach(() => { store = createStore(); + props = { + accessLevel: 'Maintainer', + authorId: 26, + canDelete: true, + canEdit: true, + canAwardEmoji: true, + canReportAsAbuse: true, + noteId: '539', + noteUrl: `${TEST_HOST}/group/project/merge_requests/1#note_1`, + reportAbusePath: `${TEST_HOST}/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26`, + showReply: false, + }; }); afterEach(() => { @@ -17,31 +41,10 @@ describe('noteActions', () => { }); describe('user is logged in', () => { - let props; - beforeEach(() => { - props = { - accessLevel: 'Maintainer', - authorId: 26, - canDelete: true, - canEdit: true, - canAwardEmoji: true, - canReportAsAbuse: true, - noteId: '539', - noteUrl: 'https://localhost:3000/group/project/merge_requests/1#note_1', - reportAbusePath: - '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', - }; - store.dispatch('setUserData', userDataMock); - const localVue = createLocalVue(); - wrapper = shallowMount(noteActions, { - store, - propsData: props, - localVue, - sync: false, - }); + wrapper = createWrapper(props); }); it('should render access level badge', () => { @@ -91,28 +94,14 @@ describe('noteActions', () => { }); describe('user is not logged in', () => { - let props; - beforeEach(() => { store.dispatch('setUserData', {}); - props = { - accessLevel: 'Maintainer', - authorId: 26, + wrapper = createWrapper({ + ...props, canDelete: false, canEdit: false, canAwardEmoji: false, canReportAsAbuse: false, - noteId: '539', - noteUrl: 'https://localhost:3000/group/project/merge_requests/1#note_1', - reportAbusePath: - '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', - }; - const localVue = createLocalVue(); - wrapper = shallowMount(noteActions, { - store, - propsData: props, - localVue, - sync: false, }); }); @@ -124,4 +113,88 @@ describe('noteActions', () => { expect(wrapper.find('.more-actions').exists()).toBe(false); }); }); + + describe('with feature flag replyToIndividualNotes enabled', () => { + beforeEach(() => { + gon.features = { + replyToIndividualNotes: true, + }; + }); + + afterEach(() => { + gon.features = {}; + }); + + describe('for showReply = true', () => { + beforeEach(() => { + wrapper = createWrapper({ + ...props, + showReply: true, + }); + }); + + it('shows a reply button', () => { + const replyButton = wrapper.find({ ref: 'replyButton' }); + + expect(replyButton.exists()).toBe(true); + }); + }); + + describe('for showReply = false', () => { + beforeEach(() => { + wrapper = createWrapper({ + ...props, + showReply: false, + }); + }); + + it('does not show a reply button', () => { + const replyButton = wrapper.find({ ref: 'replyButton' }); + + expect(replyButton.exists()).toBe(false); + }); + }); + }); + + describe('with feature flag replyToIndividualNotes disabled', () => { + beforeEach(() => { + gon.features = { + replyToIndividualNotes: false, + }; + }); + + afterEach(() => { + gon.features = {}; + }); + + describe('for showReply = true', () => { + beforeEach(() => { + wrapper = createWrapper({ + ...props, + showReply: true, + }); + }); + + it('does not show a reply button', () => { + const replyButton = wrapper.find({ ref: 'replyButton' }); + + expect(replyButton.exists()).toBe(false); + }); + }); + + describe('for showReply = false', () => { + beforeEach(() => { + wrapper = createWrapper({ + ...props, + showReply: false, + }); + }); + + it('does not show a reply button', () => { + const replyButton = wrapper.find({ ref: 'replyButton' }); + + expect(replyButton.exists()).toBe(false); + }); + }); + }); }); diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 22bee049f9c..d5c0bf6b25d 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -1,57 +1,49 @@ import $ from 'jquery'; import _ from 'underscore'; import Vue from 'vue'; -import notesApp from '~/notes/components/notes_app.vue'; +import { mount, createLocalVue } from '@vue/test-utils'; +import NotesApp from '~/notes/components/notes_app.vue'; import service from '~/notes/services/notes_service'; import createStore from '~/notes/stores'; import '~/behaviors/markdown/render_gfm'; -import { mountComponentWithStore } from 'spec/helpers'; import * as mockData from '../mock_data'; -const vueMatchers = { - toIncludeElement() { - return { - compare(vm, selector) { - const result = { - pass: vm.$el.querySelector(selector) !== null, - }; - return result; - }, - }; - }, -}; - describe('note_app', () => { let mountComponent; - let vm; + let wrapper; let store; beforeEach(() => { - jasmine.addMatchers(vueMatchers); $('body').attr('data-page', 'projects:merge_requests:show'); - setFixtures('<div class="js-vue-notes-event"><div id="app"></div></div>'); - - const IssueNotesApp = Vue.extend(notesApp); - store = createStore(); mountComponent = data => { - const props = data || { + const propsData = data || { noteableData: mockData.noteableDataMock, notesData: mockData.notesDataMock, userData: mockData.userDataMock, }; - - return mountComponentWithStore(IssueNotesApp, { - props, - store, - el: document.getElementById('app'), - }); + const localVue = createLocalVue(); + + return mount( + { + components: { + NotesApp, + }, + template: '<div class="js-vue-notes-event"><notes-app v-bind="$attrs" /></div>', + }, + { + propsData, + store, + localVue, + sync: false, + }, + ); }; }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); describe('set data', () => { @@ -65,7 +57,7 @@ describe('note_app', () => { beforeEach(() => { Vue.http.interceptors.push(responseInterceptor); - vm = mountComponent(); + wrapper = mountComponent(); }); afterEach(() => { @@ -73,26 +65,26 @@ describe('note_app', () => { }); it('should set notes data', () => { - expect(vm.$store.state.notesData).toEqual(mockData.notesDataMock); + expect(store.state.notesData).toEqual(mockData.notesDataMock); }); it('should set issue data', () => { - expect(vm.$store.state.noteableData).toEqual(mockData.noteableDataMock); + expect(store.state.noteableData).toEqual(mockData.noteableDataMock); }); it('should set user data', () => { - expect(vm.$store.state.userData).toEqual(mockData.userDataMock); + expect(store.state.userData).toEqual(mockData.userDataMock); }); it('should fetch discussions', () => { - expect(vm.$store.state.discussions).toEqual([]); + expect(store.state.discussions).toEqual([]); }); }); describe('render', () => { beforeEach(() => { Vue.http.interceptors.push(mockData.individualNoteInterceptor); - vm = mountComponent(); + wrapper = mountComponent(); }); afterEach(() => { @@ -107,51 +99,50 @@ describe('note_app', () => { setTimeout(() => { expect( - vm.$el.querySelector('.main-notes-list .note-header-author-name').textContent.trim(), + wrapper + .find('.main-notes-list .note-header-author-name') + .text() + .trim(), ).toEqual(note.author.name); - expect(vm.$el.querySelector('.main-notes-list .note-text').innerHTML).toEqual( - note.note_html, - ); + expect(wrapper.find('.main-notes-list .note-text').html()).toContain(note.note_html); done(); }, 0); }); it('should render form', () => { - expect(vm.$el.querySelector('.js-main-target-form').tagName).toEqual('FORM'); - expect( - vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), - ).toEqual('Write a comment or drag your files here…'); + expect(wrapper.find('.js-main-target-form').name()).toEqual('form'); + expect(wrapper.find('.js-main-target-form textarea').attributes('placeholder')).toEqual( + 'Write a comment or drag your files here…', + ); }); it('should not render form when commenting is disabled', () => { store.state.commentsDisabled = true; - vm = mountComponent(); + wrapper = mountComponent(); - expect(vm.$el.querySelector('.js-main-target-form')).toEqual(null); + expect(wrapper.find('.js-main-target-form').exists()).toBe(false); }); it('should render form comment button as disabled', () => { - expect(vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled')).toEqual( - 'disabled', - ); + expect(wrapper.find('.js-note-new-discussion').attributes('disabled')).toEqual('disabled'); }); }); describe('while fetching data', () => { beforeEach(() => { - vm = mountComponent(); + wrapper = mountComponent(); }); it('renders skeleton notes', () => { - expect(vm).toIncludeElement('.animation-container'); + expect(wrapper.find('.animation-container').exists()).toBe(true); }); it('should render form', () => { - expect(vm.$el.querySelector('.js-main-target-form').tagName).toEqual('FORM'); - expect( - vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), - ).toEqual('Write a comment or drag your files here…'); + expect(wrapper.find('.js-main-target-form').name()).toEqual('form'); + expect(wrapper.find('.js-main-target-form textarea').attributes('placeholder')).toEqual( + 'Write a comment or drag your files here…', + ); }); }); @@ -160,9 +151,9 @@ describe('note_app', () => { beforeEach(done => { Vue.http.interceptors.push(mockData.individualNoteInterceptor); spyOn(service, 'updateNote').and.callThrough(); - vm = mountComponent(); + wrapper = mountComponent(); setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); + wrapper.find('.js-note-edit').trigger('click'); Vue.nextTick(done); }, 0); }); @@ -175,12 +166,12 @@ describe('note_app', () => { }); it('renders edit form', () => { - expect(vm).toIncludeElement('.js-vue-issue-note-form'); + expect(wrapper.find('.js-vue-issue-note-form').exists()).toBe(true); }); it('calls the service to update the note', done => { - vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; - vm.$el.querySelector('.js-vue-issue-save').click(); + wrapper.find('.js-vue-issue-note-form').value = 'this is a note'; + wrapper.find('.js-vue-issue-save').trigger('click'); expect(service.updateNote).toHaveBeenCalled(); // Wait for the requests to finish before destroying @@ -194,10 +185,10 @@ describe('note_app', () => { beforeEach(done => { Vue.http.interceptors.push(mockData.discussionNoteInterceptor); spyOn(service, 'updateNote').and.callThrough(); - vm = mountComponent(); + wrapper = mountComponent(); setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); + wrapper.find('.js-note-edit').trigger('click'); Vue.nextTick(done); }, 0); }); @@ -210,12 +201,12 @@ describe('note_app', () => { }); it('renders edit form', () => { - expect(vm).toIncludeElement('.js-vue-issue-note-form'); + expect(wrapper.find('.js-vue-issue-note-form').exists()).toBe(true); }); it('updates the note and resets the edit form', done => { - vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; - vm.$el.querySelector('.js-vue-issue-save').click(); + wrapper.find('.js-vue-issue-note-form').value = 'this is a note'; + wrapper.find('.js-vue-issue-save').trigger('click'); expect(service.updateNote).toHaveBeenCalled(); // Wait for the requests to finish before destroying @@ -228,30 +219,36 @@ describe('note_app', () => { describe('new note form', () => { beforeEach(() => { - vm = mountComponent(); + wrapper = mountComponent(); }); it('should render markdown docs url', () => { const { markdownDocsPath } = mockData.notesDataMock; - expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual( - 'Markdown', - ); + expect( + wrapper + .find(`a[href="${markdownDocsPath}"]`) + .text() + .trim(), + ).toEqual('Markdown'); }); it('should render quick action docs url', () => { const { quickActionsDocsPath } = mockData.notesDataMock; - expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual( - 'quick actions', - ); + expect( + wrapper + .find(`a[href="${quickActionsDocsPath}"]`) + .text() + .trim(), + ).toEqual('quick actions'); }); }); describe('edit form', () => { beforeEach(() => { Vue.http.interceptors.push(mockData.individualNoteInterceptor); - vm = mountComponent(); + wrapper = mountComponent(); }); afterEach(() => { @@ -260,12 +257,15 @@ describe('note_app', () => { it('should render markdown docs url', done => { setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); + wrapper.find('.js-note-edit').trigger('click'); const { markdownDocsPath } = mockData.notesDataMock; Vue.nextTick(() => { expect( - vm.$el.querySelector(`.edit-note a[href="${markdownDocsPath}"]`).textContent.trim(), + wrapper + .find(`.edit-note a[href="${markdownDocsPath}"]`) + .text() + .trim(), ).toEqual('Markdown is supported'); done(); }); @@ -274,13 +274,11 @@ describe('note_app', () => { it('should not render quick actions docs url', done => { setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); + wrapper.find('.js-note-edit').trigger('click'); const { quickActionsDocsPath } = mockData.notesDataMock; Vue.nextTick(() => { - expect(vm.$el.querySelector(`.edit-note a[href="${quickActionsDocsPath}"]`)).toEqual( - null, - ); + expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).exists()).toBe(false); done(); }); }, 0); @@ -295,12 +293,19 @@ describe('note_app', () => { noteId: 1, }, }); + const toggleAwardAction = jasmine.createSpy('toggleAward'); + wrapper.vm.$store.hotUpdate({ + actions: { + toggleAward: toggleAwardAction, + }, + }); - spyOn(vm.$store, 'dispatch'); + wrapper.vm.$parent.$el.dispatchEvent(toggleAwardEvent); - vm.$el.parentElement.dispatchEvent(toggleAwardEvent); + expect(toggleAwardAction).toHaveBeenCalledTimes(1); + const [, payload] = toggleAwardAction.calls.argsFor(0); - expect(vm.$store.dispatch).toHaveBeenCalledWith('toggleAward', { + expect(payload).toEqual({ awardName: 'test', noteId: 1, }); diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js index c4b7eb17393..2eae22e095f 100644 --- a/spec/javascripts/notes/components/noteable_discussion_spec.js +++ b/spec/javascripts/notes/components/noteable_discussion_spec.js @@ -1,6 +1,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import createStore from '~/notes/stores'; import noteableDiscussion from '~/notes/components/noteable_discussion.vue'; +import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; import '~/behaviors/markdown/render_gfm'; import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; import mockDiffFile from '../../diffs/mock_data/diff_file'; @@ -57,27 +58,23 @@ describe('noteable_discussion component', () => { }); describe('actions', () => { - it('should render reply button', () => { - expect( - wrapper - .find('.js-vue-discussion-reply') - .text() - .trim(), - ).toEqual('Reply...'); - }); - it('should toggle reply form', done => { - wrapper.find('.js-vue-discussion-reply').trigger('click'); + const replyPlaceholder = wrapper.find(ReplyPlaceholder); - wrapper.vm.$nextTick(() => { - expect(wrapper.vm.isReplying).toEqual(true); + wrapper.vm + .$nextTick() + .then(() => { + expect(wrapper.vm.isReplying).toEqual(false); - // There is a watcher for `isReplying` which will init autosave in the next tick - wrapper.vm.$nextTick(() => { + replyPlaceholder.vm.$emit('onClick'); + }) + .then(() => wrapper.vm.$nextTick()) + .then(() => { + expect(wrapper.vm.isReplying).toEqual(true); expect(wrapper.vm.$refs.noteForm).not.toBeNull(); - done(); - }); - }); + }) + .then(done) + .catch(done.fail); }); it('does not render jump to discussion button', () => { diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 7ae45c40c28..348743081eb 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -165,7 +165,6 @@ export const note = { report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1', path: '/gitlab-org/gitlab-ce/notes/546', - cached_markdown_version: 11, }; export const discussionMock = { diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 2e3cd5e8f36..73f960dd21e 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -585,4 +585,18 @@ describe('Actions Notes Store', () => { ); }); }); + + describe('convertToDiscussion', () => { + it('commits CONVERT_TO_DISCUSSION with noteId', done => { + const noteId = 'dummy-note-id'; + testAction( + actions.convertToDiscussion, + noteId, + {}, + [{ type: 'CONVERT_TO_DISCUSSION', payload: noteId }], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index b6b2c7d60a5..4f8d3069bb5 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -517,4 +517,27 @@ describe('Notes Store mutations', () => { ); }); }); + + describe('CONVERT_TO_DISCUSSION', () => { + let discussion; + let state; + + beforeEach(() => { + discussion = { + id: 42, + individual_note: true, + }; + state = { discussions: [discussion] }; + }); + + it('toggles individual_note', () => { + mutations.CONVERT_TO_DISCUSSION(state, discussion.id); + + expect(discussion.individual_note).toBe(false); + }); + + it('throws if discussion was not found', () => { + expect(() => mutations.CONVERT_TO_DISCUSSION(state, 99)).toThrow(); + }); + }); }); diff --git a/spec/javascripts/serverless/components/environment_row_spec.js b/spec/javascripts/serverless/components/environment_row_spec.js new file mode 100644 index 00000000000..bdf7a714910 --- /dev/null +++ b/spec/javascripts/serverless/components/environment_row_spec.js @@ -0,0 +1,81 @@ +import Vue from 'vue'; + +import environmentRowComponent from '~/serverless/components/environment_row.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import ServerlessStore from '~/serverless/stores/serverless_store'; + +import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data'; + +const createComponent = (env, envName) => + mountComponent(Vue.extend(environmentRowComponent), { env, envName }); + +describe('environment row component', () => { + describe('default global cluster case', () => { + let vm; + + beforeEach(() => { + const store = new ServerlessStore(false, '/cluster_path', 'help_path'); + store.updateFunctionsFromServer(mockServerlessFunctions); + vm = createComponent(store.state.functions['*'], '*'); + }); + + it('has the correct envId', () => { + expect(vm.envId).toEqual('env-global'); + vm.$destroy(); + }); + + it('is open by default', () => { + expect(vm.isOpenClass).toEqual({ 'is-open': true }); + vm.$destroy(); + }); + + it('generates correct output', () => { + expect(vm.$el.querySelectorAll('li').length).toEqual(2); + expect(vm.$el.id).toEqual('env-global'); + expect(vm.$el.classList.contains('is-open')).toBe(true); + expect(vm.$el.querySelector('div.title').innerHTML.trim()).toEqual('*'); + + vm.$destroy(); + }); + + it('opens and closes correctly', () => { + expect(vm.isOpen).toBe(true); + + vm.toggleOpen(); + Vue.nextTick(() => { + expect(vm.isOpen).toBe(false); + }); + + vm.$destroy(); + }); + }); + + describe('default named cluster case', () => { + let vm; + + beforeEach(() => { + const store = new ServerlessStore(false, '/cluster_path', 'help_path'); + store.updateFunctionsFromServer(mockServerlessFunctionsDiffEnv); + vm = createComponent(store.state.functions.test, 'test'); + }); + + it('has the correct envId', () => { + expect(vm.envId).toEqual('env-test'); + vm.$destroy(); + }); + + it('is open by default', () => { + expect(vm.isOpenClass).toEqual({ 'is-open': true }); + vm.$destroy(); + }); + + it('generates correct output', () => { + expect(vm.$el.querySelectorAll('li').length).toEqual(1); + expect(vm.$el.id).toEqual('env-test'); + expect(vm.$el.classList.contains('is-open')).toBe(true); + expect(vm.$el.querySelector('div.title').innerHTML.trim()).toEqual('test'); + + vm.$destroy(); + }); + }); +}); diff --git a/spec/javascripts/serverless/components/function_row_spec.js b/spec/javascripts/serverless/components/function_row_spec.js new file mode 100644 index 00000000000..6933a8f6c87 --- /dev/null +++ b/spec/javascripts/serverless/components/function_row_spec.js @@ -0,0 +1,33 @@ +import Vue from 'vue'; + +import functionRowComponent from '~/serverless/components/function_row.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +import { mockServerlessFunction } from '../mock_data'; + +const createComponent = func => mountComponent(Vue.extend(functionRowComponent), { func }); + +describe('functionRowComponent', () => { + it('Parses the function details correctly', () => { + const vm = createComponent(mockServerlessFunction); + + expect(vm.$el.querySelector('b').innerHTML).toEqual(mockServerlessFunction.name); + expect(vm.$el.querySelector('span').innerHTML).toEqual(mockServerlessFunction.image); + expect(vm.$el.querySelector('time').getAttribute('data-original-title')).not.toBe(null); + expect(vm.$el.querySelector('div.url-text-field').innerHTML).toEqual( + mockServerlessFunction.url, + ); + + vm.$destroy(); + }); + + it('handles clicks correctly', () => { + const vm = createComponent(mockServerlessFunction); + + expect(vm.checkClass(vm.$el.querySelector('p'))).toBe(true); // check somewhere inside the row + expect(vm.checkClass(vm.$el.querySelector('svg'))).toBe(false); // check a button image + expect(vm.checkClass(vm.$el.querySelector('div.url-text-field'))).toBe(false); // check the url bar + + vm.$destroy(); + }); +}); diff --git a/spec/javascripts/serverless/components/functions_spec.js b/spec/javascripts/serverless/components/functions_spec.js new file mode 100644 index 00000000000..85cfe71281f --- /dev/null +++ b/spec/javascripts/serverless/components/functions_spec.js @@ -0,0 +1,68 @@ +import Vue from 'vue'; + +import functionsComponent from '~/serverless/components/functions.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import ServerlessStore from '~/serverless/stores/serverless_store'; + +import { mockServerlessFunctions } from '../mock_data'; + +const createComponent = ( + functions, + installed = true, + loadingData = true, + hasFunctionData = true, +) => { + const component = Vue.extend(functionsComponent); + + return mountComponent(component, { + functions, + installed, + clustersPath: '/testClusterPath', + helpPath: '/helpPath', + loadingData, + hasFunctionData, + }); +}; + +describe('functionsComponent', () => { + it('should render empty state when Knative is not installed', () => { + const vm = createComponent({}, false); + + expect(vm.$el.querySelector('div.row').classList.contains('js-empty-state')).toBe(true); + expect(vm.$el.querySelector('h4.state-title').innerHTML.trim()).toEqual( + 'Getting started with serverless', + ); + + vm.$destroy(); + }); + + it('should render a loading component', () => { + const vm = createComponent({}); + + expect(vm.$el.querySelector('.gl-responsive-table-row')).not.toBe(null); + expect(vm.$el.querySelector('div.animation-container')).not.toBe(null); + }); + + it('should render empty state when there is no function data', () => { + const vm = createComponent({}, true, false, false); + + expect( + vm.$el.querySelector('.empty-state, .js-empty-state').classList.contains('js-empty-state'), + ).toBe(true); + + expect(vm.$el.querySelector('h4.state-title').innerHTML.trim()).toEqual( + 'No functions available', + ); + + vm.$destroy(); + }); + + it('should render the functions list', () => { + const store = new ServerlessStore(false, '/cluster_path', 'help_path'); + store.updateFunctionsFromServer(mockServerlessFunctions); + const vm = createComponent(store.state.functions, true, false); + + expect(vm.$el.querySelector('div.groups-list-tree-container')).not.toBe(null); + expect(vm.$el.querySelector('#env-global').classList.contains('has-children')).toBe(true); + }); +}); diff --git a/spec/javascripts/serverless/components/url_spec.js b/spec/javascripts/serverless/components/url_spec.js new file mode 100644 index 00000000000..21a879a49bb --- /dev/null +++ b/spec/javascripts/serverless/components/url_spec.js @@ -0,0 +1,28 @@ +import Vue from 'vue'; + +import urlComponent from '~/serverless/components/url.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +const createComponent = uri => { + const component = Vue.extend(urlComponent); + + return mountComponent(component, { + uri, + }); +}; + +describe('urlComponent', () => { + it('should render correctly', () => { + const uri = 'http://testfunc.apps.example.com'; + const vm = createComponent(uri); + + expect(vm.$el.classList.contains('clipboard-group')).toBe(true); + expect(vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text')).toEqual( + uri, + ); + + expect(vm.$el.querySelector('.url-text-field').innerHTML).toEqual(uri); + + vm.$destroy(); + }); +}); diff --git a/spec/javascripts/serverless/mock_data.js b/spec/javascripts/serverless/mock_data.js new file mode 100644 index 00000000000..ecd393b174c --- /dev/null +++ b/spec/javascripts/serverless/mock_data.js @@ -0,0 +1,79 @@ +export const mockServerlessFunctions = [ + { + name: 'testfunc1', + namespace: 'tm-example', + environment_scope: '*', + cluster_id: 46, + detail_url: '/testuser/testproj/serverless/functions/*/testfunc1', + podcount: null, + created_at: '2019-02-05T01:01:23Z', + url: 'http://testfunc1.tm-example.apps.example.com', + description: 'A test service', + image: 'knative-test-container-buildtemplate', + }, + { + name: 'testfunc2', + namespace: 'tm-example', + environment_scope: '*', + cluster_id: 46, + detail_url: '/testuser/testproj/serverless/functions/*/testfunc2', + podcount: null, + created_at: '2019-02-05T01:01:23Z', + url: 'http://testfunc2.tm-example.apps.example.com', + description: 'A second test service\nThis one with additional descriptions', + image: 'knative-test-echo-buildtemplate', + }, +]; + +export const mockServerlessFunctionsDiffEnv = [ + { + name: 'testfunc1', + namespace: 'tm-example', + environment_scope: '*', + cluster_id: 46, + detail_url: '/testuser/testproj/serverless/functions/*/testfunc1', + podcount: null, + created_at: '2019-02-05T01:01:23Z', + url: 'http://testfunc1.tm-example.apps.example.com', + description: 'A test service', + image: 'knative-test-container-buildtemplate', + }, + { + name: 'testfunc2', + namespace: 'tm-example', + environment_scope: 'test', + cluster_id: 46, + detail_url: '/testuser/testproj/serverless/functions/*/testfunc2', + podcount: null, + created_at: '2019-02-05T01:01:23Z', + url: 'http://testfunc2.tm-example.apps.example.com', + description: 'A second test service\nThis one with additional descriptions', + image: 'knative-test-echo-buildtemplate', + }, +]; + +export const mockServerlessFunction = { + name: 'testfunc1', + namespace: 'tm-example', + environment_scope: '*', + cluster_id: 46, + detail_url: '/testuser/testproj/serverless/functions/*/testfunc1', + podcount: '3', + created_at: '2019-02-05T01:01:23Z', + url: 'http://testfunc1.tm-example.apps.example.com', + description: 'A test service', + image: 'knative-test-container-buildtemplate', +}; + +export const mockMultilineServerlessFunction = { + name: 'testfunc1', + namespace: 'tm-example', + environment_scope: '*', + cluster_id: 46, + detail_url: '/testuser/testproj/serverless/functions/*/testfunc1', + podcount: '3', + created_at: '2019-02-05T01:01:23Z', + url: 'http://testfunc1.tm-example.apps.example.com', + description: 'testfunc1\nA test service line\\nWith additional services', + image: 'knative-test-container-buildtemplate', +}; diff --git a/spec/javascripts/serverless/stores/serverless_store_spec.js b/spec/javascripts/serverless/stores/serverless_store_spec.js new file mode 100644 index 00000000000..72fd903d7d1 --- /dev/null +++ b/spec/javascripts/serverless/stores/serverless_store_spec.js @@ -0,0 +1,36 @@ +import ServerlessStore from '~/serverless/stores/serverless_store'; +import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data'; + +describe('Serverless Functions Store', () => { + let store; + + beforeEach(() => { + store = new ServerlessStore(false, '/cluster_path', 'help_path'); + }); + + describe('#updateFunctionsFromServer', () => { + it('should pass an empty hash object', () => { + store.updateFunctionsFromServer(); + + expect(store.state.functions).toEqual({}); + }); + + it('should group functions to one global environment', () => { + const mockServerlessData = mockServerlessFunctions; + store.updateFunctionsFromServer(mockServerlessData); + + expect(Object.keys(store.state.functions)).toEqual(jasmine.objectContaining(['*'])); + expect(store.state.functions['*'].length).toEqual(2); + }); + + it('should group functions to multiple environments', () => { + const mockServerlessData = mockServerlessFunctionsDiffEnv; + store.updateFunctionsFromServer(mockServerlessData); + + expect(Object.keys(store.state.functions)).toEqual(jasmine.objectContaining(['*'])); + expect(store.state.functions['*'].length).toEqual(1); + expect(store.state.functions.test.length).toEqual(1); + expect(store.state.functions.test[0].name).toEqual('testfunc2'); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 072e98fc0e8..75b197fb2ba 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -58,7 +58,7 @@ export default { merge_user: null, diff_head_sha: '104096c51715e12e7ae41f9333e9fa35b73f385d', diff_head_commit_short_id: '104096c5', - merge_commit_message: + default_merge_commit_message: "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22", pipeline: { id: 172, @@ -213,7 +213,7 @@ export default { merge_check_path: '/root/acets-app/merge_requests/22/merge_check', ci_environments_status_url: '/root/acets-app/merge_requests/22/ci_environments_status', project_archived: false, - merge_commit_message_with_description: + default_merge_commit_message_with_description: "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22", diverged_commits_count: 0, only_allow_merge_if_pipeline_succeeds: false, diff --git a/spec/javascripts/ide/components/file_finder/index_spec.js b/spec/javascripts/vue_shared/components/file_finder/index_spec.js index 15ef8c31f91..bae4741f652 100644 --- a/spec/javascripts/ide/components/file_finder/index_spec.js +++ b/spec/javascripts/vue_shared/components/file_finder/index_spec.js @@ -1,54 +1,51 @@ import Vue from 'vue'; -import store from '~/ide/stores'; -import FindFileComponent from '~/ide/components/file_finder/index.vue'; +import Mousetrap from 'mousetrap'; +import FindFileComponent from '~/vue_shared/components/file_finder/index.vue'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; -import router from '~/ide/ide_router'; -import { file, resetStore } from '../../helpers'; -import { mountComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { file } from 'spec/ide/helpers'; +import timeoutPromise from 'spec/helpers/set_timeout_promise_helper'; -describe('IDE File finder item spec', () => { +describe('File finder item spec', () => { const Component = Vue.extend(FindFileComponent); let vm; - beforeEach(done => { - setFixtures('<div id="app"></div>'); - - vm = mountComponentWithStore(Component, { - store, - el: '#app', - props: { - index: 0, + function createComponent(props) { + vm = new Component({ + propsData: { + files: [], + visible: true, + loading: false, + ...props, }, }); - setTimeout(done); + vm.$mount('#app'); + } + + beforeEach(() => { + setFixtures('<div id="app"></div>'); }); afterEach(() => { vm.$destroy(); - - resetStore(vm.$store); }); describe('with entries', () => { beforeEach(done => { - Vue.set(vm.$store.state.entries, 'folder', { - ...file('folder'), - path: 'folder', - type: 'folder', - }); - - Vue.set(vm.$store.state.entries, 'index.js', { - ...file('index.js'), - path: 'index.js', - type: 'blob', - url: '/index.jsurl', - }); - - Vue.set(vm.$store.state.entries, 'component.js', { - ...file('component.js'), - path: 'component.js', - type: 'blob', + createComponent({ + files: [ + { + ...file('index.js'), + path: 'index.js', + type: 'blob', + url: '/index.jsurl', + }, + { + ...file('component.js'), + path: 'component.js', + type: 'blob', + }, + ], }); setTimeout(done); @@ -56,13 +53,14 @@ describe('IDE File finder item spec', () => { it('renders list of blobs', () => { expect(vm.$el.textContent).toContain('index.js'); + expect(vm.$el.textContent).toContain('component.js'); expect(vm.$el.textContent).not.toContain('folder'); }); it('filters entries', done => { vm.searchText = 'index'; - vm.$nextTick(() => { + setTimeout(() => { expect(vm.$el.textContent).toContain('index.js'); expect(vm.$el.textContent).not.toContain('component.js'); @@ -73,8 +71,8 @@ describe('IDE File finder item spec', () => { it('shows clear button when searchText is not empty', done => { vm.searchText = 'index'; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.dropdown-input-clear').classList).toContain('show'); + setTimeout(() => { + expect(vm.$el.querySelector('.dropdown-input').classList).toContain('has-value'); expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden'); done(); @@ -84,11 +82,11 @@ describe('IDE File finder item spec', () => { it('clear button resets searchText', done => { vm.searchText = 'index'; - vm.$nextTick() + timeoutPromise() .then(() => { vm.$el.querySelector('.dropdown-input-clear').click(); }) - .then(vm.$nextTick) + .then(timeoutPromise) .then(() => { expect(vm.searchText).toBe(''); }) @@ -100,11 +98,11 @@ describe('IDE File finder item spec', () => { spyOn(vm.$refs.searchInput, 'focus'); vm.searchText = 'index'; - vm.$nextTick() + timeoutPromise() .then(() => { vm.$el.querySelector('.dropdown-input-clear').click(); }) - .then(vm.$nextTick) + .then(timeoutPromise) .then(() => { expect(vm.$refs.searchInput.focus).toHaveBeenCalled(); }) @@ -116,7 +114,7 @@ describe('IDE File finder item spec', () => { it('returns 1 when no filtered entries exist', done => { vm.searchText = 'testing 123'; - vm.$nextTick(() => { + setTimeout(() => { expect(vm.listShowCount).toBe(1); done(); @@ -136,7 +134,7 @@ describe('IDE File finder item spec', () => { it('returns 33 when entries dont exist', done => { vm.searchText = 'testing 123'; - vm.$nextTick(() => { + setTimeout(() => { expect(vm.listHeight).toBe(33); done(); @@ -148,7 +146,7 @@ describe('IDE File finder item spec', () => { it('returns length of filtered blobs', done => { vm.searchText = 'index'; - vm.$nextTick(() => { + setTimeout(() => { expect(vm.filteredBlobsLength).toBe(1); done(); @@ -162,7 +160,7 @@ describe('IDE File finder item spec', () => { vm.focusedIndex = 1; vm.searchText = 'test'; - vm.$nextTick(() => { + setTimeout(() => { expect(vm.focusedIndex).toBe(0); done(); @@ -170,16 +168,16 @@ describe('IDE File finder item spec', () => { }); }); - describe('fileFindVisible', () => { + describe('visible', () => { it('returns searchText when false', done => { vm.searchText = 'test'; - vm.$store.state.fileFindVisible = true; + vm.visible = true; - vm.$nextTick() + timeoutPromise() .then(() => { - vm.$store.state.fileFindVisible = false; + vm.visible = false; }) - .then(vm.$nextTick) + .then(timeoutPromise) .then(() => { expect(vm.searchText).toBe(''); }) @@ -191,20 +189,19 @@ describe('IDE File finder item spec', () => { describe('openFile', () => { beforeEach(() => { - spyOn(router, 'push'); - spyOn(vm, 'toggleFileFinder'); + spyOn(vm, '$emit'); }); it('closes file finder', () => { - vm.openFile(vm.$store.state.entries['index.js']); + vm.openFile(vm.files[0]); - expect(vm.toggleFileFinder).toHaveBeenCalled(); + expect(vm.$emit).toHaveBeenCalledWith('toggle', false); }); it('pushes to router', () => { - vm.openFile(vm.$store.state.entries['index.js']); + vm.openFile(vm.files[0]); - expect(router.push).toHaveBeenCalledWith('/project/index.jsurl'); + expect(vm.$emit).toHaveBeenCalledWith('click', vm.files[0]); }); }); @@ -217,8 +214,8 @@ describe('IDE File finder item spec', () => { vm.$refs.searchInput.dispatchEvent(event); - vm.$nextTick(() => { - expect(vm.openFile).toHaveBeenCalledWith(vm.$store.state.entries['index.js']); + setTimeout(() => { + expect(vm.openFile).toHaveBeenCalledWith(vm.files[0]); done(); }); @@ -228,12 +225,12 @@ describe('IDE File finder item spec', () => { const event = new CustomEvent('keyup'); event.keyCode = ESC_KEY_CODE; - spyOn(vm, 'toggleFileFinder'); + spyOn(vm, '$emit'); vm.$refs.searchInput.dispatchEvent(event); - vm.$nextTick(() => { - expect(vm.toggleFileFinder).toHaveBeenCalled(); + setTimeout(() => { + expect(vm.$emit).toHaveBeenCalledWith('toggle', false); done(); }); @@ -287,18 +284,85 @@ describe('IDE File finder item spec', () => { }); describe('without entries', () => { - it('renders loading text when loading', done => { - store.state.loading = true; - - vm.$nextTick(() => { - expect(vm.$el.textContent).toContain('Loading...'); - - done(); + it('renders loading text when loading', () => { + createComponent({ + loading: true, }); + + expect(vm.$el.textContent).toContain('Loading...'); }); it('renders no files text', () => { + createComponent(); + expect(vm.$el.textContent).toContain('No files found.'); }); }); + + describe('keyboard shortcuts', () => { + beforeEach(done => { + createComponent(); + + spyOn(vm, 'toggle'); + + vm.$nextTick(done); + }); + + it('calls toggle on `t` key press', done => { + Mousetrap.trigger('t'); + + vm.$nextTick() + .then(() => { + expect(vm.toggle).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('calls toggle on `command+p` key press', done => { + Mousetrap.trigger('command+p'); + + vm.$nextTick() + .then(() => { + expect(vm.toggle).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('calls toggle on `ctrl+p` key press', done => { + Mousetrap.trigger('ctrl+p'); + + vm.$nextTick() + .then(() => { + expect(vm.toggle).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('always allows `command+p` to trigger toggle', () => { + expect( + vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'command+p'), + ).toBe(false); + }); + + it('always allows `ctrl+p` to trigger toggle', () => { + expect( + vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'ctrl+p'), + ).toBe(false); + }); + + it('onlys handles `t` when focused in input-field', () => { + expect( + vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'), + ).toBe(true); + }); + + it('stops callback in monaco editor', () => { + setFixtures('<div class="inputarea"></div>'); + + expect(vm.mousetrapStopCallback(null, document.querySelector('.inputarea'), 't')).toBe(true); + }); + }); }); diff --git a/spec/javascripts/ide/components/file_finder/item_spec.js b/spec/javascripts/vue_shared/components/file_finder/item_spec.js index 0f1116c6912..c1511643a9d 100644 --- a/spec/javascripts/ide/components/file_finder/item_spec.js +++ b/spec/javascripts/vue_shared/components/file_finder/item_spec.js @@ -1,9 +1,9 @@ import Vue from 'vue'; -import ItemComponent from '~/ide/components/file_finder/item.vue'; -import { file } from '../../helpers'; +import ItemComponent from '~/vue_shared/components/file_finder/item.vue'; +import { file } from 'spec/ide/helpers'; import createComponent from '../../../helpers/vue_mount_component_helper'; -describe('IDE File finder item spec', () => { +describe('File finder item spec', () => { const Component = Vue.extend(ItemComponent); let vm; let localFile; |