diff options
Diffstat (limited to 'spec')
32 files changed, 602 insertions, 389 deletions
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb index 0613148172f..74d3544ce92 100644 --- a/spec/features/projects/settings/registry_settings_spec.rb +++ b/spec/features/projects/settings/registry_settings_spec.rb @@ -26,7 +26,6 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy' it 'saves expiration policy submit the form' do within '#js-registry-policies' do within '.card-body' do - find('.gl-toggle-wrapper button:not(.is-disabled)').click select('7 days until tags are automatically removed', from: 'Expiration interval:') select('Every day', from: 'Expiration schedule:') select('50 tags per image name', from: 'Number of tags to retain:') diff --git a/spec/frontend/__mocks__/sortablejs/index.js b/spec/frontend/__mocks__/sortablejs/index.js index a1166d21561..5039af54542 100644 --- a/spec/frontend/__mocks__/sortablejs/index.js +++ b/spec/frontend/__mocks__/sortablejs/index.js @@ -1,4 +1,4 @@ -import Sortablejs from 'sortablejs'; +const Sortablejs = jest.genMockFromModule('sortablejs'); export default Sortablejs; export const Sortable = Sortablejs; diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js new file mode 100644 index 00000000000..7cf6ec913b4 --- /dev/null +++ b/spec/frontend/boards/components/board_column_spec.js @@ -0,0 +1,172 @@ +import Vue from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import AxiosMockAdapter from 'axios-mock-adapter'; + +import Board from '~/boards/components/board_column.vue'; +import List from '~/boards/models/list'; +import { ListType } from '~/boards/constants'; +import axios from '~/lib/utils/axios_utils'; + +import { TEST_HOST } from 'helpers/test_constants'; +import { listObj } from 'jest/boards/mock_data'; + +describe('Board Column Component', () => { + let wrapper; + let axiosMock; + + beforeEach(() => { + window.gon = {}; + axiosMock = new AxiosMockAdapter(axios); + axiosMock.onGet(`${TEST_HOST}/lists/1/issues`).reply(200, { issues: [] }); + }); + + afterEach(() => { + axiosMock.restore(); + + wrapper.destroy(); + + localStorage.clear(); + }); + + const createComponent = ({ + listType = ListType.backlog, + collapsed = false, + withLocalStorage = true, + } = {}) => { + const boardId = '1'; + + const listMock = { + ...listObj, + list_type: listType, + collapsed, + }; + + if (listType === ListType.assignee) { + delete listMock.label; + listMock.user = {}; + } + + // Making List reactive + const list = Vue.observable(new List(listMock)); + + if (withLocalStorage) { + localStorage.setItem( + `boards.${boardId}.${list.type}.${list.id}.expanded`, + (!collapsed).toString(), + ); + } + + wrapper = shallowMount(Board, { + propsData: { + boardId, + disabled: false, + issueLinkBase: '/', + rootPath: '/', + list, + }, + }); + }; + + const isExpandable = () => wrapper.classes('is-expandable'); + const isCollapsed = () => wrapper.classes('is-collapsed'); + + const findAddIssueButton = () => wrapper.find({ ref: 'newIssueBtn' }); + + describe('Add issue button', () => { + const hasNoAddButton = [ListType.promotion, ListType.blank, ListType.closed]; + const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee]; + + it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => { + createComponent({ listType }); + + expect(findAddIssueButton().exists()).toBe(false); + }); + + it.each(hasAddButton)('does render when List Type is `%s`', listType => { + createComponent({ listType }); + + expect(findAddIssueButton().exists()).toBe(true); + }); + + it('has a test for each list type', () => { + Object.values(ListType).forEach(value => { + expect([...hasAddButton, ...hasNoAddButton]).toContain(value); + }); + }); + + it('does render when logged out', () => { + createComponent(); + + expect(findAddIssueButton().exists()).toBe(true); + }); + }); + + describe('Given different list types', () => { + it('is expandable when List Type is `backlog`', () => { + createComponent({ listType: ListType.backlog }); + + expect(isExpandable()).toBe(true); + }); + }); + + describe('expanding / collapsing the column', () => { + it('does not collapse when clicking the header', () => { + createComponent(); + expect(isCollapsed()).toBe(false); + wrapper.find('.board-header').trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(isCollapsed()).toBe(false); + }); + }); + + it('collapses expanded Column when clicking the collapse icon', () => { + createComponent(); + expect(wrapper.vm.list.isExpanded).toBe(true); + wrapper.find('.board-title-caret').trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(isCollapsed()).toBe(true); + }); + }); + + it('expands collapsed Column when clicking the expand icon', () => { + createComponent({ collapsed: true }); + expect(isCollapsed()).toBe(true); + wrapper.find('.board-title-caret').trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(isCollapsed()).toBe(false); + }); + }); + + it("when logged in it calls list update and doesn't set localStorage", () => { + jest.spyOn(List.prototype, 'update'); + window.gon.current_user_id = 1; + + createComponent({ withLocalStorage: false }); + + wrapper.find('.board-title-caret').trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.list.update).toHaveBeenCalledTimes(1); + expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(null); + }); + }); + + it("when logged out it doesn't call list update and sets localStorage", () => { + jest.spyOn(List.prototype, 'update'); + + createComponent(); + + wrapper.find('.board-title-caret').trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.list.update).toHaveBeenCalledTimes(0); + expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe( + String(wrapper.vm.list.isExpanded), + ); + }); + }); + }); +}); diff --git a/spec/frontend/boards/list_spec.js b/spec/frontend/boards/list_spec.js index c0dd5afe498..b30281f8df5 100644 --- a/spec/frontend/boards/list_spec.js +++ b/spec/frontend/boards/list_spec.js @@ -56,7 +56,7 @@ describe('List model', () => { label: { id: 1, title: 'test', - color: 'red', + color: '#ff0000', text_color: 'white', }, }); @@ -64,8 +64,7 @@ describe('List model', () => { expect(list.id).toBe(listObj.id); expect(list.type).toBe('label'); expect(list.position).toBe(0); - expect(list.label.color).toBe('red'); - expect(list.label.textColor).toBe('white'); + expect(list.label).toEqual(listObj.label); }); }); diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index fa4154676a2..97d49de6f2e 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -15,7 +15,7 @@ export const listObj = { label: { id: 5000, title: 'Test', - color: 'red', + color: '#ff0000', description: 'testing;', textColor: 'white', }, @@ -30,7 +30,7 @@ export const listObjDuplicate = { label: { id: listObj.label.id, title: 'Test', - color: 'red', + color: '#ff0000', description: 'testing;', }, }; diff --git a/spec/frontend/lib/utils/file_upload_spec.js b/spec/frontend/lib/utils/file_upload_spec.js index 1255d6fc14f..1dff5d4f925 100644 --- a/spec/frontend/lib/utils/file_upload_spec.js +++ b/spec/frontend/lib/utils/file_upload_spec.js @@ -1,4 +1,4 @@ -import fileUpload from '~/lib/utils/file_upload'; +import fileUpload, { getFilename } from '~/lib/utils/file_upload'; describe('File upload', () => { beforeEach(() => { @@ -62,3 +62,15 @@ describe('File upload', () => { expect(input.click).not.toHaveBeenCalled(); }); }); + +describe('getFilename', () => { + it('returns first value correctly', () => { + const event = { + clipboardData: { + getData: () => 'test.png\rtest.txt', + }, + }; + + expect(getFilename(event)).toBe('test.png'); + }); +}); diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js index 4c76f9c50fb..9162bee2078 100644 --- a/spec/frontend/notes/components/diff_discussion_header_spec.js +++ b/spec/frontend/notes/components/diff_discussion_header_spec.js @@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils'; import createStore from '~/notes/stores'; import diffDiscussionHeader from '~/notes/components/diff_discussion_header.vue'; -import { discussionMock } from '../../../javascripts/notes/mock_data'; +import { discussionMock } from '../mock_data'; import mockDiffFile from '../../diffs/mock_data/diff_discussions'; const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; diff --git a/spec/frontend/notes/components/diff_with_note_spec.js b/spec/frontend/notes/components/diff_with_note_spec.js new file mode 100644 index 00000000000..d6d42e1988d --- /dev/null +++ b/spec/frontend/notes/components/diff_with_note_spec.js @@ -0,0 +1,86 @@ +import { mount } from '@vue/test-utils'; +import DiffWithNote from '~/notes/components/diff_with_note.vue'; +import { createStore } from '~/mr_notes/stores'; + +const discussionFixture = 'merge_requests/diff_discussion.json'; +const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json'; + +describe('diff_with_note', () => { + let store; + let wrapper; + + const selectors = { + get diffTable() { + return wrapper.find('.diff-content table'); + }, + get diffRows() { + return wrapper.findAll('.diff-content .line_holder'); + }, + get noteRow() { + return wrapper.find('.diff-content .notes_holder'); + }, + }; + + beforeEach(() => { + store = createStore(); + store.replaceState({ + ...store.state, + notes: { + noteableData: { + current_user: {}, + }, + }, + }); + }); + + describe('text diff', () => { + beforeEach(() => { + const diffDiscussion = getJSONFixture(discussionFixture)[0]; + + wrapper = mount(DiffWithNote, { + propsData: { + discussion: diffDiscussion, + }, + store, + }); + }); + + it('removes trailing "+" char', () => { + const richText = wrapper.vm.$el + .querySelectorAll('.line_holder')[4] + .querySelector('.line_content').textContent[0]; + + expect(richText).not.toEqual('+'); + }); + + it('removes trailing "-" char', () => { + const richText = wrapper.vm.$el.querySelector('#LC13').parentNode.textContent[0]; + + expect(richText).not.toEqual('-'); + }); + + it('shows text diff', () => { + expect(wrapper.classes('text-file')).toBe(true); + expect(selectors.diffTable.exists()).toBe(true); + }); + + it('shows diff lines', () => { + expect(selectors.diffRows.length).toBe(12); + }); + + it('shows notes row', () => { + expect(selectors.noteRow.exists()).toBe(true); + }); + }); + + describe('image diff', () => { + beforeEach(() => { + const imageDiscussion = getJSONFixture(imageDiscussionFixture)[0]; + wrapper = mount(DiffWithNote, { propsData: { discussion: imageDiscussion }, store }); + }); + + it('shows image diff', () => { + expect(selectors.diffTable.exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/notes/components/discussion_filter_spec.js b/spec/frontend/notes/components/discussion_filter_spec.js new file mode 100644 index 00000000000..b8d2d721443 --- /dev/null +++ b/spec/frontend/notes/components/discussion_filter_spec.js @@ -0,0 +1,219 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; + +import { createLocalVue, mount } from '@vue/test-utils'; +import AxiosMockAdapter from 'axios-mock-adapter'; + +import axios from '~/lib/utils/axios_utils'; +import notesModule from '~/notes/stores/modules'; +import DiscussionFilter from '~/notes/components/discussion_filter.vue'; +import { DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTER_TYPES } from '~/notes/constants'; + +import { discussionFiltersMock, discussionMock } from '../mock_data'; +import { TEST_HOST } from 'jest/helpers/test_constants'; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +const DISCUSSION_PATH = `${TEST_HOST}/example`; + +describe('DiscussionFilter component', () => { + let wrapper; + let store; + let eventHub; + let mock; + + const filterDiscussion = jest.fn(); + + const mountComponent = () => { + const discussions = [ + { + ...discussionMock, + id: discussionMock.id, + notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }], + }, + ]; + + const defaultStore = { ...notesModule() }; + + store = new Vuex.Store({ + ...defaultStore, + actions: { + ...defaultStore.actions, + filterDiscussion, + }, + }); + + store.state.notesData.discussionsPath = DISCUSSION_PATH; + + store.state.discussions = discussions; + + return mount(DiscussionFilter, { + store, + propsData: { + filters: discussionFiltersMock, + selectedValue: DISCUSSION_FILTERS_DEFAULT_VALUE, + }, + localVue, + }); + }; + + beforeEach(() => { + mock = new AxiosMockAdapter(axios); + + // We are mocking the discussions retrieval, + // as it doesn't matter for our tests here + mock.onGet(DISCUSSION_PATH).reply(200, ''); + window.mrTabs = undefined; + wrapper = mountComponent(); + }); + + afterEach(() => { + wrapper.vm.$destroy(); + mock.restore(); + }); + + it('renders the all filters', () => { + expect(wrapper.findAll('.dropdown-menu li').length).toBe(discussionFiltersMock.length); + }); + + it('renders the default selected item', () => { + expect( + wrapper + .find('#discussion-filter-dropdown') + .text() + .trim(), + ).toBe(discussionFiltersMock[0].title); + }); + + it('updates to the selected item', () => { + const filterItem = wrapper.find( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`, + ); + + filterItem.trigger('click'); + + expect(wrapper.vm.currentFilter.title).toBe(filterItem.text().trim()); + }); + + it('only updates when selected filter changes', () => { + wrapper + .find(`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`) + .trigger('click'); + + expect(filterDiscussion).not.toHaveBeenCalled(); + }); + + it('disables commenting when "Show history only" filter is applied', () => { + const filterItem = wrapper.find( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`, + ); + filterItem.trigger('click'); + + expect(wrapper.vm.$store.state.commentsDisabled).toBe(true); + }); + + it('enables commenting when "Show history only" filter is not applied', () => { + const filterItem = wrapper.find( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`, + ); + filterItem.trigger('click'); + + expect(wrapper.vm.$store.state.commentsDisabled).toBe(false); + }); + + it('renders a dropdown divider for the default filter', () => { + const defaultFilter = wrapper.findAll( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] > *`, + ); + + expect(defaultFilter.at(defaultFilter.length - 1).classes('dropdown-divider')).toBe(true); + }); + + describe('Merge request tabs', () => { + eventHub = new Vue(); + + beforeEach(() => { + window.mrTabs = { + eventHub, + currentTab: 'show', + }; + + wrapper = mountComponent(); + }); + + afterEach(() => { + window.mrTabs = undefined; + }); + + it('only renders when discussion tab is active', done => { + eventHub.$emit('MergeRequestTabChange', 'commit'); + + wrapper.vm.$nextTick(() => { + expect(wrapper.isEmpty()).toBe(true); + done(); + }); + }); + }); + + describe('URL with Links to notes', () => { + afterEach(() => { + window.location.hash = ''; + }); + + it('updates the filter when the URL links to a note', done => { + window.location.hash = `note_${discussionMock.notes[0].id}`; + wrapper.vm.currentValue = discussionFiltersMock[2].value; + wrapper.vm.handleLocationHash(); + + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE); + done(); + }); + }); + + it('does not update the filter when the current filter is "Show all activity"', done => { + window.location.hash = `note_${discussionMock.notes[0].id}`; + wrapper.vm.handleLocationHash(); + + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE); + done(); + }); + }); + + it('only updates filter when the URL links to a note', done => { + window.location.hash = `testing123`; + wrapper.vm.handleLocationHash(); + + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE); + done(); + }); + }); + + it('fetches discussions when there is a hash', done => { + window.location.hash = `note_${discussionMock.notes[0].id}`; + wrapper.vm.currentValue = discussionFiltersMock[2].value; + jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {}); + wrapper.vm.handleLocationHash(); + + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.selectFilter).toHaveBeenCalled(); + done(); + }); + }); + + it('does not fetch discussions when there is no hash', done => { + window.location.hash = ''; + jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {}); + wrapper.vm.handleLocationHash(); + + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.selectFilter).not.toHaveBeenCalled(); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/notes/components/discussion_resolve_with_issue_button_spec.js b/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js index 4348445f7ca..4348445f7ca 100644 --- a/spec/javascripts/notes/components/discussion_resolve_with_issue_button_spec.js +++ b/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js diff --git a/spec/javascripts/notes/components/note_actions/reply_button_spec.js b/spec/frontend/notes/components/note_actions/reply_button_spec.js index 720ab10b270..720ab10b270 100644 --- a/spec/javascripts/notes/components/note_actions/reply_button_spec.js +++ b/spec/frontend/notes/components/note_actions/reply_button_spec.js diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js index 5d13f587ca7..5d13f587ca7 100644 --- a/spec/javascripts/notes/components/note_actions_spec.js +++ b/spec/frontend/notes/components/note_actions_spec.js diff --git a/spec/javascripts/notes/components/note_awards_list_spec.js b/spec/frontend/notes/components/note_awards_list_spec.js index 90aa1684272..822b1f9efce 100644 --- a/spec/javascripts/notes/components/note_awards_list_spec.js +++ b/spec/frontend/notes/components/note_awards_list_spec.js @@ -1,14 +1,24 @@ import Vue from 'vue'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import createStore from '~/notes/stores'; import awardsNote from '~/notes/components/note_awards_list.vue'; import { noteableDataMock, notesDataMock } from '../mock_data'; +import { TEST_HOST } from 'jest/helpers/test_constants'; 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(200, ''); + const Component = Vue.extend(awardsNote); store = createStore(); @@ -32,12 +42,13 @@ describe('note_awards_list component', () => { noteAuthorId: 2, noteId: '545', canAwardEmoji: true, - toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji', + toggleAwardPath, }, }).$mount(); }); afterEach(() => { + mock.restore(); vm.$destroy(); }); @@ -49,8 +60,8 @@ describe('note_awards_list component', () => { }); it('should be possible to remove awarded emoji', () => { - spyOn(vm, 'handleAward').and.callThrough(); - spyOn(vm, 'toggleAwardRequest').and.callThrough(); + jest.spyOn(vm, 'handleAward'); + jest.spyOn(vm, 'toggleAwardRequest'); vm.$el.querySelector('.js-awards-block button').click(); expect(vm.handleAward).toHaveBeenCalledWith('flag_tz'); @@ -138,7 +149,7 @@ describe('note_awards_list component', () => { }); it('should not be possible to remove awarded emoji', () => { - spyOn(vm, 'toggleAwardRequest').and.callThrough(); + jest.spyOn(vm, 'toggleAwardRequest'); vm.$el.querySelector('.js-awards-block button').click(); diff --git a/spec/javascripts/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js index efad0785afe..efad0785afe 100644 --- a/spec/javascripts/notes/components/note_body_spec.js +++ b/spec/frontend/notes/components/note_body_spec.js diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js index 8ab8bce9027..bccac03126c 100644 --- a/spec/javascripts/notes/components/note_form_spec.js +++ b/spec/frontend/notes/components/note_form_spec.js @@ -4,6 +4,10 @@ import NoteForm from '~/notes/components/note_form.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import { noteableDataMock, notesDataMock } from '../mock_data'; +import { getDraft, updateDraft } from '~/lib/utils/autosave'; + +jest.mock('~/lib/utils/autosave'); + describe('issue_note_form component', () => { const dummyAutosaveKey = 'some-autosave-key'; const dummyDraft = 'dummy draft content'; @@ -23,7 +27,7 @@ describe('issue_note_form component', () => { }; beforeEach(() => { - spyOnDependency(NoteForm, 'getDraft').and.callFake(key => { + getDraft.mockImplementation(key => { if (key === dummyAutosaveKey) { return dummyDraft; } @@ -55,19 +59,15 @@ describe('issue_note_form component', () => { expect(wrapper.vm.noteHash).toBe(`#note_${props.noteId}`); }); - it('return note hash as `#` when `noteId` is empty', done => { + it('return note hash as `#` when `noteId` is empty', () => { wrapper.setProps({ ...props, noteId: '', }); - wrapper.vm - .$nextTick() - .then(() => { - expect(wrapper.vm.noteHash).toBe('#'); - }) - .then(done) - .catch(done.fail); + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.noteHash).toBe('#'); + }); }); }); @@ -76,7 +76,7 @@ describe('issue_note_form component', () => { wrapper = createComponentWrapper(); }); - it('should show conflict message if note changes outside the component', done => { + it('should show conflict message if note changes outside the component', () => { wrapper.setProps({ ...props, isEditing: true, @@ -86,21 +86,17 @@ describe('issue_note_form component', () => { const message = 'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.'; - wrapper.vm - .$nextTick() - .then(() => { - const conflictWarning = wrapper.find('.js-conflict-edit-warning'); - - expect(conflictWarning.exists()).toBe(true); - expect( - conflictWarning - .text() - .replace(/\s+/g, ' ') - .trim(), - ).toBe(message); - }) - .then(done) - .catch(done.fail); + return wrapper.vm.$nextTick().then(() => { + const conflictWarning = wrapper.find('.js-conflict-edit-warning'); + + expect(conflictWarning.exists()).toBe(true); + expect( + conflictWarning + .text() + .replace(/\s+/g, ' ') + .trim(), + ).toBe(message); + }); }); }); @@ -136,7 +132,7 @@ describe('issue_note_form component', () => { describe('up', () => { it('should ender edit mode', () => { // TODO: do not spy on vm - spyOn(wrapper.vm, 'editMyLastNote').and.callThrough(); + jest.spyOn(wrapper.vm, 'editMyLastNote'); textarea.trigger('keydown.up'); @@ -164,61 +160,50 @@ describe('issue_note_form component', () => { }); describe('actions', () => { - it('should be possible to cancel', done => { + it('should be possible to cancel', () => { // TODO: do not spy on vm - spyOn(wrapper.vm, 'cancelHandler').and.callThrough(); + jest.spyOn(wrapper.vm, 'cancelHandler'); wrapper.setProps({ ...props, isEditing: true, }); - wrapper.vm - .$nextTick() - .then(() => { - const cancelButton = wrapper.find('.note-edit-cancel'); - cancelButton.trigger('click'); + return wrapper.vm.$nextTick().then(() => { + const cancelButton = wrapper.find('.note-edit-cancel'); + cancelButton.trigger('click'); - expect(wrapper.vm.cancelHandler).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); + expect(wrapper.vm.cancelHandler).toHaveBeenCalled(); + }); }); - it('should be possible to update the note', done => { + it('should be possible to update the note', () => { wrapper.setProps({ ...props, isEditing: true, }); - wrapper.vm - .$nextTick() - .then(() => { - const textarea = wrapper.find('textarea'); - textarea.setValue('Foo'); - const saveButton = wrapper.find('.js-vue-issue-save'); - saveButton.trigger('click'); - - expect(wrapper.vm.isSubmitting).toEqual(true); - }) - .then(done) - .catch(done.fail); + return wrapper.vm.$nextTick().then(() => { + const textarea = wrapper.find('textarea'); + textarea.setValue('Foo'); + const saveButton = wrapper.find('.js-vue-issue-save'); + saveButton.trigger('click'); + + expect(wrapper.vm.isSubmitting).toBe(true); + }); }); }); }); describe('with autosaveKey', () => { describe('with draft', () => { - beforeEach(done => { + beforeEach(() => { Object.assign(props, { noteBody: '', autosaveKey: dummyAutosaveKey, }); wrapper = createComponentWrapper(); - wrapper.vm - .$nextTick() - .then(done) - .catch(done.fail); + return wrapper.vm.$nextTick(); }); it('displays the draft in textarea', () => { @@ -229,17 +214,14 @@ describe('issue_note_form component', () => { }); describe('without draft', () => { - beforeEach(done => { + beforeEach(() => { Object.assign(props, { noteBody: '', autosaveKey: 'some key without draft', }); wrapper = createComponentWrapper(); - wrapper.vm - .$nextTick() - .then(done) - .catch(done.fail); + return wrapper.vm.$nextTick(); }); it('leaves the textarea empty', () => { @@ -250,7 +232,6 @@ describe('issue_note_form component', () => { }); it('updates the draft if textarea content changes', () => { - const updateDraftSpy = spyOnDependency(NoteForm, 'updateDraft').and.stub(); Object.assign(props, { noteBody: '', autosaveKey: dummyAutosaveKey, @@ -261,7 +242,7 @@ describe('issue_note_form component', () => { textarea.setValue(dummyContent); - expect(updateDraftSpy).toHaveBeenCalledWith(dummyAutosaveKey, dummyContent); + expect(updateDraft).toHaveBeenCalledWith(dummyAutosaveKey, dummyContent); }); }); }); diff --git a/spec/javascripts/notes/components/note_signed_out_widget_spec.js b/spec/frontend/notes/components/note_signed_out_widget_spec.js index e217a2caa73..e217a2caa73 100644 --- a/spec/javascripts/notes/components/note_signed_out_widget_spec.js +++ b/spec/frontend/notes/components/note_signed_out_widget_spec.js diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js index ee84fd2b091..b91f599f158 100644 --- a/spec/javascripts/notes/components/noteable_discussion_spec.js +++ b/spec/frontend/notes/components/noteable_discussion_spec.js @@ -12,8 +12,8 @@ import { loggedOutnoteableData, userDataMock, } from '../mock_data'; -import mockDiffFile from '../../diffs/mock_data/diff_file'; -import { trimText } from '../../helpers/text_helper'; +import mockDiffFile from 'jest/diffs/mock_data/diff_file'; +import { trimText } from 'helpers/text_helper'; const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; @@ -47,27 +47,24 @@ describe('noteable_discussion component', () => { expect(wrapper.find('.discussion-header').exists()).toBe(false); }); - it('should render thread header', done => { + it('should render thread header', () => { const discussion = { ...discussionMock }; discussion.diff_file = mockDiffFile; discussion.diff_discussion = true; + discussion.expanded = false; wrapper.setProps({ discussion }); - wrapper.vm - .$nextTick() - .then(() => { - expect(wrapper.find('.discussion-header').exists()).toBe(true); - }) - .then(done) - .catch(done.fail); + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.find('.discussion-header').exists()).toBe(true); + }); }); describe('actions', () => { - it('should toggle reply form', done => { + it('should toggle reply form', () => { const replyPlaceholder = wrapper.find(ReplyPlaceholder); - wrapper.vm + return wrapper.vm .$nextTick() .then(() => { expect(wrapper.vm.isReplying).toEqual(false); @@ -89,9 +86,7 @@ describe('noteable_discussion component', () => { expect(noteFormProps.line).toBe(null); expect(noteFormProps.saveButtonTitle).toBe('Comment'); expect(noteFormProps.autosaveKey).toBe(`Note/Issue/${discussionMock.id}/Reply`); - }) - .then(done) - .catch(done.fail); + }); }); it('does not render jump to thread button', () => { @@ -115,7 +110,7 @@ describe('noteable_discussion component', () => { }); describe('for unresolved thread', () => { - beforeEach(done => { + beforeEach(() => { const discussion = { ...getJSONFixture(discussionWithTwoUnresolvedNotes)[0], expanded: true, @@ -131,10 +126,7 @@ describe('noteable_discussion component', () => { wrapper.setProps({ discussion }); - wrapper.vm - .$nextTick() - .then(done) - .catch(done.fail); + return wrapper.vm.$nextTick(); }); it('displays a button to resolve with issue', () => { diff --git a/spec/javascripts/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js index 1906dae7800..0d67b1d87a9 100644 --- a/spec/javascripts/notes/components/noteable_note_spec.js +++ b/spec/frontend/notes/components/noteable_note_spec.js @@ -86,7 +86,7 @@ describe('issue_note', () => { it('prevents note preview xss', done => { const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`; - const alertSpy = spyOn(window, 'alert'); + const alertSpy = jest.spyOn(window, 'alert'); store.hotUpdate({ actions: { updateNote() {}, @@ -96,11 +96,11 @@ describe('issue_note', () => { noteBodyComponent.vm.$emit('handleFormUpdate', noteBody, null, () => {}); - setTimeout(() => { + setImmediate(() => { expect(alertSpy).not.toHaveBeenCalled(); expect(wrapper.vm.note.note_html).toEqual(escape(noteBody)); done(); - }, 0); + }); }); describe('cancel edit', () => { diff --git a/spec/javascripts/notes/components/toggle_replies_widget_spec.js b/spec/frontend/notes/components/toggle_replies_widget_spec.js index 8485ec0262f..b4f68b039cf 100644 --- a/spec/javascripts/notes/components/toggle_replies_widget_spec.js +++ b/spec/frontend/notes/components/toggle_replies_widget_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; import toggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue'; import { note } from '../mock_data'; @@ -44,7 +44,7 @@ describe('toggle replies widget for notes', () => { }); it('should emit toggle event when the replies text clicked', () => { - const spy = spyOn(vm, '$emit'); + const spy = jest.spyOn(vm, '$emit'); vm.$el.querySelector('.js-replies-text').click(); @@ -68,7 +68,7 @@ describe('toggle replies widget for notes', () => { }); it('should emit toggle event when the collapse replies text called', () => { - const spy = spyOn(vm, '$emit'); + const spy = jest.spyOn(vm, '$emit'); vm.$el.querySelector('.js-collapse-replies').click(); diff --git a/spec/javascripts/notes/stores/collapse_utils_spec.js b/spec/frontend/notes/stores/collapse_utils_spec.js index d3019f4b9a4..d3019f4b9a4 100644 --- a/spec/javascripts/notes/stores/collapse_utils_spec.js +++ b/spec/frontend/notes/stores/collapse_utils_spec.js diff --git a/spec/helpers/container_expiration_policies_helper_spec.rb b/spec/helpers/container_expiration_policies_helper_spec.rb index f7e851fb012..6dcbadd89cb 100644 --- a/spec/helpers/container_expiration_policies_helper_spec.rb +++ b/spec/helpers/container_expiration_policies_helper_spec.rb @@ -37,8 +37,8 @@ describe ContainerExpirationPoliciesHelper do expected_result = [ { key: '7d', label: '7 days until tags are automatically removed' }, { key: '14d', label: '14 days until tags are automatically removed' }, - { key: '30d', label: '30 days until tags are automatically removed', default: true }, - { key: '90d', label: '90 days until tags are automatically removed' } + { key: '30d', label: '30 days until tags are automatically removed' }, + { key: '90d', label: '90 days until tags are automatically removed', default: true } ] expect(helper.older_than_options).to eq(expected_result) diff --git a/spec/javascripts/helpers/init_vue_mr_page_helper.js b/spec/javascripts/helpers/init_vue_mr_page_helper.js index 3fa29cb9136..04f969fcd2d 100644 --- a/spec/javascripts/helpers/init_vue_mr_page_helper.js +++ b/spec/javascripts/helpers/init_vue_mr_page_helper.js @@ -1,7 +1,7 @@ import MockAdapter from 'axios-mock-adapter'; import initMRPage from '~/mr_notes/index'; import axios from '~/lib/utils/axios_utils'; -import { userDataMock, notesDataMock, noteableDataMock } from '../notes/mock_data'; +import { userDataMock, notesDataMock, noteableDataMock } from '../../frontend/notes/mock_data'; import diffFileMockData from '../diffs/mock_data/diff_file'; export default function initVueMRPage() { diff --git a/spec/javascripts/notes/components/diff_with_note_spec.js b/spec/javascripts/notes/components/diff_with_note_spec.js deleted file mode 100644 index 573aac2c3e0..00000000000 --- a/spec/javascripts/notes/components/diff_with_note_spec.js +++ /dev/null @@ -1,89 +0,0 @@ -import Vue from 'vue'; -import { mountComponentWithStore } from 'spec/helpers'; -import DiffWithNote from '~/notes/components/diff_with_note.vue'; -import { createStore } from '~/mr_notes/stores'; - -const discussionFixture = 'merge_requests/diff_discussion.json'; -const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json'; - -describe('diff_with_note', () => { - let store; - let vm; - const diffDiscussionMock = getJSONFixture(discussionFixture)[0]; - const diffDiscussion = diffDiscussionMock; - const Component = Vue.extend(DiffWithNote); - const props = { - discussion: diffDiscussion, - }; - const selectors = { - get container() { - return vm.$el; - }, - get diffTable() { - return this.container.querySelector('.diff-content table'); - }, - get diffRows() { - return this.container.querySelectorAll('.diff-content .line_holder'); - }, - get noteRow() { - return this.container.querySelector('.diff-content .notes_holder'); - }, - }; - - beforeEach(() => { - store = createStore(); - store.replaceState({ - ...store.state, - notes: { - noteableData: { - current_user: {}, - }, - }, - }); - }); - - describe('text diff', () => { - beforeEach(() => { - vm = mountComponentWithStore(Component, { props, store }); - }); - - it('removes trailing "+" char', () => { - const richText = vm.$el.querySelectorAll('.line_holder')[4].querySelector('.line_content') - .textContent[0]; - - expect(richText).not.toEqual('+'); - }); - - it('removes trailing "-" char', () => { - const richText = vm.$el.querySelector('#LC13').parentNode.textContent[0]; - - expect(richText).not.toEqual('-'); - }); - - it('shows text diff', () => { - expect(selectors.container).toHaveClass('text-file'); - expect(selectors.diffTable).toExist(); - }); - - it('shows diff lines', () => { - expect(selectors.diffRows.length).toBe(12); - }); - - it('shows notes row', () => { - expect(selectors.noteRow).toExist(); - }); - }); - - describe('image diff', () => { - beforeEach(() => { - const imageDiffDiscussionMock = getJSONFixture(imageDiscussionFixture)[0]; - props.discussion = imageDiffDiscussionMock; - }); - - it('shows image diff', () => { - vm = mountComponentWithStore(Component, { props, store }); - - expect(selectors.diffTable).not.toExist(); - }); - }); -}); diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js deleted file mode 100644 index 7524de36ac5..00000000000 --- a/spec/javascripts/notes/components/discussion_filter_spec.js +++ /dev/null @@ -1,187 +0,0 @@ -import Vue from 'vue'; -import createStore from '~/notes/stores'; -import DiscussionFilter from '~/notes/components/discussion_filter.vue'; -import { DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTER_TYPES } from '~/notes/constants'; -import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper'; -import { discussionFiltersMock, discussionMock } from '../mock_data'; - -describe('DiscussionFilter component', () => { - let vm; - let store; - let eventHub; - - const mountComponent = () => { - store = createStore(); - - const discussions = [ - { - ...discussionMock, - id: discussionMock.id, - notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }], - }, - ]; - const Component = Vue.extend(DiscussionFilter); - const selectedValue = DISCUSSION_FILTERS_DEFAULT_VALUE; - const props = { filters: discussionFiltersMock, selectedValue }; - - store.state.discussions = discussions; - return mountComponentWithStore(Component, { - el: null, - store, - props, - }); - }; - - beforeEach(() => { - window.mrTabs = undefined; - vm = mountComponent(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('renders the all filters', () => { - expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual( - discussionFiltersMock.length, - ); - }); - - it('renders the default selected item', () => { - expect(vm.$el.querySelector('#discussion-filter-dropdown').textContent.trim()).toEqual( - discussionFiltersMock[0].title, - ); - }); - - it('updates to the selected item', () => { - const filterItem = vm.$el.querySelector( - `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`, - ); - filterItem.click(); - - expect(vm.currentFilter.title).toEqual(filterItem.textContent.trim()); - }); - - it('only updates when selected filter changes', () => { - const filterItem = vm.$el.querySelector( - `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`, - ); - - spyOn(vm, 'filterDiscussion'); - filterItem.click(); - - expect(vm.filterDiscussion).not.toHaveBeenCalled(); - }); - - it('disables commenting when "Show history only" filter is applied', () => { - const filterItem = vm.$el.querySelector( - `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`, - ); - filterItem.click(); - - expect(vm.$store.state.commentsDisabled).toBe(true); - }); - - it('enables commenting when "Show history only" filter is not applied', () => { - const filterItem = vm.$el.querySelector( - `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`, - ); - filterItem.click(); - - expect(vm.$store.state.commentsDisabled).toBe(false); - }); - - it('renders a dropdown divider for the default filter', () => { - const defaultFilter = vm.$el.querySelector( - `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"]`, - ); - - expect(defaultFilter.lastChild.classList).toContain('dropdown-divider'); - }); - - describe('Merge request tabs', () => { - eventHub = new Vue(); - - beforeEach(() => { - window.mrTabs = { - eventHub, - currentTab: 'show', - }; - - vm = mountComponent(); - }); - - afterEach(() => { - window.mrTabs = undefined; - }); - - it('only renders when discussion tab is active', done => { - eventHub.$emit('MergeRequestTabChange', 'commit'); - - vm.$nextTick(() => { - expect(vm.$el.querySelector).toBeUndefined(); - done(); - }); - }); - }); - - describe('URL with Links to notes', () => { - afterEach(() => { - window.location.hash = ''; - }); - - it('updates the filter when the URL links to a note', done => { - window.location.hash = `note_${discussionMock.notes[0].id}`; - vm.currentValue = discussionFiltersMock[2].value; - vm.handleLocationHash(); - - vm.$nextTick(() => { - expect(vm.currentValue).toEqual(DISCUSSION_FILTERS_DEFAULT_VALUE); - done(); - }); - }); - - it('does not update the filter when the current filter is "Show all activity"', done => { - window.location.hash = `note_${discussionMock.notes[0].id}`; - vm.handleLocationHash(); - - vm.$nextTick(() => { - expect(vm.currentValue).toEqual(DISCUSSION_FILTERS_DEFAULT_VALUE); - done(); - }); - }); - - it('only updates filter when the URL links to a note', done => { - window.location.hash = `testing123`; - vm.handleLocationHash(); - - vm.$nextTick(() => { - expect(vm.currentValue).toEqual(DISCUSSION_FILTERS_DEFAULT_VALUE); - done(); - }); - }); - - it('fetches discussions when there is a hash', done => { - window.location.hash = `note_${discussionMock.notes[0].id}`; - vm.currentValue = discussionFiltersMock[2].value; - spyOn(vm, 'selectFilter'); - vm.handleLocationHash(); - - vm.$nextTick(() => { - expect(vm.selectFilter).toHaveBeenCalled(); - done(); - }); - }); - - it('does not fetch discussions when there is no hash', done => { - window.location.hash = ''; - spyOn(vm, 'selectFilter'); - vm.handleLocationHash(); - - vm.$nextTick(() => { - expect(vm.selectFilter).not.toHaveBeenCalled(); - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/notes/helpers.js b/spec/javascripts/notes/helpers.js deleted file mode 100644 index 7bcba609311..00000000000 --- a/spec/javascripts/notes/helpers.js +++ /dev/null @@ -1 +0,0 @@ -export * from '../../frontend/notes/helpers.js'; diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js deleted file mode 100644 index 89e4553092a..00000000000 --- a/spec/javascripts/notes/mock_data.js +++ /dev/null @@ -1 +0,0 @@ -export * from '../../frontend/notes/mock_data.js'; diff --git a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb index 59639409183..5f80ef9538a 100644 --- a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb @@ -9,7 +9,14 @@ describe Gitlab::SidekiqMiddleware::ClientMetrics do let(:queue) { :test } let(:worker_class) { worker.class } let(:job) { {} } - let(:default_labels) { { queue: queue.to_s, boundary: "", external_dependencies: "no", feature_category: "", urgency: "low" } } + let(:default_labels) do + { queue: queue.to_s, + worker: worker_class.to_s, + boundary: "", + external_dependencies: "no", + feature_category: "", + urgency: "low" } + end shared_examples "a metrics client middleware" do context "with mocked prometheus" do diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb index 3343587beff..3214bd758e7 100644 --- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb @@ -11,7 +11,14 @@ describe Gitlab::SidekiqMiddleware::ServerMetrics do let(:job) { {} } let(:job_status) { :done } let(:labels_with_job_status) { labels.merge(job_status: job_status.to_s) } - let(:default_labels) { { queue: queue.to_s, boundary: "", external_dependencies: "no", feature_category: "", urgency: "low" } } + let(:default_labels) do + { queue: queue.to_s, + worker: worker_class.to_s, + boundary: "", + external_dependencies: "no", + feature_category: "", + urgency: "low" } + end shared_examples "a metrics middleware" do context "with mocked prometheus" do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 425194ba0e3..844e50dbb58 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1924,7 +1924,7 @@ describe Ci::Pipeline, :mailer do describe '#update_status' do context 'when pipeline is empty' do it 'updates does not change pipeline status' do - expect(pipeline.statuses.latest.slow_composite_status).to be_nil + expect(pipeline.statuses.latest.slow_composite_status(project: project)).to be_nil expect { pipeline.update_legacy_status } .to change { pipeline.reload.status } diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 40d9afcdd14..73b81b2225a 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -423,7 +423,7 @@ describe CommitStatus do end it 'returns a correct compound status' do - expect(described_class.all.slow_composite_status).to eq 'running' + expect(described_class.all.slow_composite_status(project: project)).to eq 'running' end end @@ -433,7 +433,7 @@ describe CommitStatus do end it 'returns status that indicates success' do - expect(described_class.all.slow_composite_status).to eq 'success' + expect(described_class.all.slow_composite_status(project: project)).to eq 'success' end end @@ -444,7 +444,7 @@ describe CommitStatus do end it 'returns status according to the scope' do - expect(described_class.latest.slow_composite_status).to eq 'success' + expect(described_class.latest.slow_composite_status(project: project)).to eq 'success' end end end diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index 99d09af80d0..68047f24ec3 100644 --- a/spec/models/concerns/has_status_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -6,7 +6,7 @@ describe HasStatus do describe '.slow_composite_status' do using RSpec::Parameterized::TableSyntax - subject { CommitStatus.slow_composite_status } + subject { CommitStatus.slow_composite_status(project: nil) } shared_examples 'build status summary' do context 'all successful' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 61c871ead92..291c628bfde 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -4400,6 +4400,12 @@ describe User, :do_not_mock_admin_mode do it { is_expected.to be expected_result } end + + context 'when email is of Gitlab and is not confirmed' do + let(:user) { build(:user, email: 'test@gitlab.com', confirmed_at: nil) } + + it { is_expected.to be false } + end end describe '#current_highest_access_level' do |