From 8b61452138ecc511b52cd49be4ee6b8a80390c50 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 20 Dec 2019 15:07:34 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .../components/discussion_filter_note_spec.js | 93 ++++ spec/frontend/notes/components/note_header_spec.js | 125 +++++ spec/frontend/notes/stores/getters_spec.js | 388 ++++++++++++++ spec/frontend/notes/stores/mutation_spec.js | 584 +++++++++++++++++++++ 4 files changed, 1190 insertions(+) create mode 100644 spec/frontend/notes/components/discussion_filter_note_spec.js create mode 100644 spec/frontend/notes/components/note_header_spec.js create mode 100644 spec/frontend/notes/stores/getters_spec.js create mode 100644 spec/frontend/notes/stores/mutation_spec.js (limited to 'spec/frontend/notes') diff --git a/spec/frontend/notes/components/discussion_filter_note_spec.js b/spec/frontend/notes/components/discussion_filter_note_spec.js new file mode 100644 index 00000000000..6b5f42a84e8 --- /dev/null +++ b/spec/frontend/notes/components/discussion_filter_note_spec.js @@ -0,0 +1,93 @@ +import Vue from 'vue'; +import DiscussionFilterNote from '~/notes/components/discussion_filter_note.vue'; +import eventHub from '~/notes/event_hub'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('DiscussionFilterNote component', () => { + let vm; + + const createComponent = () => { + const Component = Vue.extend(DiscussionFilterNote); + + return mountComponent(Component); + }; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('timelineContent', () => { + it('returns string containing instruction for switching feed type', () => { + expect(vm.timelineContent).toBe( + "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.", + ); + }); + }); + }); + + describe('methods', () => { + describe('selectFilter', () => { + it('emits `dropdownSelect` event on `eventHub` with provided param', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + + vm.selectFilter(1); + + expect(eventHub.$emit).toHaveBeenCalledWith('dropdownSelect', 1); + }); + }); + }); + + describe('template', () => { + it('renders component container element', () => { + expect(vm.$el.classList.contains('discussion-filter-note')).toBe(true); + }); + + it('renders comment icon element', () => { + expect(vm.$el.querySelector('.timeline-icon svg use').getAttribute('xlink:href')).toContain( + 'comment', + ); + }); + + it('renders filter information note', () => { + expect(vm.$el.querySelector('.timeline-content').innerText.trim()).toContain( + "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.", + ); + }); + + it('renders filter buttons', () => { + const buttonsContainerEl = vm.$el.querySelector('.discussion-filter-actions'); + + expect(buttonsContainerEl.querySelector('button:first-child').innerText.trim()).toContain( + 'Show all activity', + ); + + expect(buttonsContainerEl.querySelector('button:last-child').innerText.trim()).toContain( + 'Show comments only', + ); + }); + + it('clicking `Show all activity` button calls `selectFilter("all")` method', () => { + const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:first-child'); + jest.spyOn(vm, 'selectFilter').mockImplementation(() => {}); + + showAllBtn.dispatchEvent(new Event('click')); + + expect(vm.selectFilter).toHaveBeenCalledWith(0); + }); + + it('clicking `Show comments only` button calls `selectFilter("comments")` method', () => { + const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:last-child'); + jest.spyOn(vm, 'selectFilter').mockImplementation(() => {}); + + showAllBtn.dispatchEvent(new Event('click')); + + expect(vm.selectFilter).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js new file mode 100644 index 00000000000..9b432387654 --- /dev/null +++ b/spec/frontend/notes/components/note_header_spec.js @@ -0,0 +1,125 @@ +import Vue from 'vue'; +import noteHeader from '~/notes/components/note_header.vue'; +import createStore from '~/notes/stores'; + +describe('note_header component', () => { + let store; + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(noteHeader); + store = createStore(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('individual note', () => { + beforeEach(() => { + vm = new Component({ + store, + propsData: { + actionText: 'commented', + actionTextHtml: '', + author: { + avatar_url: null, + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', + }, + createdAt: '2017-08-02T10:51:58.559Z', + includeToggle: false, + noteId: '1394', + expanded: true, + }, + }).$mount(); + }); + + it('should render user information', () => { + expect(vm.$el.querySelector('.note-header-author-name').textContent.trim()).toEqual('Root'); + expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual('/root'); + expect(vm.$el.querySelector('.note-header-info a').dataset.userId).toEqual('1'); + expect(vm.$el.querySelector('.note-header-info a').dataset.username).toEqual('root'); + expect(vm.$el.querySelector('.note-header-info a').classList).toContain('js-user-link'); + }); + + it('should render timestamp link', () => { + expect(vm.$el.querySelector('a[href="#note_1394"]')).toBeDefined(); + }); + + it('should not render user information when prop `author` is empty object', done => { + vm.author = {}; + Vue.nextTick() + .then(() => { + expect(vm.$el.querySelector('.note-header-author-name')).toBeNull(); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('discussion', () => { + beforeEach(() => { + vm = new Component({ + store, + propsData: { + actionText: 'started a discussion', + actionTextHtml: '', + author: { + avatar_url: null, + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', + }, + createdAt: '2017-08-02T10:51:58.559Z', + includeToggle: true, + noteId: '1395', + expanded: true, + }, + }).$mount(); + }); + + it('should render toggle button', () => { + expect(vm.$el.querySelector('.js-vue-toggle-button')).toBeDefined(); + }); + + it('emits toggle event on click', done => { + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + + vm.$el.querySelector('.js-vue-toggle-button').click(); + + Vue.nextTick(() => { + expect(vm.$emit).toHaveBeenCalledWith('toggleHandler'); + done(); + }); + }); + + it('renders up arrow when open', done => { + vm.expanded = true; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-vue-toggle-button i').classList).toContain( + 'fa-chevron-up', + ); + done(); + }); + }); + + it('renders down arrow when closed', done => { + vm.expanded = false; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-vue-toggle-button i').classList).toContain( + 'fa-chevron-down', + ); + done(); + }); + }); + }); +}); diff --git a/spec/frontend/notes/stores/getters_spec.js b/spec/frontend/notes/stores/getters_spec.js new file mode 100644 index 00000000000..83417bd70ef --- /dev/null +++ b/spec/frontend/notes/stores/getters_spec.js @@ -0,0 +1,388 @@ +import * as getters from '~/notes/stores/getters'; +import { + notesDataMock, + userDataMock, + noteableDataMock, + individualNote, + collapseNotesMock, + discussion1, + discussion2, + discussion3, + resolvedDiscussion1, + unresolvableDiscussion, +} from '../mock_data'; + +const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; + +// Helper function to ensure that we're using the same schema across tests. +const createDiscussionNeighborParams = (discussionId, diffOrder, step) => ({ + discussionId, + diffOrder, + step, +}); + +describe('Getters Notes Store', () => { + let state; + + preloadFixtures(discussionWithTwoUnresolvedNotes); + + beforeEach(() => { + state = { + discussions: [individualNote], + targetNoteHash: 'hash', + lastFetchedAt: 'timestamp', + isNotesFetched: false, + notesData: notesDataMock, + userData: userDataMock, + noteableData: noteableDataMock, + }; + }); + + describe('showJumpToNextDiscussion', () => { + it('should return true if there are 2 or more unresolved discussions', () => { + const localGetters = { + unresolvedDiscussionsIdsByDate: ['123', '456'], + allResolvableDiscussions: [], + }; + + expect(getters.showJumpToNextDiscussion(state, localGetters)()).toBe(true); + }); + + it('should return false if there are 1 or less unresolved discussions', () => { + const localGetters = { + unresolvedDiscussionsIdsByDate: ['123'], + allResolvableDiscussions: [], + }; + + expect(getters.showJumpToNextDiscussion(state, localGetters)()).toBe(false); + }); + }); + + describe('discussions', () => { + it('should return all discussions in the store', () => { + expect(getters.discussions(state)).toEqual([individualNote]); + }); + }); + + describe('resolvedDiscussionsById', () => { + it('ignores unresolved system notes', () => { + const [discussion] = getJSONFixture(discussionWithTwoUnresolvedNotes); + discussion.notes[0].resolved = true; + discussion.notes[1].resolved = false; + state.discussions.push(discussion); + + expect(getters.resolvedDiscussionsById(state)).toEqual({ + [discussion.id]: discussion, + }); + }); + }); + + describe('Collapsed notes', () => { + const stateCollapsedNotes = { + discussions: collapseNotesMock, + targetNoteHash: 'hash', + lastFetchedAt: 'timestamp', + + notesData: notesDataMock, + userData: userDataMock, + noteableData: noteableDataMock, + }; + + it('should return a single system note when a description was updated multiple times', () => { + expect(getters.discussions(stateCollapsedNotes).length).toEqual(1); + }); + }); + + describe('targetNoteHash', () => { + it('should return `targetNoteHash`', () => { + expect(getters.targetNoteHash(state)).toEqual('hash'); + }); + }); + + describe('getNotesData', () => { + it('should return all data in `notesData`', () => { + expect(getters.getNotesData(state)).toEqual(notesDataMock); + }); + }); + + describe('getNoteableData', () => { + it('should return all data in `noteableData`', () => { + expect(getters.getNoteableData(state)).toEqual(noteableDataMock); + }); + }); + + describe('getUserData', () => { + it('should return all data in `userData`', () => { + expect(getters.getUserData(state)).toEqual(userDataMock); + }); + }); + + describe('notesById', () => { + it('should return the note for the given id', () => { + expect(getters.notesById(state)).toEqual({ 1390: individualNote.notes[0] }); + }); + }); + + describe('getCurrentUserLastNote', () => { + it('should return the last note of the current user', () => { + expect(getters.getCurrentUserLastNote(state)).toEqual(individualNote.notes[0]); + }); + }); + + describe('openState', () => { + it('should return the issue state', () => { + expect(getters.openState(state)).toEqual(noteableDataMock.state); + }); + }); + + describe('isNotesFetched', () => { + it('should return the state for the fetching notes', () => { + expect(getters.isNotesFetched(state)).toBeFalsy(); + }); + }); + + describe('allResolvableDiscussions', () => { + it('should return only resolvable discussions in same order', () => { + state.discussions = [ + discussion3, + unresolvableDiscussion, + discussion1, + unresolvableDiscussion, + discussion2, + ]; + + expect(getters.allResolvableDiscussions(state)).toEqual([ + discussion3, + discussion1, + discussion2, + ]); + }); + + it('should return empty array if there are no resolvable discussions', () => { + state.discussions = [unresolvableDiscussion, unresolvableDiscussion]; + + expect(getters.allResolvableDiscussions(state)).toEqual([]); + }); + }); + + describe('unresolvedDiscussionsIdsByDiff', () => { + it('should return all discussions IDs in diff order', () => { + const localGetters = { + allResolvableDiscussions: [discussion3, discussion1, discussion2], + }; + + expect(getters.unresolvedDiscussionsIdsByDiff(state, localGetters)).toEqual([ + 'abc1', + 'abc2', + 'abc3', + ]); + }); + + it('should return empty array if all discussions have been resolved', () => { + const localGetters = { + allResolvableDiscussions: [resolvedDiscussion1], + }; + + expect(getters.unresolvedDiscussionsIdsByDiff(state, localGetters)).toEqual([]); + }); + }); + + describe('unresolvedDiscussionsIdsByDate', () => { + it('should return all discussions in date ascending order', () => { + const localGetters = { + allResolvableDiscussions: [discussion3, discussion1, discussion2], + }; + + expect(getters.unresolvedDiscussionsIdsByDate(state, localGetters)).toEqual([ + 'abc2', + 'abc1', + 'abc3', + ]); + }); + + it('should return empty array if all discussions have been resolved', () => { + const localGetters = { + allResolvableDiscussions: [resolvedDiscussion1], + }; + + expect(getters.unresolvedDiscussionsIdsByDate(state, localGetters)).toEqual([]); + }); + }); + + describe('unresolvedDiscussionsIdsOrdered', () => { + const localGetters = { + unresolvedDiscussionsIdsByDate: ['123', '456'], + unresolvedDiscussionsIdsByDiff: ['abc', 'def'], + }; + + it('should return IDs ordered by diff when diffOrder param is true', () => { + expect(getters.unresolvedDiscussionsIdsOrdered(state, localGetters)(true)).toEqual([ + 'abc', + 'def', + ]); + }); + + it('should return IDs ordered by date when diffOrder param is not true', () => { + expect(getters.unresolvedDiscussionsIdsOrdered(state, localGetters)(false)).toEqual([ + '123', + '456', + ]); + + expect(getters.unresolvedDiscussionsIdsOrdered(state, localGetters)(undefined)).toEqual([ + '123', + '456', + ]); + }); + }); + + describe('isLastUnresolvedDiscussion', () => { + const localGetters = { + unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'], + }; + + it('should return true if the discussion id provided is the last', () => { + expect(getters.isLastUnresolvedDiscussion(state, localGetters)('789')).toBe(true); + }); + + it('should return false if the discussion id provided is not the last', () => { + expect(getters.isLastUnresolvedDiscussion(state, localGetters)('123')).toBe(false); + expect(getters.isLastUnresolvedDiscussion(state, localGetters)('456')).toBe(false); + }); + }); + + describe('findUnresolvedDiscussionIdNeighbor', () => { + let localGetters; + beforeEach(() => { + localGetters = { + unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'], + }; + }); + + [ + { step: 1, id: '123', expected: '456' }, + { step: 1, id: '456', expected: '789' }, + { step: 1, id: '789', expected: '123' }, + { step: -1, id: '123', expected: '789' }, + { step: -1, id: '456', expected: '123' }, + { step: -1, id: '789', expected: '456' }, + ].forEach(({ step, id, expected }) => { + it(`with step ${step} and id ${id}, returns next value`, () => { + const params = createDiscussionNeighborParams(id, true, step); + + expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe( + expected, + ); + }); + }); + + describe('with 1 unresolved discussion', () => { + beforeEach(() => { + localGetters = { + unresolvedDiscussionsIdsOrdered: () => ['123'], + }; + }); + + [{ step: 1, id: '123', expected: '123' }, { step: -1, id: '123', expected: '123' }].forEach( + ({ step, id, expected }) => { + it(`with step ${step} and match, returns only value`, () => { + const params = createDiscussionNeighborParams(id, true, step); + + expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe( + expected, + ); + }); + }, + ); + + it('with no match, returns only value', () => { + const params = createDiscussionNeighborParams('bogus', true, 1); + + expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe('123'); + }); + }); + + describe('with 0 unresolved discussions', () => { + beforeEach(() => { + localGetters = { + unresolvedDiscussionsIdsOrdered: () => [], + }; + }); + + [{ step: 1 }, { step: -1 }].forEach(({ step }) => { + it(`with step ${step}, returns undefined`, () => { + const params = createDiscussionNeighborParams('bogus', true, step); + + expect( + getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params), + ).toBeUndefined(); + }); + }); + }); + }); + + describe('findUnresolvedDiscussionIdNeighbor aliases', () => { + let neighbor; + let findUnresolvedDiscussionIdNeighbor; + let localGetters; + + beforeEach(() => { + neighbor = {}; + findUnresolvedDiscussionIdNeighbor = jest.fn(() => neighbor); + localGetters = { findUnresolvedDiscussionIdNeighbor }; + }); + + describe('nextUnresolvedDiscussionId', () => { + it('should return result of find neighbor', () => { + const expectedParams = createDiscussionNeighborParams('123', true, 1); + const result = getters.nextUnresolvedDiscussionId(state, localGetters)('123', true); + + expect(findUnresolvedDiscussionIdNeighbor).toHaveBeenCalledWith(expectedParams); + expect(result).toBe(neighbor); + }); + }); + + describe('previosuUnresolvedDiscussionId', () => { + it('should return result of find neighbor', () => { + const expectedParams = createDiscussionNeighborParams('123', true, -1); + const result = getters.previousUnresolvedDiscussionId(state, localGetters)('123', true); + + expect(findUnresolvedDiscussionIdNeighbor).toHaveBeenCalledWith(expectedParams); + expect(result).toBe(neighbor); + }); + }); + }); + + describe('firstUnresolvedDiscussionId', () => { + const localGetters = { + unresolvedDiscussionsIdsByDate: ['123', '456'], + unresolvedDiscussionsIdsByDiff: ['abc', 'def'], + }; + + it('should return the first discussion id by diff when diffOrder param is true', () => { + expect(getters.firstUnresolvedDiscussionId(state, localGetters)(true)).toBe('abc'); + }); + + it('should return the first discussion id by date when diffOrder param is not true', () => { + expect(getters.firstUnresolvedDiscussionId(state, localGetters)(false)).toBe('123'); + expect(getters.firstUnresolvedDiscussionId(state, localGetters)(undefined)).toBe('123'); + }); + + it('should be falsy if all discussions are resolved', () => { + const localGettersFalsy = { + unresolvedDiscussionsIdsByDiff: [], + unresolvedDiscussionsIdsByDate: [], + }; + + expect(getters.firstUnresolvedDiscussionId(state, localGettersFalsy)(true)).toBeFalsy(); + expect(getters.firstUnresolvedDiscussionId(state, localGettersFalsy)(false)).toBeFalsy(); + }); + }); + + describe('getDiscussion', () => { + it('returns discussion by ID', () => { + state.discussions.push({ id: '1' }); + + expect(getters.getDiscussion(state)('1')).toEqual({ id: '1' }); + }); + }); +}); diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js new file mode 100644 index 00000000000..49debe348e2 --- /dev/null +++ b/spec/frontend/notes/stores/mutation_spec.js @@ -0,0 +1,584 @@ +import Vue from 'vue'; +import mutations from '~/notes/stores/mutations'; +import { DISCUSSION_NOTE } from '~/notes/constants'; +import { + note, + discussionMock, + notesDataMock, + userDataMock, + noteableDataMock, + individualNote, +} from '../mock_data'; + +const RESOLVED_NOTE = { resolvable: true, resolved: true }; +const UNRESOLVED_NOTE = { resolvable: true, resolved: false }; +const SYSTEM_NOTE = { resolvable: false, resolved: false }; +const WEIRD_NOTE = { resolvable: false, resolved: true }; + +describe('Notes Store mutations', () => { + describe('ADD_NEW_NOTE', () => { + let state; + let noteData; + + beforeEach(() => { + state = { discussions: [] }; + noteData = { + expanded: true, + id: note.discussion_id, + individual_note: true, + notes: [note], + reply_id: note.discussion_id, + }; + mutations.ADD_NEW_NOTE(state, note); + }); + + it('should add a new note to an array of notes', () => { + expect(state).toEqual({ + discussions: [noteData], + }); + + expect(state.discussions.length).toBe(1); + }); + + it('should not add the same note to the notes array', () => { + mutations.ADD_NEW_NOTE(state, note); + + expect(state.discussions.length).toBe(1); + }); + }); + + describe('ADD_NEW_REPLY_TO_DISCUSSION', () => { + const newReply = Object.assign({}, note, { discussion_id: discussionMock.id }); + + let state; + + beforeEach(() => { + state = { discussions: [{ ...discussionMock }] }; + }); + + it('should add a reply to a specific discussion', () => { + mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply); + + expect(state.discussions[0].notes.length).toEqual(4); + }); + + it('should not add the note if it already exists in the discussion', () => { + mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply); + mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply); + + expect(state.discussions[0].notes.length).toEqual(4); + }); + }); + + describe('DELETE_NOTE', () => { + it('should delete a note ', () => { + const state = { discussions: [discussionMock] }; + const toDelete = discussionMock.notes[0]; + const lengthBefore = discussionMock.notes.length; + + mutations.DELETE_NOTE(state, toDelete); + + expect(state.discussions[0].notes.length).toEqual(lengthBefore - 1); + }); + }); + + describe('EXPAND_DISCUSSION', () => { + it('should expand a collapsed discussion', () => { + const discussion = Object.assign({}, discussionMock, { expanded: false }); + + const state = { + discussions: [discussion], + }; + + mutations.EXPAND_DISCUSSION(state, { discussionId: discussion.id }); + + expect(state.discussions[0].expanded).toEqual(true); + }); + }); + + describe('COLLAPSE_DISCUSSION', () => { + it('should collapse an expanded discussion', () => { + const discussion = Object.assign({}, discussionMock, { expanded: true }); + + const state = { + discussions: [discussion], + }; + + mutations.COLLAPSE_DISCUSSION(state, { discussionId: discussion.id }); + + expect(state.discussions[0].expanded).toEqual(false); + }); + }); + + describe('REMOVE_PLACEHOLDER_NOTES', () => { + it('should remove all placeholder notes in indivudal notes and discussion', () => { + const placeholderNote = Object.assign({}, individualNote, { isPlaceholderNote: true }); + const state = { discussions: [placeholderNote] }; + mutations.REMOVE_PLACEHOLDER_NOTES(state); + + expect(state.discussions).toEqual([]); + }); + }); + + describe('SET_NOTES_DATA', () => { + it('should set an object with notesData', () => { + const state = { + notesData: {}, + }; + + mutations.SET_NOTES_DATA(state, notesDataMock); + + expect(state.notesData).toEqual(notesDataMock); + }); + }); + + describe('SET_NOTEABLE_DATA', () => { + it('should set the issue data', () => { + const state = { + noteableData: {}, + }; + + mutations.SET_NOTEABLE_DATA(state, noteableDataMock); + + expect(state.noteableData).toEqual(noteableDataMock); + }); + }); + + describe('SET_USER_DATA', () => { + it('should set the user data', () => { + const state = { + userData: {}, + }; + + mutations.SET_USER_DATA(state, userDataMock); + + expect(state.userData).toEqual(userDataMock); + }); + }); + + describe('SET_INITIAL_DISCUSSIONS', () => { + it('should set the initial notes received', () => { + const state = { + discussions: [], + }; + const legacyNote = { + id: 2, + individual_note: true, + notes: [ + { + note: '1', + }, + { + note: '2', + }, + ], + }; + + mutations.SET_INITIAL_DISCUSSIONS(state, [note, legacyNote]); + + expect(state.discussions[0].id).toEqual(note.id); + expect(state.discussions[1].notes[0].note).toBe(legacyNote.notes[0].note); + expect(state.discussions[2].notes[0].note).toBe(legacyNote.notes[1].note); + expect(state.discussions.length).toEqual(3); + }); + + it('adds truncated_diff_lines if discussion is a diffFile', () => { + const state = { + discussions: [], + }; + + mutations.SET_INITIAL_DISCUSSIONS(state, [ + { + ...note, + diff_file: { + file_hash: 'a', + }, + truncated_diff_lines: [{ text: '+a', rich_text: '+a' }], + }, + ]); + + expect(state.discussions[0].truncated_diff_lines).toEqual([{ rich_text: 'a' }]); + }); + + it('adds empty truncated_diff_lines when not in discussion', () => { + const state = { + discussions: [], + }; + + mutations.SET_INITIAL_DISCUSSIONS(state, [ + { + ...note, + diff_file: { + file_hash: 'a', + }, + }, + ]); + + expect(state.discussions[0].truncated_diff_lines).toEqual([]); + }); + }); + + describe('SET_LAST_FETCHED_AT', () => { + it('should set timestamp', () => { + const state = { + lastFetchedAt: [], + }; + + mutations.SET_LAST_FETCHED_AT(state, 'timestamp'); + + expect(state.lastFetchedAt).toEqual('timestamp'); + }); + }); + + describe('SET_TARGET_NOTE_HASH', () => { + it('should set the note hash', () => { + const state = { + targetNoteHash: [], + }; + + mutations.SET_TARGET_NOTE_HASH(state, 'hash'); + + expect(state.targetNoteHash).toEqual('hash'); + }); + }); + + describe('SHOW_PLACEHOLDER_NOTE', () => { + it('should set a placeholder note', () => { + const state = { + discussions: [], + }; + mutations.SHOW_PLACEHOLDER_NOTE(state, note); + + expect(state.discussions[0].isPlaceholderNote).toEqual(true); + }); + }); + + describe('TOGGLE_AWARD', () => { + it('should add award if user has not reacted yet', () => { + const state = { + discussions: [note], + userData: userDataMock, + }; + + const data = { + note, + awardName: 'cartwheel', + }; + + mutations.TOGGLE_AWARD(state, data); + const lastIndex = state.discussions[0].award_emoji.length - 1; + + expect(state.discussions[0].award_emoji[lastIndex]).toEqual({ + name: 'cartwheel', + user: { id: userDataMock.id, name: userDataMock.name, username: userDataMock.username }, + }); + }); + + it('should remove award if user already reacted', () => { + const state = { + discussions: [note], + userData: { + id: 1, + name: 'Administrator', + username: 'root', + }, + }; + + const data = { + note, + awardName: 'bath_tone3', + }; + mutations.TOGGLE_AWARD(state, data); + + expect(state.discussions[0].award_emoji.length).toEqual(2); + }); + }); + + describe('TOGGLE_DISCUSSION', () => { + it('should open a closed discussion', () => { + const discussion = Object.assign({}, discussionMock, { expanded: false }); + + const state = { + discussions: [discussion], + }; + + mutations.TOGGLE_DISCUSSION(state, { discussionId: discussion.id }); + + expect(state.discussions[0].expanded).toEqual(true); + }); + + it('should close a opened discussion', () => { + const state = { + discussions: [discussionMock], + }; + + mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id }); + + expect(state.discussions[0].expanded).toEqual(false); + }); + + it('forces a discussions expanded state', () => { + const state = { + discussions: [{ ...discussionMock, expanded: false }], + }; + + mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id, forceExpanded: true }); + + expect(state.discussions[0].expanded).toEqual(true); + }); + }); + + describe('UPDATE_NOTE', () => { + it('should update a note', () => { + const state = { + discussions: [individualNote], + }; + + const updated = Object.assign({}, individualNote.notes[0], { note: 'Foo' }); + + mutations.UPDATE_NOTE(state, updated); + + expect(state.discussions[0].notes[0].note).toEqual('Foo'); + }); + + it('transforms an individual note to discussion', () => { + const state = { + discussions: [individualNote], + }; + + const transformedNote = { ...individualNote.notes[0], type: DISCUSSION_NOTE }; + + mutations.UPDATE_NOTE(state, transformedNote); + + expect(state.discussions[0].individual_note).toEqual(false); + }); + }); + + describe('CLOSE_ISSUE', () => { + it('should set issue as closed', () => { + const state = { + discussions: [], + targetNoteHash: null, + lastFetchedAt: null, + isToggleStateButtonLoading: false, + notesData: {}, + userData: {}, + noteableData: {}, + }; + + mutations.CLOSE_ISSUE(state); + + expect(state.noteableData.state).toEqual('closed'); + }); + }); + + describe('REOPEN_ISSUE', () => { + it('should set issue as closed', () => { + const state = { + discussions: [], + targetNoteHash: null, + lastFetchedAt: null, + isToggleStateButtonLoading: false, + notesData: {}, + userData: {}, + noteableData: {}, + }; + + mutations.REOPEN_ISSUE(state); + + expect(state.noteableData.state).toEqual('reopened'); + }); + }); + + describe('TOGGLE_STATE_BUTTON_LOADING', () => { + it('should set isToggleStateButtonLoading as true', () => { + const state = { + discussions: [], + targetNoteHash: null, + lastFetchedAt: null, + isToggleStateButtonLoading: false, + notesData: {}, + userData: {}, + noteableData: {}, + }; + + mutations.TOGGLE_STATE_BUTTON_LOADING(state, true); + + expect(state.isToggleStateButtonLoading).toEqual(true); + }); + + it('should set isToggleStateButtonLoading as false', () => { + const state = { + discussions: [], + targetNoteHash: null, + lastFetchedAt: null, + isToggleStateButtonLoading: true, + notesData: {}, + userData: {}, + noteableData: {}, + }; + + mutations.TOGGLE_STATE_BUTTON_LOADING(state, false); + + expect(state.isToggleStateButtonLoading).toEqual(false); + }); + }); + + describe('SET_NOTES_FETCHED_STATE', () => { + it('should set the given state', () => { + const state = { + isNotesFetched: false, + }; + + mutations.SET_NOTES_FETCHED_STATE(state, true); + + expect(state.isNotesFetched).toEqual(true); + }); + }); + + describe('SET_DISCUSSION_DIFF_LINES', () => { + it('sets truncated_diff_lines', () => { + const state = { + discussions: [ + { + id: 1, + }, + ], + }; + + mutations.SET_DISCUSSION_DIFF_LINES(state, { + discussionId: 1, + diffLines: [{ text: '+a', rich_text: '+a' }], + }); + + expect(state.discussions[0].truncated_diff_lines).toEqual([{ rich_text: 'a' }]); + }); + + it('keeps reactivity of discussion', () => { + const state = {}; + Vue.set(state, 'discussions', [ + { + id: 1, + expanded: false, + }, + ]); + const discussion = state.discussions[0]; + + mutations.SET_DISCUSSION_DIFF_LINES(state, { + discussionId: 1, + diffLines: [{ rich_text: 'a' }], + }); + + discussion.expanded = true; + + expect(state.discussions[0].expanded).toBe(true); + }); + }); + + describe('DISABLE_COMMENTS', () => { + it('should set comments disabled state', () => { + const state = {}; + + mutations.DISABLE_COMMENTS(state, true); + + expect(state.commentsDisabled).toEqual(true); + }); + }); + + describe('UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS', () => { + it('with unresolvable discussions, updates state', () => { + const state = { + discussions: [ + { individual_note: false, resolvable: true, notes: [UNRESOLVED_NOTE] }, + { individual_note: true, resolvable: true, notes: [UNRESOLVED_NOTE] }, + { individual_note: false, resolvable: false, notes: [UNRESOLVED_NOTE] }, + ], + }; + + mutations.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS(state); + + expect(state).toEqual( + expect.objectContaining({ + resolvableDiscussionsCount: 1, + unresolvedDiscussionsCount: 1, + hasUnresolvedDiscussions: false, + }), + ); + }); + + it('with resolvable discussions, updates state', () => { + const state = { + discussions: [ + { + individual_note: false, + resolvable: true, + notes: [RESOLVED_NOTE, SYSTEM_NOTE, RESOLVED_NOTE], + }, + { + individual_note: false, + resolvable: true, + notes: [RESOLVED_NOTE, SYSTEM_NOTE, WEIRD_NOTE], + }, + { + individual_note: false, + resolvable: true, + notes: [SYSTEM_NOTE, RESOLVED_NOTE, WEIRD_NOTE, UNRESOLVED_NOTE], + }, + { + individual_note: false, + resolvable: true, + notes: [UNRESOLVED_NOTE], + }, + ], + }; + + mutations.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS(state); + + expect(state).toEqual( + expect.objectContaining({ + resolvableDiscussionsCount: 4, + unresolvedDiscussionsCount: 2, + hasUnresolvedDiscussions: true, + }), + ); + }); + }); + + describe('CONVERT_TO_DISCUSSION', () => { + let discussion; + let state; + + beforeEach(() => { + discussion = { + id: 42, + individual_note: true, + }; + state = { convertedDisscussionIds: [] }; + }); + + it('adds a discussion to convertedDisscussionIds', () => { + mutations.CONVERT_TO_DISCUSSION(state, discussion.id); + + expect(state.convertedDisscussionIds).toContain(discussion.id); + }); + }); + + describe('REMOVE_CONVERTED_DISCUSSION', () => { + let discussion; + let state; + + beforeEach(() => { + discussion = { + id: 42, + individual_note: true, + }; + state = { convertedDisscussionIds: [41, 42] }; + }); + + it('removes a discussion from convertedDisscussionIds', () => { + mutations.REMOVE_CONVERTED_DISCUSSION(state, discussion.id); + + expect(state.convertedDisscussionIds).not.toContain(discussion.id); + }); + }); +}); -- cgit v1.2.1