diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-20 11:43:17 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-20 11:43:17 +0000 |
commit | dfc94207fec2d84314b1a5410cface22e8b369bd (patch) | |
tree | c54022f61ced104305889a64de080998a0dc773b /spec/frontend/notes | |
parent | b874efeff674f6bf0355d5d242ecf81c6f7155df (diff) | |
download | gitlab-ce-dfc94207fec2d84314b1a5410cface22e8b369bd.tar.gz |
Add latest changes from gitlab-org/gitlab@15-11-stable-eev15.11.0-rc42
Diffstat (limited to 'spec/frontend/notes')
10 files changed, 306 insertions, 350 deletions
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js index 062cd098640..04143bb5b60 100644 --- a/spec/frontend/notes/components/comment_form_spec.js +++ b/spec/frontend/notes/components/comment_form_spec.js @@ -5,6 +5,7 @@ import MockAdapter from 'axios-mock-adapter'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import batchComments from '~/batch_comments/stores/modules/batch_comments'; import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import { createAlert } from '~/alert'; @@ -27,6 +28,8 @@ jest.mock('~/alert'); Vue.use(Vuex); describe('issue_comment_form component', () => { + useLocalStorageSpy(); + let store; let wrapper; let axiosMock; @@ -649,6 +652,37 @@ describe('issue_comment_form component', () => { }); }); + describe('check sensitive tokens', () => { + const sensitiveMessage = 'token: glpat-1234567890abcdefghij'; + const nonSensitiveMessage = 'text'; + + it('should not save note when it contains sensitive token', () => { + mountComponent({ + mountFunction: mount, + initialData: { note: sensitiveMessage }, + }); + + jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue(); + + clickCommentButton(); + + expect(wrapper.vm.saveNote).not.toHaveBeenCalled(); + }); + + it('should save note it does not contain sensitive token', () => { + mountComponent({ + mountFunction: mount, + initialData: { note: nonSensitiveMessage }, + }); + + jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue(); + + clickCommentButton(); + + expect(wrapper.vm.saveNote).toHaveBeenCalled(); + }); + }); + describe('user is not logged in', () => { beforeEach(() => { mountComponent({ userData: null, noteableData: loggedOutnoteableData, mountFunction: mount }); diff --git a/spec/frontend/notes/components/discussion_filter_spec.js b/spec/frontend/notes/components/discussion_filter_spec.js index ed1ced1b3d1..28e5e65c177 100644 --- a/spec/frontend/notes/components/discussion_filter_spec.js +++ b/spec/frontend/notes/components/discussion_filter_spec.js @@ -1,4 +1,4 @@ -import { GlDropdown } from '@gitlab/ui'; +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import AxiosMockAdapter from 'axios-mock-adapter'; @@ -77,17 +77,16 @@ describe('DiscussionFilter component', () => { // as it doesn't matter for our tests here mock.onGet(DISCUSSION_PATH).reply(HTTP_STATUS_OK, ''); window.mrTabs = undefined; - wrapper = mountComponent(); jest.spyOn(Tracking, 'event'); }); afterEach(() => { - wrapper.vm.$destroy(); mock.restore(); }); describe('default', () => { beforeEach(() => { + wrapper = mountComponent(); jest.spyOn(store, 'dispatch').mockImplementation(); }); @@ -104,6 +103,7 @@ describe('DiscussionFilter component', () => { describe('when asc', () => { beforeEach(() => { + wrapper = mountComponent(); jest.spyOn(store, 'dispatch').mockImplementation(); }); @@ -123,6 +123,7 @@ describe('DiscussionFilter component', () => { describe('when desc', () => { beforeEach(() => { + wrapper = mountComponent(); store.state.discussionSortOrder = DESC; jest.spyOn(store, 'dispatch').mockImplementation(); }); @@ -145,56 +146,62 @@ describe('DiscussionFilter component', () => { }); }); - it('renders the all filters', () => { - expect(wrapper.findAll('.discussion-filter-container .dropdown-item').length).toBe( - discussionFiltersMock.length, - ); - }); + describe('discussion filter functionality', () => { + beforeEach(() => { + wrapper = mountComponent(); + }); - it('renders the default selected item', () => { - expect(wrapper.find('.discussion-filter-container .dropdown-item').text().trim()).toBe( - discussionFiltersMock[0].title, - ); - }); + it('renders the all filters', () => { + expect(wrapper.findAll('.discussion-filter-container .dropdown-item').length).toBe( + discussionFiltersMock.length, + ); + }); - it('disables the dropdown when discussions are loading', () => { - store.state.isLoading = true; + it('renders the default selected item', () => { + expect(wrapper.find('.discussion-filter-container .dropdown-item').text().trim()).toBe( + discussionFiltersMock[0].title, + ); + }); - expect(wrapper.findComponent(GlDropdown).props('disabled')).toBe(true); - }); + it('disables the dropdown when discussions are loading', () => { + store.state.isLoading = true; - it('updates to the selected item', () => { - const filterItem = findFilter(DISCUSSION_FILTER_TYPES.ALL); + expect(wrapper.findComponent(GlDropdown).props('disabled')).toBe(true); + }); - filterItem.trigger('click'); + it('updates to the selected item', () => { + const filterItem = findFilter(DISCUSSION_FILTER_TYPES.ALL); - expect(wrapper.vm.currentFilter.title).toBe(filterItem.text().trim()); - }); + filterItem.trigger('click'); - it('only updates when selected filter changes', () => { - findFilter(DISCUSSION_FILTER_TYPES.ALL).trigger('click'); + expect(filterItem.text().trim()).toBe('Show all activity'); + }); - expect(filterDiscussion).not.toHaveBeenCalled(); - }); + it('only updates when selected filter changes', () => { + findFilter(DISCUSSION_FILTER_TYPES.ALL).trigger('click'); + + expect(filterDiscussion).not.toHaveBeenCalled(); + }); - it('disables timeline view if it was enabled', () => { - store.state.isTimelineEnabled = true; + it('disables timeline view if it was enabled', () => { + store.state.isTimelineEnabled = true; - findFilter(DISCUSSION_FILTER_TYPES.HISTORY).trigger('click'); + findFilter(DISCUSSION_FILTER_TYPES.HISTORY).trigger('click'); - expect(wrapper.vm.$store.state.isTimelineEnabled).toBe(false); - }); + expect(store.state.isTimelineEnabled).toBe(false); + }); - it('disables commenting when "Show history only" filter is applied', () => { - findFilter(DISCUSSION_FILTER_TYPES.HISTORY).trigger('click'); + it('disables commenting when "Show history only" filter is applied', () => { + findFilter(DISCUSSION_FILTER_TYPES.HISTORY).trigger('click'); - expect(wrapper.vm.$store.state.commentsDisabled).toBe(true); - }); + expect(store.state.commentsDisabled).toBe(true); + }); - it('enables commenting when "Show history only" filter is not applied', () => { - findFilter(DISCUSSION_FILTER_TYPES.ALL).trigger('click'); + it('enables commenting when "Show history only" filter is not applied', () => { + findFilter(DISCUSSION_FILTER_TYPES.ALL).trigger('click'); - expect(wrapper.vm.$store.state.commentsDisabled).toBe(false); + expect(store.state.commentsDisabled).toBe(false); + }); }); describe('Merge request tabs', () => { @@ -222,52 +229,41 @@ describe('DiscussionFilter component', () => { }); describe('URL with Links to notes', () => { + const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + afterEach(() => { window.location.hash = ''; }); - it('updates the filter when the URL links to a note', async () => { - window.location.hash = `note_${discussionMock.notes[0].id}`; - wrapper.vm.currentValue = discussionFiltersMock[2].value; - wrapper.vm.handleLocationHash(); - - await nextTick(); - expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE); - }); - it('does not update the filter when the current filter is "Show all activity"', async () => { window.location.hash = `note_${discussionMock.notes[0].id}`; - wrapper.vm.handleLocationHash(); + wrapper = mountComponent(); await nextTick(); - expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE); + const filtered = findDropdownItems().filter((el) => el.classes('is-active')); + + expect(filtered).toHaveLength(1); + expect(filtered.at(0).text()).toBe(discussionFiltersMock[0].title); }); it('only updates filter when the URL links to a note', async () => { window.location.hash = `testing123`; - wrapper.vm.handleLocationHash(); + wrapper = mountComponent(); await nextTick(); - expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE); - }); + const filtered = findDropdownItems().filter((el) => el.classes('is-active')); - it('fetches discussions when there is a hash', async () => { - window.location.hash = `note_${discussionMock.notes[0].id}`; - wrapper.vm.currentValue = discussionFiltersMock[2].value; - jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {}); - wrapper.vm.handleLocationHash(); - - await nextTick(); - expect(wrapper.vm.selectFilter).toHaveBeenCalled(); + expect(filtered).toHaveLength(1); + expect(filtered.at(0).text()).toBe(discussionFiltersMock[0].title); }); it('does not fetch discussions when there is no hash', async () => { window.location.hash = ''; - jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {}); - wrapper.vm.handleLocationHash(); + const selectFilterSpy = jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {}); + wrapper = mountComponent(); await nextTick(); - expect(wrapper.vm.selectFilter).not.toHaveBeenCalled(); + expect(selectFilterSpy).not.toHaveBeenCalled(); }); }); }); diff --git a/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js b/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js index bee08ee0605..7860e9d45da 100644 --- a/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js +++ b/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js @@ -22,7 +22,7 @@ describe('NoteTimelineEventButton', () => { const findTimelineButton = () => wrapper.findComponent(GlButton); - it('emits click-promote-comment-to-event', async () => { + it('emits click-promote-comment-to-event', () => { findTimelineButton().vm.$emit('click'); expect(wrapper.emitted('click-promote-comment-to-event')).toEqual([[emitData]]); diff --git a/spec/frontend/notes/components/note_awards_list_spec.js b/spec/frontend/notes/components/note_awards_list_spec.js index 89ac0216f41..0107b27f980 100644 --- a/spec/frontend/notes/components/note_awards_list_spec.js +++ b/spec/frontend/notes/components/note_awards_list_spec.js @@ -1,76 +1,110 @@ import AxiosMockAdapter from 'axios-mock-adapter'; import Vue from 'vue'; +import Vuex from 'vuex'; import { TEST_HOST } from 'helpers/test_constants'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { userDataMock } from 'jest/notes/mock_data'; +import EmojiPicker from '~/emoji/components/picker.vue'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import awardsNote from '~/notes/components/note_awards_list.vue'; import createStore from '~/notes/stores'; -import { noteableDataMock, notesDataMock } from '../mock_data'; -describe('note_awards_list component', () => { - let store; - let vm; - let awardsMock; - let mock; - - const toggleAwardPath = `${TEST_HOST}/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji`; - - beforeEach(() => { - mock = new AxiosMockAdapter(axios); - - mock.onPost(toggleAwardPath).reply(HTTP_STATUS_OK, ''); +Vue.use(Vuex); - const Component = Vue.extend(awardsNote); - - store = createStore(); - store.dispatch('setNoteableData', noteableDataMock); - store.dispatch('setNotesData', notesDataMock); - awardsMock = [ - { - name: 'flag_tz', - user: { id: 1, name: 'Administrator', username: 'root' }, - }, - { - name: 'cartwheel_tone3', - user: { id: 12, name: 'Bobbie Stehr', username: 'erin' }, - }, - ]; +describe('Note Awards List', () => { + let wrapper; + let mock; - vm = new Component({ + const awardsMock = [ + { + name: 'flag_tz', + user: { id: 1, name: 'Administrator', username: 'root' }, + }, + { + name: 'cartwheel_tone3', + user: { id: 12, name: 'Bobbie Stehr', username: 'erin' }, + }, + ]; + const toggleAwardPathMock = `${TEST_HOST}/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji`; + + const defaultProps = { + awards: awardsMock, + noteAuthorId: 2, + noteId: '545', + canAwardEmoji: false, + toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji', + }; + + const findAddAward = () => wrapper.find('.js-add-award'); + const findAwardButton = () => wrapper.findByTestId('award-button'); + const findAllEmojiAwards = () => wrapper.findAll('gl-emoji'); + const findEmojiPicker = () => wrapper.findComponent(EmojiPicker); + + const createComponent = (props = defaultProps, store = createStore()) => { + wrapper = mountExtended(awardsNote, { store, propsData: { - awards: awardsMock, - noteAuthorId: 2, - noteId: '545', - canAwardEmoji: true, - toggleAwardPath, + ...props, }, - }).$mount(); - }); + }); + }; + + describe('Note Awards functionality', () => { + const toggleAwardRequestSpy = jest.fn(); + const fakeStore = () => { + return new Vuex.Store({ + getters: { + getUserData: () => userDataMock, + }, + actions: { + toggleAwardRequest: toggleAwardRequestSpy, + }, + }); + }; - afterEach(() => { - mock.restore(); - vm.$destroy(); - }); + beforeEach(() => { + mock = new AxiosMockAdapter(axios); + mock.onPost(toggleAwardPathMock).reply(HTTP_STATUS_OK, ''); - it('should render awarded emojis', () => { - expect(vm.$el.querySelector('.js-awards-block button [data-name="flag_tz"]')).toBeDefined(); - expect( - vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]'), - ).toBeDefined(); - }); + createComponent( + { + awards: awardsMock, + noteAuthorId: 2, + noteId: '545', + canAwardEmoji: true, + toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji', + }, + fakeStore(), + ); + }); - it('should be possible to remove awarded emoji', () => { - jest.spyOn(vm, 'handleAward'); - jest.spyOn(vm, 'toggleAwardRequest'); - vm.$el.querySelector('.js-awards-block button').click(); + afterEach(() => { + mock.restore(); + }); - expect(vm.handleAward).toHaveBeenCalledWith('flag_tz'); - expect(vm.toggleAwardRequest).toHaveBeenCalled(); - }); + it('should render awarded emojis', () => { + const emojiAwards = findAllEmojiAwards(); + + expect(emojiAwards).toHaveLength(awardsMock.length); + expect(emojiAwards.at(0).attributes('data-name')).toBe('flag_tz'); + expect(emojiAwards.at(1).attributes('data-name')).toBe('cartwheel_tone3'); + }); + + it('should be possible to add new emoji', () => { + expect(findEmojiPicker().exists()).toBe(true); + }); + + it('should be possible to remove awarded emoji', async () => { + await findAwardButton().vm.$emit('click'); - it('should be possible to add new emoji', () => { - expect(vm.$el.querySelector('.js-add-award')).toBeDefined(); + const { toggleAwardPath, noteId } = defaultProps; + expect(toggleAwardRequestSpy).toHaveBeenCalledWith(expect.anything(), { + awardName: awardsMock[0].name, + endpoint: toggleAwardPath, + noteId, + }); + }); }); describe('when the user name contains special HTML characters', () => { @@ -79,85 +113,69 @@ describe('note_awards_list component', () => { user: { id: index, name: `&<>"\`'-${index}`, username: `user-${index}` }, }); - const mountComponent = () => { - const Component = Vue.extend(awardsNote); - vm = new Component({ - store, - propsData: { - awards: awardsMock, - noteAuthorId: 0, - noteId: '545', - canAwardEmoji: true, - toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji', - }, - }).$mount(); + const customProps = { + awards: awardsMock, + noteAuthorId: 0, + noteId: '545', + canAwardEmoji: true, + toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji', }; - const findTooltip = () => vm.$el.querySelector('[title]').getAttribute('title'); - - it('should only escape & and " characters', () => { - awardsMock = [...new Array(1)].map(createAwardEmoji); - mountComponent(); - const escapedName = awardsMock[0].user.name.replace(/&/g, '&').replace(/"/g, '"'); - - expect(vm.$el.querySelector('[title]').outerHTML).toContain(escapedName); - }); - it('should not escape special HTML characters twice when only 1 person awarded', () => { - awardsMock = [...new Array(1)].map(createAwardEmoji); - mountComponent(); + const awardsCopy = [...new Array(1)].map(createAwardEmoji); + createComponent({ + ...customProps, + awards: awardsCopy, + }); - awardsMock.forEach((award) => { - expect(findTooltip()).toContain(award.user.name); + awardsCopy.forEach((award) => { + expect(findAwardButton().attributes('title')).toContain(award.user.name); }); }); it('should not escape special HTML characters twice when 2 people awarded', () => { - awardsMock = [...new Array(2)].map(createAwardEmoji); - mountComponent(); + const awardsCopy = [...new Array(2)].map(createAwardEmoji); + createComponent({ + ...customProps, + awards: awardsCopy, + }); - awardsMock.forEach((award) => { - expect(findTooltip()).toContain(award.user.name); + awardsCopy.forEach((award) => { + expect(findAwardButton().attributes('title')).toContain(award.user.name); }); }); it('should not escape special HTML characters twice when more than 10 people awarded', () => { - awardsMock = [...new Array(11)].map(createAwardEmoji); - mountComponent(); + const awardsCopy = [...new Array(11)].map(createAwardEmoji); + createComponent({ + ...customProps, + awards: awardsCopy, + }); // Testing only the first 10 awards since 11 onward will not be displayed. - awardsMock.slice(0, 10).forEach((award) => { - expect(findTooltip()).toContain(award.user.name); + awardsCopy.slice(0, 10).forEach((award) => { + expect(findAwardButton().attributes('title')).toContain(award.user.name); }); }); }); - describe('when the user cannot award emoji', () => { + describe('when the user cannot award an emoji', () => { beforeEach(() => { - const Component = Vue.extend(awardsNote); - - vm = new Component({ - store, - propsData: { - awards: awardsMock, - noteAuthorId: 2, - noteId: '545', - canAwardEmoji: false, - toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji', - }, - }).$mount(); + createComponent({ + awards: awardsMock, + noteAuthorId: 2, + noteId: '545', + canAwardEmoji: false, + toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji', + }); }); - it('should not be possible to remove awarded emoji', () => { - jest.spyOn(vm, 'toggleAwardRequest'); - - vm.$el.querySelector('.js-awards-block button').click(); - - expect(vm.toggleAwardRequest).not.toHaveBeenCalled(); + it('should display an award emoji button with a disabled class', () => { + expect(findAwardButton().classes()).toContain('disabled'); }); it('should not be possible to add new emoji', () => { - expect(vm.$el.querySelector('.js-add-award')).toBeNull(); + expect(findAddAward().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js index b4f185004bb..c4f8e50b969 100644 --- a/spec/frontend/notes/components/note_body_spec.js +++ b/spec/frontend/notes/components/note_body_spec.js @@ -7,10 +7,7 @@ import NoteAwardsList from '~/notes/components/note_awards_list.vue'; import NoteForm from '~/notes/components/note_form.vue'; import createStore from '~/notes/stores'; import notes from '~/notes/stores/modules/index'; -import Autosave from '~/autosave'; - import Suggestions from '~/vue_shared/components/markdown/suggestions.vue'; - import { noteableDataMock, notesDataMock, note } from '../mock_data'; jest.mock('~/autosave'); @@ -82,11 +79,6 @@ describe('issue_note_body component', () => { expect(wrapper.findComponent(NoteForm).props('saveButtonTitle')).toBe(buttonText); }); - it('adds autosave', () => { - // passing undefined instead of an element because of shallowMount - expect(Autosave).toHaveBeenCalledWith(undefined, ['Note', note.noteable_type, note.id]); - }); - describe('isInternalNote', () => { beforeEach(() => { wrapper.setProps({ isInternalNote: true }); diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js index 59362e18098..d6413d33c99 100644 --- a/spec/frontend/notes/components/note_form_spec.js +++ b/spec/frontend/notes/components/note_form_spec.js @@ -1,42 +1,39 @@ -import { GlLink } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; +import { GlLink, GlFormCheckbox } from '@gitlab/ui'; import { nextTick } from 'vue'; import batchComments from '~/batch_comments/stores/modules/batch_comments'; -import { getDraft, updateDraft } from '~/lib/utils/autosave'; import NoteForm from '~/notes/components/note_form.vue'; import createStore from '~/notes/stores'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import { AT_WHO_ACTIVE_CLASS } from '~/gfm_auto_complete'; +import eventHub from '~/environments/event_hub'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import { noteableDataMock, notesDataMock, discussionMock, note } from '../mock_data'; jest.mock('~/lib/utils/autosave'); describe('issue_note_form component', () => { - const dummyAutosaveKey = 'some-autosave-key'; - const dummyDraft = 'dummy draft content'; - let store; let wrapper; let props; - const createComponentWrapper = () => { - return mount(NoteForm, { + const createComponentWrapper = (propsData = {}, provide = {}) => { + wrapper = mountExtended(NoteForm, { store, - propsData: props, + propsData: { + ...props, + ...propsData, + }, + provide: { + glFeatures: provide, + }, }); }; - const findCancelButton = () => wrapper.find('[data-testid="cancel"]'); + const findCancelButton = () => wrapper.findByTestId('cancel'); + const findCancelCommentButton = () => wrapper.findByTestId('cancelBatchCommentsEnabled'); + const findMarkdownField = () => wrapper.findComponent(MarkdownField); beforeEach(() => { - getDraft.mockImplementation((key) => { - if (key === dummyAutosaveKey) { - return dummyDraft; - } - - return null; - }); - store = createStore(); store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); @@ -50,27 +47,37 @@ describe('issue_note_form component', () => { describe('noteHash', () => { beforeEach(() => { - wrapper = createComponentWrapper(); + createComponentWrapper(); }); it('returns note hash string based on `noteId`', () => { expect(wrapper.vm.noteHash).toBe(`#note_${props.noteId}`); }); - it('return note hash as `#` when `noteId` is empty', async () => { - wrapper.setProps({ - ...props, + it('return note hash as `#` when `noteId` is empty', () => { + createComponentWrapper({ noteId: '', }); - await nextTick(); expect(wrapper.vm.noteHash).toBe('#'); }); }); + it('hides content editor switcher if feature flag content_editor_on_issues is off', () => { + createComponentWrapper({}, { contentEditorOnIssues: false }); + + expect(wrapper.text()).not.toContain('Rich text'); + }); + + it('shows content editor switcher if feature flag content_editor_on_issues is on', () => { + createComponentWrapper({}, { contentEditorOnIssues: true }); + + expect(wrapper.text()).toContain('Rich text'); + }); + describe('conflicts editing', () => { beforeEach(() => { - wrapper = createComponentWrapper(); + createComponentWrapper(); }); it('should show conflict message if note changes outside the component', async () => { @@ -94,15 +101,13 @@ describe('issue_note_form component', () => { describe('form', () => { beforeEach(() => { - wrapper = createComponentWrapper(); + createComponentWrapper(); }); it('should render text area with placeholder', () => { const textarea = wrapper.find('textarea'); - expect(textarea.attributes('placeholder')).toEqual( - 'Write a comment or drag your files here…', - ); + expect(textarea.attributes('placeholder')).toBe('Write a comment or drag your files here…'); }); it('should set data-supports-quick-actions to enable autocomplete', () => { @@ -117,23 +122,21 @@ describe('issue_note_form component', () => { ${true} | ${'Write an internal note or drag your files here…'} `( 'should set correct textarea placeholder text when discussion confidentiality is $internal', - ({ internal, placeholder }) => { + async ({ internal, placeholder }) => { props.note = { ...note, internal, }; - wrapper = createComponentWrapper(); + createComponentWrapper(); + + await nextTick(); expect(wrapper.find('textarea').attributes('placeholder')).toBe(placeholder); }, ); it('should link to markdown docs', () => { - const { markdownDocsPath } = notesDataMock; - const markdownField = wrapper.findComponent(MarkdownField); - const markdownFieldProps = markdownField.props(); - - expect(markdownFieldProps.markdownDocsPath).toBe(markdownDocsPath); + expect(findMarkdownField().props('markdownDocsPath')).toBe(notesDataMock.markdownDocsPath); }); describe('keyboard events', () => { @@ -146,12 +149,11 @@ describe('issue_note_form component', () => { describe('up', () => { it('should ender edit mode', () => { - // TODO: do not spy on vm - jest.spyOn(wrapper.vm, 'editMyLastNote'); + const eventHubSpy = jest.spyOn(eventHub, '$emit'); textarea.trigger('keydown.up'); - expect(wrapper.vm.editMyLastNote).toHaveBeenCalled(); + expect(eventHubSpy).not.toHaveBeenCalled(); }); }); @@ -159,17 +161,13 @@ describe('issue_note_form component', () => { it('should save note when cmd+enter is pressed', () => { textarea.trigger('keydown.enter', { metaKey: true }); - const { handleFormUpdate } = wrapper.emitted(); - - expect(handleFormUpdate.length).toBe(1); + expect(wrapper.emitted('handleFormUpdate')).toHaveLength(1); }); it('should save note when ctrl+enter is pressed', () => { textarea.trigger('keydown.enter', { ctrlKey: true }); - const { handleFormUpdate } = wrapper.emitted(); - - expect(handleFormUpdate.length).toBe(1); + expect(wrapper.emitted('handleFormUpdate')).toHaveLength(1); }); it('should disable textarea when ctrl+enter is pressed', async () => { @@ -185,151 +183,62 @@ describe('issue_note_form component', () => { }); describe('actions', () => { - it('should be possible to cancel', async () => { - wrapper.setProps({ - ...props, - }); - await nextTick(); + it('should be possible to cancel', () => { + createComponentWrapper(); - const cancelButton = findCancelButton(); - cancelButton.vm.$emit('click'); - await nextTick(); + findCancelButton().vm.$emit('click'); - expect(wrapper.emitted().cancelForm).toHaveLength(1); + expect(wrapper.emitted('cancelForm')).toHaveLength(1); }); it('will not cancel form if there is an active at-who-active class', async () => { - wrapper.setProps({ - ...props, - }); - await nextTick(); + createComponentWrapper(); - const textareaEl = wrapper.vm.$refs.textarea; + const textareaEl = wrapper.vm.$refs.markdownEditor.$el.querySelector('textarea'); const cancelButton = findCancelButton(); textareaEl.classList.add(AT_WHO_ACTIVE_CLASS); cancelButton.vm.$emit('click'); await nextTick(); - expect(wrapper.emitted().cancelForm).toBeUndefined(); + expect(wrapper.emitted('cancelForm')).toBeUndefined(); }); - it('should be possible to update the note', async () => { - wrapper.setProps({ - ...props, - }); - await nextTick(); + it('should be possible to update the note', () => { + createComponentWrapper(); const textarea = wrapper.find('textarea'); textarea.setValue('Foo'); const saveButton = wrapper.find('.js-vue-issue-save'); saveButton.vm.$emit('click'); - expect(wrapper.vm.isSubmitting).toBe(true); + expect(wrapper.emitted('handleFormUpdate')).toHaveLength(1); }); }); }); - describe('with autosaveKey', () => { - describe('with draft', () => { - beforeEach(() => { - Object.assign(props, { - noteBody: '', - autosaveKey: dummyAutosaveKey, - }); - wrapper = createComponentWrapper(); - - return nextTick(); - }); - - it('displays the draft in textarea', () => { - const textarea = wrapper.find('textarea'); - - expect(textarea.element.value).toBe(dummyDraft); - }); - }); - - describe('without draft', () => { - beforeEach(() => { - Object.assign(props, { - noteBody: '', - autosaveKey: 'some key without draft', - }); - wrapper = createComponentWrapper(); - - return nextTick(); - }); - - it('leaves the textarea empty', () => { - const textarea = wrapper.find('textarea'); - - expect(textarea.element.value).toBe(''); - }); - }); - - it('updates the draft if textarea content changes', () => { - Object.assign(props, { - noteBody: '', - autosaveKey: dummyAutosaveKey, - }); - wrapper = createComponentWrapper(); - const textarea = wrapper.find('textarea'); - const dummyContent = 'some new content'; - - textarea.setValue(dummyContent); - - expect(updateDraft).toHaveBeenCalledWith(dummyAutosaveKey, dummyContent); - }); - - it('does not save draft when ctrl+enter is pressed', () => { - const options = { - noteBody: '', - autosaveKey: dummyAutosaveKey, - }; - - props = { ...props, ...options }; - wrapper = createComponentWrapper(); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ isSubmittingWithKeydown: true }); - - const textarea = wrapper.find('textarea'); - textarea.setValue('some content'); - textarea.trigger('keydown.enter', { metaKey: true }); - - expect(updateDraft).not.toHaveBeenCalled(); - }); - }); - describe('with batch comments', () => { beforeEach(() => { store.registerModule('batchComments', batchComments()); - wrapper = createComponentWrapper(); - wrapper.setProps({ - ...props, + createComponentWrapper({ isDraft: true, noteId: '', discussion: { ...discussionMock, for_commit: false }, }); }); - it('should be possible to cancel', async () => { - jest.spyOn(wrapper.vm, 'cancelHandler'); + it('should be possible to cancel', () => { + findCancelCommentButton().vm.$emit('click'); - await nextTick(); - const cancelButton = wrapper.find('[data-testid="cancelBatchCommentsEnabled"]'); - cancelButton.vm.$emit('click'); - - expect(wrapper.vm.cancelHandler).toHaveBeenCalledWith(true); + expect(wrapper.emitted('cancelForm')).toEqual([[true, false]]); }); it('shows resolve checkbox', () => { - expect(wrapper.find('.js-resolve-checkbox').exists()).toBe(true); + expect(wrapper.findComponent(GlFormCheckbox).exists()).toBe(true); }); - it('hides resolve checkbox', async () => { - wrapper.setProps({ + it('hides resolve checkbox', () => { + createComponentWrapper({ isDraft: false, discussion: { ...discussionMock, @@ -344,15 +253,11 @@ describe('issue_note_form component', () => { }, }); - await nextTick(); - - expect(wrapper.find('.js-resolve-checkbox').exists()).toBe(false); + expect(wrapper.findComponent(GlFormCheckbox).exists()).toBe(false); }); - it('hides actions for commits', async () => { - wrapper.setProps({ discussion: { for_commit: true } }); - - await nextTick(); + it('hides actions for commits', () => { + createComponentWrapper({ discussion: { for_commit: true } }); expect(wrapper.find('.note-form-actions').text()).not.toContain('Start a review'); }); @@ -361,13 +266,12 @@ describe('issue_note_form component', () => { it('should start review or add to review when cmd+enter is pressed', async () => { const textarea = wrapper.find('textarea'); - jest.spyOn(wrapper.vm, 'handleAddToReview'); - textarea.setValue('Foo'); textarea.trigger('keydown.enter', { metaKey: true }); await nextTick(); - expect(wrapper.vm.handleAddToReview).toHaveBeenCalled(); + + expect(wrapper.emitted('handleFormUpdateAddToReview')).toEqual([['Foo', false]]); }); }); }); diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js index b158cfff10d..bce335aa035 100644 --- a/spec/frontend/notes/components/noteable_note_spec.js +++ b/spec/frontend/notes/components/noteable_note_spec.js @@ -375,6 +375,17 @@ describe('issue_note', () => { expect(wrapper.emitted('handleUpdateNote')).toHaveLength(1); }); + it('should not update note with sensitive token', () => { + const sensitiveMessage = 'token: glpat-1234567890abcdefghij'; + + createWrapper(); + updateActions(); + wrapper + .findComponent(NoteBody) + .vm.$emit('handleFormUpdate', { ...params, noteText: sensitiveMessage }); + expect(updateNote).not.toHaveBeenCalled(); + }); + it('does not stringify empty position', () => { createWrapper(); updateActions(); diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js index 832264aa7d3..3fe31506223 100644 --- a/spec/frontend/notes/components/notes_app_spec.js +++ b/spec/frontend/notes/components/notes_app_spec.js @@ -174,7 +174,7 @@ describe('note_app', () => { }); describe('while fetching data', () => { - beforeEach(async () => { + beforeEach(() => { wrapper = mountComponent(); }); diff --git a/spec/frontend/notes/deprecated_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js index 40f10ca901b..355ecb78187 100644 --- a/spec/frontend/notes/deprecated_notes_spec.js +++ b/spec/frontend/notes/deprecated_notes_spec.js @@ -1,9 +1,11 @@ /* eslint-disable import/no-commonjs, no-new */ -import MockAdapter from 'axios-mock-adapter'; import $ from 'jquery'; +import MockAdapter from 'axios-mock-adapter'; +import htmlPipelineSchedulesEditSnippets from 'test_fixtures/snippets/show.html'; +import htmlPipelineSchedulesEditCommit from 'test_fixtures/commit/show.html'; import '~/behaviors/markdown/render_gfm'; -import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { TEST_HOST } from 'helpers/test_constants'; import waitForPromises from 'helpers/wait_for_promises'; import axios from '~/lib/utils/axios_utils'; @@ -19,7 +21,6 @@ const Notes = require('~/deprecated_notes').default; const FLASH_TYPE_ALERT = 'alert'; const NOTES_POST_PATH = /(.*)\/notes\?html=true$/; -const fixture = 'snippets/show.html'; let mockAxios; window.project_uploads_path = `${TEST_HOST}/uploads`; @@ -36,7 +37,7 @@ function wrappedDiscussionNote(note) { // eslint-disable-next-line jest/no-disabled-tests describe.skip('Old Notes (~/deprecated_notes.js)', () => { beforeEach(() => { - loadHTMLFixture(fixture); + setHTMLFixture(htmlPipelineSchedulesEditSnippets); // Re-declare this here so that test_setup.js#beforeEach() doesn't // overwrite it. @@ -671,7 +672,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => { let $notesContainer; beforeEach(() => { - loadHTMLFixture('commit/show.html'); + setHTMLFixture(htmlPipelineSchedulesEditCommit); mockAxios.onPost(NOTES_POST_PATH).reply(HTTP_STATUS_OK, note); new Notes('', []); diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js index 0d3ebea7af2..97249d232dc 100644 --- a/spec/frontend/notes/stores/actions_spec.js +++ b/spec/frontend/notes/stores/actions_spec.js @@ -257,14 +257,14 @@ describe('Actions Notes Store', () => { axiosMock.onGet(notesDataMock.notesPath).reply(HTTP_STATUS_OK, pollResponse, pollHeaders); const failureMock = () => axiosMock.onGet(notesDataMock.notesPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); - const advanceAndRAF = async (time) => { + const advanceAndRAF = (time) => { if (time) { jest.advanceTimersByTime(time); } return waitForPromises(); }; - const advanceXMoreIntervals = async (number) => { + const advanceXMoreIntervals = (number) => { const timeoutLength = pollInterval * number; return advanceAndRAF(timeoutLength); @@ -273,7 +273,7 @@ describe('Actions Notes Store', () => { await store.dispatch('poll'); await advanceAndRAF(2); }; - const cleanUp = async () => { + const cleanUp = () => { jest.clearAllTimers(); return store.dispatch('stopPolling'); |