diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 18:25:58 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 18:25:58 +0000 |
commit | a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4 (patch) | |
tree | fb69158581673816a8cd895f9d352dcb3c678b1e /spec/frontend/notes | |
parent | d16b2e8639e99961de6ddc93909f3bb5c1445ba1 (diff) | |
download | gitlab-ce-a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4.tar.gz |
Add latest changes from gitlab-org/gitlab@14-0-stable-eev14.0.0-rc42
Diffstat (limited to 'spec/frontend/notes')
5 files changed, 211 insertions, 56 deletions
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js index b140eea9439..537622b7918 100644 --- a/spec/frontend/notes/components/comment_form_spec.js +++ b/spec/frontend/notes/components/comment_form_spec.js @@ -328,20 +328,45 @@ describe('issue_comment_form component', () => { mountComponent({ mountFunction: mount }); }); - it('should save note when cmd+enter is pressed', () => { - jest.spyOn(wrapper.vm, 'handleSave'); + describe('when no draft exists', () => { + it('should save note when cmd+enter is pressed', () => { + jest.spyOn(wrapper.vm, 'handleSave'); - findTextArea().trigger('keydown.enter', { metaKey: true }); + findTextArea().trigger('keydown.enter', { metaKey: true }); - expect(wrapper.vm.handleSave).toHaveBeenCalled(); + expect(wrapper.vm.handleSave).toHaveBeenCalledWith(); + }); + + it('should save note when ctrl+enter is pressed', () => { + jest.spyOn(wrapper.vm, 'handleSave'); + + findTextArea().trigger('keydown.enter', { ctrlKey: true }); + + expect(wrapper.vm.handleSave).toHaveBeenCalledWith(); + }); }); - it('should save note when ctrl+enter is pressed', () => { - jest.spyOn(wrapper.vm, 'handleSave'); + describe('when a draft exists', () => { + beforeEach(() => { + store.registerModule('batchComments', batchComments()); + store.state.batchComments.drafts = [{ note: 'A' }]; + }); + + it('should save note draft when cmd+enter is pressed', () => { + jest.spyOn(wrapper.vm, 'handleSaveDraft'); + + findTextArea().trigger('keydown.enter', { metaKey: true }); + + expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith(); + }); + + it('should save note draft when ctrl+enter is pressed', () => { + jest.spyOn(wrapper.vm, 'handleSaveDraft'); - findTextArea().trigger('keydown.enter', { ctrlKey: true }); + findTextArea().trigger('keydown.enter', { ctrlKey: true }); - expect(wrapper.vm.handleSave).toHaveBeenCalled(); + expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith(); + }); }); }); }); diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js index c6a7d7ead98..925dbcc09ec 100644 --- a/spec/frontend/notes/components/discussion_actions_spec.js +++ b/spec/frontend/notes/components/discussion_actions_spec.js @@ -20,7 +20,7 @@ const createUnallowedNote = () => describe('DiscussionActions', () => { let wrapper; - const createComponentFactory = (shallow = true) => (props) => { + const createComponentFactory = (shallow = true) => (props, options) => { const store = createStore(); const mountFn = shallow ? shallowMount : mount; @@ -34,6 +34,7 @@ describe('DiscussionActions', () => { shouldShowJumpToNextDiscussion: true, ...props, }, + ...options, }); }; @@ -90,17 +91,17 @@ describe('DiscussionActions', () => { describe('events handling', () => { const createComponent = createComponentFactory(false); - beforeEach(() => { - createComponent(); - }); - it('emits showReplyForm event when clicking on reply placeholder', () => { + createComponent({}, { attachTo: document.body }); + jest.spyOn(wrapper.vm, '$emit'); wrapper.find(ReplyPlaceholder).find('textarea').trigger('focus'); expect(wrapper.vm.$emit).toHaveBeenCalledWith('showReplyForm'); }); it('emits resolve event when clicking on resolve button', () => { + createComponent(); + jest.spyOn(wrapper.vm, '$emit'); wrapper.find(ResolveDiscussionButton).find('button').trigger('click'); expect(wrapper.vm.$emit).toHaveBeenCalledWith('resolve'); diff --git a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js index 2a4cd0df0c7..3932f818c4e 100644 --- a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js +++ b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js @@ -6,31 +6,34 @@ const placeholderText = 'Test Button Text'; describe('ReplyPlaceholder', () => { let wrapper; - const findTextarea = () => wrapper.find({ ref: 'textarea' }); - - beforeEach(() => { + const createComponent = ({ options = {} } = {}) => { wrapper = shallowMount(ReplyPlaceholder, { propsData: { placeholderText, }, + ...options, }); - }); + }; + + const findTextarea = () => wrapper.find({ ref: 'textarea' }); afterEach(() => { wrapper.destroy(); }); - it('emits focus event on button click', () => { - findTextarea().trigger('focus'); + it('emits focus event on button click', async () => { + createComponent({ options: { attachTo: document.body } }); + + await findTextarea().trigger('focus'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted()).toEqual({ - focus: [[]], - }); + expect(wrapper.emitted()).toEqual({ + focus: [[]], }); }); it('should render reply button', () => { + createComponent(); + expect(findTextarea().attributes('placeholder')).toEqual(placeholderText); }); }); diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js index 735bc2b70dd..a364a524e7b 100644 --- a/spec/frontend/notes/components/noteable_discussion_spec.js +++ b/spec/frontend/notes/components/noteable_discussion_spec.js @@ -56,6 +56,18 @@ describe('noteable_discussion component', () => { expect(wrapper.find('.discussion-header').exists()).toBe(true); }); + it('should hide actions when diff refs do not exists', async () => { + const discussion = { ...discussionMock }; + discussion.diff_file = { ...mockDiffFile, diff_refs: null }; + discussion.diff_discussion = true; + discussion.expanded = false; + + wrapper.setProps({ discussion }); + await nextTick(); + + expect(wrapper.vm.canShowReplyActions).toBe(false); + }); + describe('actions', () => { it('should toggle reply form', async () => { await nextTick(); diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js index 9b7456d54bc..7eef2017dfb 100644 --- a/spec/frontend/notes/stores/actions_spec.js +++ b/spec/frontend/notes/stores/actions_spec.js @@ -25,7 +25,19 @@ import { } from '../mock_data'; const TEST_ERROR_MESSAGE = 'Test error message'; -jest.mock('~/flash'); +const mockFlashClose = jest.fn(); +jest.mock('~/flash', () => { + const flash = jest.fn().mockImplementation(() => { + return { + close: mockFlashClose, + }; + }); + + return { + createFlash: flash, + deprecatedCreateFlash: flash, + }; +}); describe('Actions Notes Store', () => { let commit; @@ -254,42 +266,144 @@ describe('Actions Notes Store', () => { }); describe('poll', () => { - beforeEach((done) => { - axiosMock - .onGet(notesDataMock.notesPath) - .reply(200, { notes: [], last_fetched_at: '123456' }, { 'poll-interval': '1000' }); + const pollInterval = 6000; + const pollResponse = { notes: [], last_fetched_at: '123456' }; + const pollHeaders = { 'poll-interval': `${pollInterval}` }; + const successMock = () => + axiosMock.onGet(notesDataMock.notesPath).reply(200, pollResponse, pollHeaders); + const failureMock = () => axiosMock.onGet(notesDataMock.notesPath).reply(500); + const advanceAndRAF = async (time) => { + if (time) { + jest.advanceTimersByTime(time); + } + + return new Promise((resolve) => requestAnimationFrame(resolve)); + }; + const advanceXMoreIntervals = async (number) => { + const timeoutLength = pollInterval * number; + return advanceAndRAF(timeoutLength); + }; + const startPolling = async () => { + await store.dispatch('poll'); + await advanceAndRAF(2); + }; + const cleanUp = async () => { + jest.clearAllTimers(); + + return store.dispatch('stopPolling'); + }; + + beforeEach((done) => { store.dispatch('setNotesData', notesDataMock).then(done).catch(done.fail); }); - it('calls service with last fetched state', (done) => { - store - .dispatch('poll') - .then(() => { - jest.advanceTimersByTime(2); - }) - .then(() => new Promise((resolve) => requestAnimationFrame(resolve))) - .then(() => { - expect(store.state.lastFetchedAt).toBe('123456'); - - jest.advanceTimersByTime(1500); - }) - .then( - () => - new Promise((resolve) => { - requestAnimationFrame(resolve); - }), - ) - .then(() => { - const expectedGetRequests = 2; - expect(axiosMock.history.get.length).toBe(expectedGetRequests); - expect(axiosMock.history.get[expectedGetRequests - 1].headers).toMatchObject({ - 'X-Last-Fetched-At': '123456', - }); - }) - .then(() => store.dispatch('stopPolling')) - .then(done) - .catch(done.fail); + afterEach(() => { + return cleanUp(); + }); + + it('calls service with last fetched state', async () => { + successMock(); + + await startPolling(); + + expect(store.state.lastFetchedAt).toBe('123456'); + + await advanceXMoreIntervals(1); + + expect(axiosMock.history.get).toHaveLength(2); + expect(axiosMock.history.get[1].headers).toMatchObject({ + 'X-Last-Fetched-At': '123456', + }); + }); + + describe('polling side effects', () => { + it('retries twice', async () => { + failureMock(); + + await startPolling(); + + // This is the first request, not a retry + expect(axiosMock.history.get).toHaveLength(1); + + await advanceXMoreIntervals(1); + + // Retry #1 + expect(axiosMock.history.get).toHaveLength(2); + + await advanceXMoreIntervals(1); + + // Retry #2 + expect(axiosMock.history.get).toHaveLength(3); + + await advanceXMoreIntervals(10); + + // There are no more retries + expect(axiosMock.history.get).toHaveLength(3); + }); + + it('shows the error display on the second failure', async () => { + failureMock(); + + await startPolling(); + + expect(axiosMock.history.get).toHaveLength(1); + expect(Flash).not.toHaveBeenCalled(); + + await advanceXMoreIntervals(1); + + expect(axiosMock.history.get).toHaveLength(2); + expect(Flash).toHaveBeenCalled(); + expect(Flash).toHaveBeenCalledTimes(1); + }); + + it('resets the failure counter on success', async () => { + // We can't get access to the actual counter in the polling closure. + // So we can infer that it's reset by ensuring that the error is only + // shown when we cause two failures in a row - no successes between + + axiosMock + .onGet(notesDataMock.notesPath) + .replyOnce(500) // cause one error + .onGet(notesDataMock.notesPath) + .replyOnce(200, pollResponse, pollHeaders) // then a success + .onGet(notesDataMock.notesPath) + .reply(500); // and then more errors + + await startPolling(); // Failure #1 + await advanceXMoreIntervals(1); // Success #1 + await advanceXMoreIntervals(1); // Failure #2 + + // That was the first failure AFTER a success, so we should NOT see the error displayed + expect(Flash).not.toHaveBeenCalled(); + + // Now we'll allow another failure + await advanceXMoreIntervals(1); // Failure #3 + + // Since this is the second failure in a row, the error should happen + expect(Flash).toHaveBeenCalled(); + expect(Flash).toHaveBeenCalledTimes(1); + }); + + it('hides the error display if it exists on success', async () => { + jest.mock(); + failureMock(); + + await startPolling(); + await advanceXMoreIntervals(2); + + // After two errors, the error should be displayed + expect(Flash).toHaveBeenCalled(); + expect(Flash).toHaveBeenCalledTimes(1); + + axiosMock.reset(); + successMock(); + + await advanceXMoreIntervals(1); + + expect(mockFlashClose).toHaveBeenCalled(); + expect(mockFlashClose).toHaveBeenCalledTimes(1); + }); }); }); |