diff options
Diffstat (limited to 'spec/javascripts')
29 files changed, 1192 insertions, 204 deletions
diff --git a/spec/javascripts/autosave_spec.js b/spec/javascripts/autosave_spec.js index 38ae5b7e00c..dcb1c781591 100644 --- a/spec/javascripts/autosave_spec.js +++ b/spec/javascripts/autosave_spec.js @@ -59,12 +59,10 @@ describe('Autosave', () => { Autosave.prototype.restore.call(autosave); - expect( - field.trigger, - ).toHaveBeenCalled(); + expect(field.trigger).toHaveBeenCalled(); }); - it('triggers native event', (done) => { + it('triggers native event', done => { autosave.field.get(0).addEventListener('change', () => { done(); }); @@ -81,9 +79,7 @@ describe('Autosave', () => { it('does not trigger event', () => { spyOn(field, 'trigger').and.callThrough(); - expect( - field.trigger, - ).not.toHaveBeenCalled(); + expect(field.trigger).not.toHaveBeenCalled(); }); }); }); diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index 7a32e84bced..b6c61e7bad7 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -69,109 +69,100 @@ describe('Issue card component', () => { }); it('renders issue title', () => { - expect( - component.$el.querySelector('.board-card-title').textContent, - ).toContain(issue.title); + expect(component.$el.querySelector('.board-card-title').textContent).toContain(issue.title); }); it('includes issue base in link', () => { - expect( - component.$el.querySelector('.board-card-title a').getAttribute('href'), - ).toContain('/test'); + expect(component.$el.querySelector('.board-card-title a').getAttribute('href')).toContain( + '/test', + ); }); it('includes issue title on link', () => { - expect( - component.$el.querySelector('.board-card-title a').getAttribute('title'), - ).toBe(issue.title); + expect(component.$el.querySelector('.board-card-title a').getAttribute('title')).toBe( + issue.title, + ); }); it('does not render confidential icon', () => { - expect( - component.$el.querySelector('.fa-eye-flash'), - ).toBeNull(); + expect(component.$el.querySelector('.fa-eye-flash')).toBeNull(); }); - it('renders confidential icon', (done) => { + it('renders confidential icon', done => { component.issue.confidential = true; Vue.nextTick(() => { - expect( - component.$el.querySelector('.confidential-icon'), - ).not.toBeNull(); + expect(component.$el.querySelector('.confidential-icon')).not.toBeNull(); done(); }); }); it('renders issue ID with #', () => { - expect( - component.$el.querySelector('.board-card-number').textContent, - ).toContain(`#${issue.id}`); + expect(component.$el.querySelector('.board-card-number').textContent).toContain(`#${issue.id}`); }); describe('assignee', () => { it('does not render assignee', () => { - expect( - component.$el.querySelector('.board-card-assignee .avatar'), - ).toBeNull(); + expect(component.$el.querySelector('.board-card-assignee .avatar')).toBeNull(); }); describe('exists', () => { - beforeEach((done) => { + beforeEach(done => { component.issue.assignees = [user]; Vue.nextTick(() => done()); }); it('renders assignee', () => { - expect( - component.$el.querySelector('.board-card-assignee .avatar'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-card-assignee .avatar')).not.toBeNull(); }); it('sets title', () => { expect( - component.$el.querySelector('.board-card-assignee img').getAttribute('data-original-title'), + component.$el + .querySelector('.board-card-assignee img') + .getAttribute('data-original-title'), ).toContain(`Assigned to ${user.name}`); }); it('sets users path', () => { - expect( - component.$el.querySelector('.board-card-assignee a').getAttribute('href'), - ).toBe('/test'); + expect(component.$el.querySelector('.board-card-assignee a').getAttribute('href')).toBe( + '/test', + ); }); it('renders avatar', () => { - expect( - component.$el.querySelector('.board-card-assignee img'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-card-assignee img')).not.toBeNull(); }); }); describe('assignee default avatar', () => { - beforeEach((done) => { - component.issue.assignees = [new ListAssignee({ - id: 1, - name: 'testing 123', - username: 'test', - }, 'default_avatar')]; + beforeEach(done => { + component.issue.assignees = [ + new ListAssignee( + { + id: 1, + name: 'testing 123', + username: 'test', + }, + 'default_avatar', + ), + ]; Vue.nextTick(done); }); it('displays defaults avatar if users avatar is null', () => { - expect( - component.$el.querySelector('.board-card-assignee img'), - ).not.toBeNull(); - expect( - component.$el.querySelector('.board-card-assignee img').getAttribute('src'), - ).toBe('default_avatar'); + expect(component.$el.querySelector('.board-card-assignee img')).not.toBeNull(); + expect(component.$el.querySelector('.board-card-assignee img').getAttribute('src')).toBe( + 'default_avatar?width=20', + ); }); }); }); describe('multiple assignees', () => { - beforeEach((done) => { + beforeEach(done => { component.issue.assignees = [ user, new ListAssignee({ @@ -191,7 +182,8 @@ describe('Issue card component', () => { name: 'user4', username: 'user4', avatar: 'test_image', - })]; + }), + ]; Vue.nextTick(() => done()); }); @@ -201,26 +193,30 @@ describe('Issue card component', () => { }); describe('more than four assignees', () => { - beforeEach((done) => { - component.issue.assignees.push(new ListAssignee({ - id: 5, - name: 'user5', - username: 'user5', - avatar: 'test_image', - })); + beforeEach(done => { + component.issue.assignees.push( + new ListAssignee({ + id: 5, + name: 'user5', + username: 'user5', + avatar: 'test_image', + }), + ); Vue.nextTick(() => done()); }); it('renders more avatar counter', () => { - expect(component.$el.querySelector('.board-card-assignee .avatar-counter').innerText).toEqual('+2'); + expect( + component.$el.querySelector('.board-card-assignee .avatar-counter').innerText, + ).toEqual('+2'); }); it('renders three assignees', () => { expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3); }); - it('renders 99+ avatar counter', (done) => { + it('renders 99+ avatar counter', done => { for (let i = 5; i < 104; i += 1) { const u = new ListAssignee({ id: i, @@ -232,7 +228,9 @@ describe('Issue card component', () => { } Vue.nextTick(() => { - expect(component.$el.querySelector('.board-card-assignee .avatar-counter').innerText).toEqual('99+'); + expect( + component.$el.querySelector('.board-card-assignee .avatar-counter').innerText, + ).toEqual('99+'); done(); }); }); @@ -240,59 +238,51 @@ describe('Issue card component', () => { }); describe('labels', () => { - beforeEach((done) => { + beforeEach(done => { component.issue.addLabel(label1); Vue.nextTick(() => done()); }); it('renders list label', () => { - expect( - component.$el.querySelectorAll('.badge').length, - ).toBe(2); + expect(component.$el.querySelectorAll('.badge').length).toBe(2); }); it('renders label', () => { const nodes = []; - component.$el.querySelectorAll('.badge').forEach((label) => { + component.$el.querySelectorAll('.badge').forEach(label => { nodes.push(label.getAttribute('data-original-title')); }); - expect( - nodes.includes(label1.description), - ).toBe(true); + expect(nodes.includes(label1.description)).toBe(true); }); it('sets label description as title', () => { - expect( - component.$el.querySelector('.badge').getAttribute('data-original-title'), - ).toContain(label1.description); + expect(component.$el.querySelector('.badge').getAttribute('data-original-title')).toContain( + label1.description, + ); }); it('sets background color of button', () => { const nodes = []; - component.$el.querySelectorAll('.badge').forEach((label) => { + component.$el.querySelectorAll('.badge').forEach(label => { nodes.push(label.style.backgroundColor); }); - expect( - nodes.includes(label1.color), - ).toBe(true); + expect(nodes.includes(label1.color)).toBe(true); }); - it('does not render label if label does not have an ID', (done) => { - component.issue.addLabel(new ListLabel({ - title: 'closed', - })); + it('does not render label if label does not have an ID', done => { + component.issue.addLabel( + new ListLabel({ + title: 'closed', + }), + ); Vue.nextTick() .then(() => { - expect( - component.$el.querySelectorAll('.badge').length, - ).toBe(2); - expect( - component.$el.textContent, - ).not.toContain('closed'); + expect(component.$el.querySelectorAll('.badge').length).toBe(2); + expect(component.$el.textContent).not.toContain('closed'); done(); }) diff --git a/spec/javascripts/diffs/components/diff_line_note_form_spec.js b/spec/javascripts/diffs/components/diff_line_note_form_spec.js index 4600aaea70b..6fe5fdaf7f9 100644 --- a/spec/javascripts/diffs/components/diff_line_note_form_spec.js +++ b/spec/javascripts/diffs/components/diff_line_note_form_spec.js @@ -3,6 +3,7 @@ import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue'; import store from '~/mr_notes/stores'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import diffFileMockData from '../mock_data/diff_file'; +import { noteableDataMock } from '../../notes/mock_data'; describe('DiffLineNoteForm', () => { let component; @@ -21,10 +22,9 @@ describe('DiffLineNoteForm', () => { noteTargetLine: diffLines[0], }); - Object.defineProperty(component, 'isLoggedIn', { - get() { - return true; - }, + Object.defineProperties(component, { + noteableData: { value: noteableDataMock }, + isLoggedIn: { value: true }, }); component.$mount(); @@ -32,12 +32,37 @@ describe('DiffLineNoteForm', () => { describe('methods', () => { describe('handleCancelCommentForm', () => { - it('should call cancelCommentForm with lineCode', () => { + it('should ask for confirmation when shouldConfirm and isDirty passed as truthy', () => { + spyOn(window, 'confirm').and.returnValue(false); + + component.handleCancelCommentForm(true, true); + expect(window.confirm).toHaveBeenCalled(); + }); + + it('should ask for confirmation when one of the params false', () => { + spyOn(window, 'confirm').and.returnValue(false); + + component.handleCancelCommentForm(true, false); + expect(window.confirm).not.toHaveBeenCalled(); + + component.handleCancelCommentForm(false, true); + expect(window.confirm).not.toHaveBeenCalled(); + }); + + it('should call cancelCommentForm with lineCode', done => { + spyOn(window, 'confirm'); spyOn(component, 'cancelCommentForm'); + spyOn(component, 'resetAutoSave'); component.handleCancelCommentForm(); - expect(component.cancelCommentForm).toHaveBeenCalledWith({ - lineCode: diffLines[0].lineCode, + expect(window.confirm).not.toHaveBeenCalled(); + component.$nextTick(() => { + expect(component.cancelCommentForm).toHaveBeenCalledWith({ + lineCode: diffLines[0].lineCode, + }); + expect(component.resetAutoSave).toHaveBeenCalled(); + + done(); }); }); }); @@ -66,7 +91,7 @@ describe('DiffLineNoteForm', () => { describe('mounted', () => { it('should init autosave', () => { - const key = 'autosave/Note/issue///DiffNote//1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1'; + const key = 'autosave/Note/Issue/98//DiffNote//1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1'; expect(component.autosave).toBeDefined(); expect(component.autosave.key).toEqual(key); diff --git a/spec/javascripts/fixtures/search_autocomplete.html.haml b/spec/javascripts/fixtures/search_autocomplete.html.haml index 0421ed2182f..4aa54da9411 100644 --- a/spec/javascripts/fixtures/search_autocomplete.html.haml +++ b/spec/javascripts/fixtures/search_autocomplete.html.haml @@ -1,8 +1,6 @@ -.search.search-form.has-location-badge - %form.navbar-form +.search.search-form + %form.form-inline .search-input-container - %div.location-badge - This project .search-input-wrap .dropdown %input#search.search-input.dropdown-menu-toggle diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js index 092c405a70b..8a8cbd2cee4 100644 --- a/spec/javascripts/ide/components/new_dropdown/index_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js @@ -62,7 +62,9 @@ describe('new dropdown component', () => { vm.dropdownOpen = true; setTimeout(() => { - expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalled(); + expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({ + block: 'nearest', + }); done(); }); @@ -73,7 +75,7 @@ describe('new dropdown component', () => { it('calls delete action', () => { spyOn(vm, 'deleteEntry'); - vm.$el.querySelectorAll('.dropdown-menu button')[3].click(); + vm.$el.querySelectorAll('.dropdown-menu button')[4].click(); expect(vm.deleteEntry).toHaveBeenCalledWith(''); }); diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js index 70651535e87..595a2f927e9 100644 --- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js @@ -15,7 +15,7 @@ describe('new file modal component', () => { describe(type, () => { beforeEach(() => { const store = createStore(); - store.state.newEntryModal = { + store.state.entryModal = { type, path: '', }; @@ -45,7 +45,7 @@ describe('new file modal component', () => { it('$emits create', () => { spyOn(vm, 'createTempEntry'); - vm.createEntryInStore(); + vm.submitForm(); expect(vm.createTempEntry).toHaveBeenCalledWith({ name: 'testing', @@ -55,4 +55,47 @@ describe('new file modal component', () => { }); }); }); + + describe('rename entry', () => { + beforeEach(() => { + const store = createStore(); + store.state.entryModal = { + type: 'rename', + path: '', + entry: { + name: 'test', + type: 'blob', + }, + }; + + vm = createComponentWithStore(Component, store).$mount(); + }); + + ['tree', 'blob'].forEach(type => { + it(`renders title and button for renaming ${type}`, done => { + const text = type === 'tree' ? 'folder' : 'file'; + + vm.$store.state.entryModal.entry.type = type; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Rename ${text}`); + expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Rename ${text}`); + + done(); + }); + }); + }); + + describe('entryName', () => { + it('returns entries name', () => { + expect(vm.entryName).toBe('test'); + }); + + it('updated name', () => { + vm.name = 'index.js'; + + expect(vm.entryName).toBe('index.js'); + }); + }); + }); }); diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js index 792a716565c..d84f1717a61 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -8,6 +8,7 @@ import actions, { updateTempFlagForEntry, setErrorMessage, deleteEntry, + renameEntry, } from '~/ide/stores/actions'; import store from '~/ide/stores'; import * as types from '~/ide/stores/mutation_types'; @@ -468,7 +469,61 @@ describe('Multi-file store actions', () => { 'path', store.state, [{ type: types.DELETE_ENTRY, payload: 'path' }], - [{ type: 'burstUnusedSeal' }, { type: 'closeFile', payload: store.state.entries.path }], + [{ type: 'burstUnusedSeal' }], + done, + ); + }); + }); + + describe('renameEntry', () => { + it('renames entry', done => { + store.state.entries.test = { + tree: [], + }; + + testAction( + renameEntry, + { path: 'test', name: 'new-name' }, + store.state, + [ + { + type: types.RENAME_ENTRY, + payload: { path: 'test', name: 'new-name', entryPath: null }, + }, + ], + [{ type: 'deleteEntry', payload: 'test' }], + done, + ); + }); + + it('renames all entries in tree', done => { + store.state.entries.test = { + type: 'tree', + tree: [ + { + path: 'tree-1', + }, + { + path: 'tree-2', + }, + ], + }; + + testAction( + renameEntry, + { path: 'test', name: 'new-name' }, + store.state, + [ + { + type: types.RENAME_ENTRY, + payload: { path: 'test', name: 'new-name', entryPath: null }, + }, + ], + [ + { type: 'renameEntry', payload: { path: 'test', name: 'new-name', entryPath: 'tree-1' } }, + { type: 'renameEntry', payload: { path: 'test', name: 'new-name', entryPath: 'tree-2' } }, + { type: 'deleteEntry', payload: 'test' }, + ], done, ); }); diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index 133ad627f34..24a7d76f30b 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -294,9 +294,10 @@ describe('IDE commit module actions', () => { { action: 'update', file_path: jasmine.anything(), - content: jasmine.anything(), + content: undefined, encoding: jasmine.anything(), last_commit_id: undefined, + previous_path: undefined, }, ], start_branch: 'master', @@ -320,9 +321,10 @@ describe('IDE commit module actions', () => { { action: 'update', file_path: jasmine.anything(), - content: jasmine.anything(), + content: undefined, encoding: jasmine.anything(), last_commit_id: '123456789', + previous_path: undefined, }, ], start_branch: undefined, diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js index 8b5f2d0bdfa..1e836dbc3f9 100644 --- a/spec/javascripts/ide/stores/mutations_spec.js +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -206,6 +206,7 @@ describe('Multi-file store mutations', () => { it('adds to changedFiles', () => { localState.entries.filePath = { deleted: false, + type: 'blob', }; mutations.DELETE_ENTRY(localState, 'filePath'); @@ -213,4 +214,103 @@ describe('Multi-file store mutations', () => { expect(localState.changedFiles).toEqual([localState.entries.filePath]); }); }); + + describe('UPDATE_FILE_AFTER_COMMIT', () => { + it('updates URLs if prevPath is set', () => { + const f = { + ...file(), + path: 'test', + prevPath: 'testing-123', + rawPath: `${gl.TEST_HOST}/testing-123`, + permalink: `${gl.TEST_HOST}/testing-123`, + commitsPath: `${gl.TEST_HOST}/testing-123`, + blamePath: `${gl.TEST_HOST}/testing-123`, + }; + localState.entries.test = f; + localState.changedFiles.push(f); + + mutations.UPDATE_FILE_AFTER_COMMIT(localState, { file: f, lastCommit: { commit: {} } }); + + expect(f.rawPath).toBe(`${gl.TEST_HOST}/test`); + expect(f.permalink).toBe(`${gl.TEST_HOST}/test`); + expect(f.commitsPath).toBe(`${gl.TEST_HOST}/test`); + expect(f.blamePath).toBe(`${gl.TEST_HOST}/test`); + }); + }); + + describe('OPEN_NEW_ENTRY_MODAL', () => { + it('sets entryModal', () => { + localState.entries.testPath = { + ...file(), + }; + + mutations.OPEN_NEW_ENTRY_MODAL(localState, { type: 'test', path: 'testPath' }); + + expect(localState.entryModal).toEqual({ + type: 'test', + path: 'testPath', + entry: localState.entries.testPath, + }); + }); + }); + + describe('RENAME_ENTRY', () => { + beforeEach(() => { + localState.trees = { + 'gitlab-ce/master': { tree: [] }, + }; + localState.currentProjectId = 'gitlab-ce'; + localState.currentBranchId = 'master'; + localState.entries.oldPath = { + ...file(), + type: 'blob', + name: 'oldPath', + path: 'oldPath', + url: `${gl.TEST_HOST}/oldPath`, + }; + }); + + it('creates new renamed entry', () => { + mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); + + expect(localState.entries.newPath).toEqual({ + ...localState.entries.oldPath, + id: 'newPath', + name: 'newPath', + key: 'newPath-blob-name', + path: 'newPath', + tempFile: true, + prevPath: 'oldPath', + tree: [], + parentPath: '', + url: `${gl.TEST_HOST}/newPath`, + moved: jasmine.anything(), + movedPath: jasmine.anything(), + }); + }); + + it('adds new entry to changedFiles', () => { + mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); + + expect(localState.changedFiles.length).toBe(1); + expect(localState.changedFiles[0].path).toBe('newPath'); + }); + + it('sets oldEntry as moved', () => { + mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); + + expect(localState.entries.oldPath.moved).toBe(true); + }); + + it('adds to parents tree', () => { + localState.entries.oldPath.parentPath = 'parentPath'; + localState.entries.parentPath = { + ...file(), + }; + + mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); + + expect(localState.entries.parentPath.tree.length).toBe(1); + }); + }); }); diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js index 89db50b8874..9f18034f8a3 100644 --- a/spec/javascripts/ide/stores/utils_spec.js +++ b/spec/javascripts/ide/stores/utils_spec.js @@ -112,6 +112,7 @@ describe('Multi-file store utils', () => { content: 'updated file content', encoding: 'text', last_commit_id: '123456789', + previous_path: undefined, }, { action: 'create', @@ -119,13 +120,15 @@ describe('Multi-file store utils', () => { content: 'new file content', encoding: 'base64', last_commit_id: '123456789', + previous_path: undefined, }, { action: 'delete', file_path: 'deletedFile', - content: '', + content: undefined, encoding: 'text', last_commit_id: undefined, + previous_path: undefined, }, ], start_branch: undefined, @@ -172,6 +175,7 @@ describe('Multi-file store utils', () => { content: 'updated file content', encoding: 'text', last_commit_id: '123456789', + previous_path: undefined, }, { action: 'create', @@ -179,6 +183,7 @@ describe('Multi-file store utils', () => { content: 'new file content', encoding: 'base64', last_commit_id: '123456789', + previous_path: undefined, }, ], start_branch: undefined, @@ -195,13 +200,17 @@ describe('Multi-file store utils', () => { expect(utils.commitActionForFile({ tempFile: true })).toBe('create'); }); + it('returns move for moved file', () => { + expect(utils.commitActionForFile({ prevPath: 'test' })).toBe('move'); + }); + it('returns update by default', () => { expect(utils.commitActionForFile({})).toBe('update'); }); }); describe('getCommitFiles', () => { - it('returns flattened list of files and folders', () => { + it('returns list of files excluding moved files', () => { const files = [ { path: 'a', @@ -209,19 +218,9 @@ describe('Multi-file store utils', () => { deleted: true, }, { - path: 'b', - type: 'tree', - deleted: true, - tree: [ - { - path: 'c', - type: 'blob', - }, - { - path: 'd', - type: 'blob', - }, - ], + path: 'c', + type: 'blob', + moved: true, }, ]; @@ -233,16 +232,6 @@ describe('Multi-file store utils', () => { type: 'blob', deleted: true, }, - { - path: 'c', - type: 'blob', - deleted: true, - }, - { - path: 'd', - type: 'blob', - deleted: true, - }, ]); }); }); diff --git a/spec/javascripts/notes/components/discussion_counter_spec.js b/spec/javascripts/notes/components/discussion_counter_spec.js index a3869cc6498..d09bc5037ef 100644 --- a/spec/javascripts/notes/components/discussion_counter_spec.js +++ b/spec/javascripts/notes/components/discussion_counter_spec.js @@ -46,7 +46,7 @@ describe('DiscussionCounter component', () => { discussions, }); setFixtures(` - <div data-discussion-id="${firstDiscussionId}"></div> + <div class="discussion" data-discussion-id="${firstDiscussionId}"></div> `); vm.jumpToFirstUnresolvedDiscussion(); diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js index 7da931fd9cb..2a01bd85520 100644 --- a/spec/javascripts/notes/components/noteable_discussion_spec.js +++ b/spec/javascripts/notes/components/noteable_discussion_spec.js @@ -14,6 +14,7 @@ describe('noteable_discussion component', () => { preloadFixtures(discussionWithTwoUnresolvedNotes); beforeEach(() => { + window.mrTabs = {}; store = createStore(); store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); @@ -46,10 +47,15 @@ describe('noteable_discussion component', () => { it('should toggle reply form', done => { vm.$el.querySelector('.js-vue-discussion-reply').click(); + Vue.nextTick(() => { - expect(vm.$refs.noteForm).not.toBeNull(); expect(vm.isReplying).toEqual(true); - done(); + + // There is a watcher for `isReplying` which will init autosave in the next tick + Vue.nextTick(() => { + expect(vm.$refs.noteForm).not.toBeNull(); + done(); + }); }); }); @@ -101,33 +107,29 @@ describe('noteable_discussion component', () => { describe('methods', () => { describe('jumpToNextDiscussion', () => { - it('expands next unresolved discussion', () => { - spyOn(vm, 'expandDiscussion').and.stub(); - const discussions = [ - discussionMock, - { - ...discussionMock, - id: discussionMock.id + 1, - notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }], - }, - { - ...discussionMock, - id: discussionMock.id + 2, - notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: false }], - }, - ]; - const nextDiscussionId = discussionMock.id + 2; - store.replaceState({ - ...store.state, - discussions, - }); - setFixtures(` - <div data-discussion-id="${nextDiscussionId}"></div> - `); + it('expands next unresolved discussion', done => { + const discussion2 = getJSONFixture(discussionWithTwoUnresolvedNotes)[0]; + discussion2.resolved = false; + discussion2.id = 'next'; // prepare this for being identified as next one (to be jumped to) + vm.$store.dispatch('setInitialNotes', [discussionMock, discussion2]); + window.mrTabs.currentAction = 'show'; + + Vue.nextTick() + .then(() => { + spyOn(vm, 'expandDiscussion').and.stub(); + + const nextDiscussionId = discussion2.id; + + setFixtures(` + <div class="discussion" data-discussion-id="${nextDiscussionId}"></div> + `); - vm.jumpToNextDiscussion(); + vm.jumpToNextDiscussion(); - expect(vm.expandDiscussion).toHaveBeenCalledWith({ discussionId: nextDiscussionId }); + expect(vm.expandDiscussion).toHaveBeenCalledWith({ discussionId: nextDiscussionId }); + }) + .then(done) + .catch(done.fail); }); }); }); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index be2a8ba67fe..67f6a9629d9 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -1168,3 +1168,87 @@ export const collapsedSystemNotes = [ diff_discussion: false, }, ]; + +export const discussion1 = { + id: 'abc1', + resolvable: true, + resolved: false, + diff_file: { + file_path: 'about.md', + }, + position: { + formatter: { + new_line: 50, + old_line: null, + }, + }, + notes: [ + { + created_at: '2018-07-04T16:25:41.749Z', + }, + ], +}; + +export const resolvedDiscussion1 = { + id: 'abc1', + resolvable: true, + resolved: true, + diff_file: { + file_path: 'about.md', + }, + position: { + formatter: { + new_line: 50, + old_line: null, + }, + }, + notes: [ + { + created_at: '2018-07-04T16:25:41.749Z', + }, + ], +}; + +export const discussion2 = { + id: 'abc2', + resolvable: true, + resolved: false, + diff_file: { + file_path: 'README.md', + }, + position: { + formatter: { + new_line: null, + old_line: 20, + }, + }, + notes: [ + { + created_at: '2018-07-04T12:05:41.749Z', + }, + ], +}; + +export const discussion3 = { + id: 'abc3', + resolvable: true, + resolved: false, + diff_file: { + file_path: 'README.md', + }, + position: { + formatter: { + new_line: 21, + old_line: null, + }, + }, + notes: [ + { + created_at: '2018-07-05T17:25:41.749Z', + }, + ], +}; + +export const unresolvableDiscussion = { + resolvable: false, +}; diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js index 41599e00122..7f8ede51508 100644 --- a/spec/javascripts/notes/stores/getters_spec.js +++ b/spec/javascripts/notes/stores/getters_spec.js @@ -5,6 +5,11 @@ import { noteableDataMock, individualNote, collapseNotesMock, + discussion1, + discussion2, + discussion3, + resolvedDiscussion1, + unresolvableDiscussion, } from '../mock_data'; const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; @@ -109,4 +114,154 @@ describe('Getters Notes Store', () => { expect(getters.isNotesFetched(state)).toBeFalsy(); }); }); + + describe('allResolvableDiscussions', () => { + it('should return only resolvable discussions in same order', () => { + const localGetters = { + allDiscussions: [ + discussion3, + unresolvableDiscussion, + discussion1, + unresolvableDiscussion, + discussion2, + ], + }; + + expect(getters.allResolvableDiscussions(state, localGetters)).toEqual([ + discussion3, + discussion1, + discussion2, + ]); + }); + + it('should return empty array if there are no resolvable discussions', () => { + const localGetters = { + allDiscussions: [unresolvableDiscussion, unresolvableDiscussion], + }; + + expect(getters.allResolvableDiscussions(state, localGetters)).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('nextUnresolvedDiscussionId', () => { + const localGetters = { + unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'], + }; + + it('should return the ID of the discussion after the ID provided', () => { + expect(getters.nextUnresolvedDiscussionId(state, localGetters)('123')).toBe('456'); + expect(getters.nextUnresolvedDiscussionId(state, localGetters)('456')).toBe('789'); + expect(getters.nextUnresolvedDiscussionId(state, localGetters)('789')).toBe(undefined); + }); + }); + + 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(); + }); + }); }); diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index 4a4f2259d23..ddd580ae8b7 100644 --- a/spec/javascripts/pipelines/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -35,7 +35,9 @@ describe('Pipeline Url Component', () => { }, }).$mount(); - expect(component.$el.querySelector('.js-pipeline-url-link').getAttribute('href')).toEqual('foo'); + expect(component.$el.querySelector('.js-pipeline-url-link').getAttribute('href')).toEqual( + 'foo', + ); expect(component.$el.querySelector('.js-pipeline-url-link span').textContent).toEqual('#1'); }); @@ -61,11 +63,11 @@ describe('Pipeline Url Component', () => { const image = component.$el.querySelector('.js-pipeline-url-user img'); - expect( - component.$el.querySelector('.js-pipeline-url-user').getAttribute('href'), - ).toEqual(mockData.pipeline.user.web_url); + expect(component.$el.querySelector('.js-pipeline-url-user').getAttribute('href')).toEqual( + mockData.pipeline.user.web_url, + ); expect(image.getAttribute('data-original-title')).toEqual(mockData.pipeline.user.name); - expect(image.getAttribute('src')).toEqual(mockData.pipeline.user.avatar_url); + expect(image.getAttribute('src')).toEqual(`${mockData.pipeline.user.avatar_url}?width=20`); }); it('should render "API" when no user is provided', () => { @@ -100,7 +102,9 @@ describe('Pipeline Url Component', () => { }).$mount(); expect(component.$el.querySelector('.js-pipeline-url-latest').textContent).toContain('latest'); - expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain('yaml invalid'); + expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain( + 'yaml invalid', + ); expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck'); }); @@ -121,9 +125,9 @@ describe('Pipeline Url Component', () => { }, }).$mount(); - expect( - component.$el.querySelector('.js-pipeline-url-autodevops').textContent.trim(), - ).toEqual('Auto DevOps'); + expect(component.$el.querySelector('.js-pipeline-url-autodevops').textContent.trim()).toEqual( + 'Auto DevOps', + ); }); it('should render error badge when pipeline has a failure reason set', () => { @@ -142,6 +146,8 @@ describe('Pipeline Url Component', () => { }).$mount(); expect(component.$el.querySelector('.js-pipeline-url-failure').textContent).toContain('error'); - expect(component.$el.querySelector('.js-pipeline-url-failure').getAttribute('data-original-title')).toContain('some reason'); + expect( + component.$el.querySelector('.js-pipeline-url-failure').getAttribute('data-original-title'), + ).toContain('some reason'); }); }); diff --git a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js new file mode 100644 index 00000000000..d86e565036c --- /dev/null +++ b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js @@ -0,0 +1,163 @@ +import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import state from '~/reports/store/state'; +import component from '~/reports/components/grouped_test_reports_app.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; +import newFailedTestReports from '../mock_data/new_failures_report.json'; +import successTestReports from '../mock_data/no_failures_report.json'; +import mixedResultsTestReports from '../mock_data/new_and_fixed_failures_report.json'; + +describe('Grouped Test Reports App', () => { + let vm; + let mock; + const Component = Vue.extend(component); + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + vm.$store.replaceState(state()); + vm.$destroy(); + mock.restore(); + }); + + describe('with success result', () => { + beforeEach(() => { + mock.onGet('test_results.json').reply(200, successTestReports, {}); + vm = mountComponent(Component, { + endpoint: 'test_results.json', + }); + }); + + it('renders success summary text', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.fa-spinner')).toBeNull(); + expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( + 'Test summary contained no changed test results out of 11 total tests', + ); + + expect(vm.$el.textContent).toContain( + 'rspec:pg found no changed test results out of 8 total tests', + ); + expect(vm.$el.textContent).toContain( + 'java ant found no changed test results out of 3 total tests', + ); + done(); + }, 0); + }); + }); + + describe('with 204 result', () => { + beforeEach(() => { + mock.onGet('test_results.json').reply(204, {}, {}); + vm = mountComponent(Component, { + endpoint: 'test_results.json', + }); + }); + + it('renders success summary text', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.fa-spinner')).not.toBeNull(); + expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( + 'Test summary results are being parsed', + ); + + done(); + }, 0); + }); + }); + + describe('with new failed result', () => { + beforeEach(() => { + mock.onGet('test_results.json').reply(200, newFailedTestReports, {}); + vm = mountComponent(Component, { + endpoint: 'test_results.json', + }); + }); + + it('renders failed summary text + new badge', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.fa-spinner')).toBeNull(); + expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( + 'Test summary contained 2 failed test results out of 11 total tests', + ); + + expect(vm.$el.textContent).toContain( + 'rspec:pg found 2 failed test results out of 8 total tests', + ); + expect(vm.$el.textContent).toContain('New'); + expect(vm.$el.textContent).toContain( + 'java ant found no changed test results out of 3 total tests', + ); + done(); + }, 0); + }); + }); + + describe('with mixed results', () => { + beforeEach(() => { + mock.onGet('test_results.json').reply(200, mixedResultsTestReports, {}); + vm = mountComponent(Component, { + endpoint: 'test_results.json', + }); + }); + + it('renders summary text', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.fa-spinner')).toBeNull(); + expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( + 'Test summary contained 2 failed test results and 2 fixed test results out of 11 total tests', + ); + + expect(vm.$el.textContent).toContain( + 'rspec:pg found 1 failed test result and 2 fixed test results out of 8 total tests', + ); + expect(vm.$el.textContent).toContain('New'); + expect(vm.$el.textContent).toContain( + ' java ant found 1 failed test result out of 3 total tests', + ); + done(); + }, 0); + }); + }); + + describe('with error', () => { + beforeEach(() => { + mock.onGet('test_results.json').reply(500, {}, {}); + vm = mountComponent(Component, { + endpoint: 'test_results.json', + }); + }); + + it('renders loading summary text with loading icon', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( + 'Test summary failed loading results', + ); + done(); + }, 0); + }); + }); + + describe('while loading', () => { + beforeEach(() => { + mock.onGet('test_results.json').reply(200, {}, {}); + vm = mountComponent(Component, { + endpoint: 'test_results.json', + }); + }); + + it('renders loading summary text with loading icon', done => { + expect(vm.$el.querySelector('.fa-spinner')).not.toBeNull(); + expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( + 'Test summary results are being parsed', + ); + + setTimeout(() => { + done(); + }, 0); + }); + }); +}); diff --git a/spec/javascripts/reports/components/modal_spec.js b/spec/javascripts/reports/components/modal_spec.js new file mode 100644 index 00000000000..3a567c40eca --- /dev/null +++ b/spec/javascripts/reports/components/modal_spec.js @@ -0,0 +1,45 @@ +import Vue from 'vue'; +import component from '~/reports/components/modal.vue'; +import state from '~/reports/store/state'; +import mountComponent from '../../helpers/vue_mount_component_helper'; +import { trimText } from '../../helpers/vue_component_helper'; + +describe('Grouped Test Reports Modal', () => { + const Component = Vue.extend(component); + const modalDataStructure = state().modal.data; + + // populate data + modalDataStructure.execution_time.value = 0.009411; + modalDataStructure.system_output.value = 'Failure/Error: is_expected.to eq(3)\n\n'; + modalDataStructure.class.value = 'link'; + + let vm; + + beforeEach(() => { + vm = mountComponent(Component, { + title: 'Test#sum when a is 1 and b is 2 returns summary', + modalData: modalDataStructure, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders code block', () => { + expect(vm.$el.querySelector('code').textContent).toEqual(modalDataStructure.system_output.value); + }); + + it('renders link', () => { + expect(vm.$el.querySelector('.js-modal-link').getAttribute('href')).toEqual(modalDataStructure.class.value); + expect(trimText(vm.$el.querySelector('.js-modal-link').textContent)).toEqual(modalDataStructure.class.value); + }); + + it('renders miliseconds', () => { + expect(vm.$el.textContent).toContain(`${modalDataStructure.execution_time.value} ms`); + }); + + it('render title', () => { + expect(trimText(vm.$el.querySelector('.modal-title').textContent)).toEqual('Test#sum when a is 1 and b is 2 returns summary'); + }); +}); diff --git a/spec/javascripts/reports/components/test_issue_body_spec.js b/spec/javascripts/reports/components/test_issue_body_spec.js new file mode 100644 index 00000000000..0ea81f714e7 --- /dev/null +++ b/spec/javascripts/reports/components/test_issue_body_spec.js @@ -0,0 +1,71 @@ +import Vue from 'vue'; +import component from '~/reports/components/test_issue_body.vue'; +import createStore from '~/reports/store'; +import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper'; +import { trimText } from '../../helpers/vue_component_helper'; +import { issue } from '../mock_data/mock_data'; + +describe('Test Issue body', () => { + let vm; + const Component = Vue.extend(component); + const store = createStore(); + + const commonProps = { + issue, + status: 'failed', + }; + + afterEach(() => { + vm.$destroy(); + }); + + describe('on click', () => { + it('calls openModal action', () => { + vm = mountComponentWithStore(Component, { + store, + props: commonProps, + }); + + spyOn(vm, 'openModal'); + + vm.$el.querySelector('button').click(); + expect(vm.openModal).toHaveBeenCalledWith({ + issue: commonProps.issue, + }); + }); + }); + + describe('is new', () => { + beforeEach(() => { + vm = mountComponentWithStore(Component, { + store, + props: Object.assign({}, commonProps, { isNew: true }), + }); + }); + + it('renders issue name', () => { + expect(vm.$el.textContent).toContain(commonProps.issue.name); + }); + + it('renders new badge', () => { + expect(trimText(vm.$el.querySelector('.badge').textContent)).toEqual('New'); + }); + }); + + describe('not new', () => { + beforeEach(() => { + vm = mountComponentWithStore(Component, { + store, + props: commonProps, + }); + }); + + it('renders issue name', () => { + expect(vm.$el.textContent).toContain(commonProps.issue.name); + }); + + it('does not renders new badge', () => { + expect(vm.$el.querySelector('.badge')).toEqual(null); + }); + }); +}); diff --git a/spec/javascripts/reports/mock_data/mock_data.js b/spec/javascripts/reports/mock_data/mock_data.js new file mode 100644 index 00000000000..0d90253bad2 --- /dev/null +++ b/spec/javascripts/reports/mock_data/mock_data.js @@ -0,0 +1,8 @@ +// eslint-disable-next-line import/prefer-default-export +export const issue = { + result: 'failure', + name: 'Test#sum when a is 1 and b is 2 returns summary', + execution_time: 0.009411, + system_output: + "Failure/Error: is_expected.to eq(3)\n\n expected: 3\n got: -1\n\n (compared using ==)\n./spec/test_spec.rb:12:in `block (4 levels) in \u003ctop (required)\u003e'", +}; diff --git a/spec/javascripts/reports/mock_data/new_and_fixed_failures_report.json b/spec/javascripts/reports/mock_data/new_and_fixed_failures_report.json new file mode 100644 index 00000000000..ceaf894375a --- /dev/null +++ b/spec/javascripts/reports/mock_data/new_and_fixed_failures_report.json @@ -0,0 +1 @@ +{"status":"failed","summary":{"total":11,"resolved":2,"failed":2},"suites":[{"name":"rspec:pg","status":"failed","summary":{"total":8,"resolved":2,"failed":1},"new_failures":[{"status":"failed","name":"Test#subtract when a is 2 and b is 1 returns correct result","execution_time":0.00908,"system_output":"Failure/Error: is_expected.to eq(1)\n\n expected: 1\n got: 3\n\n (compared using ==)\n./spec/test_spec.rb:43:in `block (4 levels) in <top (required)>'"}],"resolved_failures":[{"status":"success","name":"Test#sum when a is 1 and b is 2 returns summary","execution_time":0.000318,"system_output":null},{"status":"success","name":"Test#sum when a is 100 and b is 200 returns summary","execution_time":0.000074,"system_output":null}],"existing_failures":[]},{"name":"java ant","status":"failed","summary":{"total":3,"resolved":0,"failed":1},"new_failures":[],"resolved_failures":[],"existing_failures":[{"status":"failed","name":"sumTest","execution_time":0.004,"system_output":"junit.framework.AssertionFailedError: expected:<3> but was:<-1>\n\tat CalculatorTest.sumTest(Unknown Source)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n"}]}]}
\ No newline at end of file diff --git a/spec/javascripts/reports/mock_data/new_failures_report.json b/spec/javascripts/reports/mock_data/new_failures_report.json new file mode 100644 index 00000000000..930efe16f65 --- /dev/null +++ b/spec/javascripts/reports/mock_data/new_failures_report.json @@ -0,0 +1 @@ +{"summary":{"total":11,"resolved":0,"failed":2},"suites":[{"name":"rspec:pg","summary":{"total":8,"resolved":0,"failed":2},"new_failures":[{"result":"failure","name":"Test#sum when a is 1 and b is 2 returns summary","execution_time":0.009411,"system_output":"Failure/Error: is_expected.to eq(3)\n\n expected: 3\n got: -1\n\n (compared using ==)\n./spec/test_spec.rb:12:in `block (4 levels) in <top (required)>'"},{"result":"failure","name":"Test#sum when a is 100 and b is 200 returns summary","execution_time":0.000162,"system_output":"Failure/Error: is_expected.to eq(300)\n\n expected: 300\n got: -100\n\n (compared using ==)\n./spec/test_spec.rb:21:in `block (4 levels) in <top (required)>'"}],"resolved_failures":[],"existing_failures":[]},{"name":"java ant","summary":{"total":3,"resolved":0,"failed":0},"new_failures":[],"resolved_failures":[],"existing_failures":[]}]}
\ No newline at end of file diff --git a/spec/javascripts/reports/mock_data/no_failures_report.json b/spec/javascripts/reports/mock_data/no_failures_report.json new file mode 100644 index 00000000000..6c0675ff7dc --- /dev/null +++ b/spec/javascripts/reports/mock_data/no_failures_report.json @@ -0,0 +1 @@ +{"status":"success","summary":{"total":11,"resolved":0,"failed":0},"suites":[{"name":"rspec:pg","status":"success","summary":{"total":8,"resolved":0,"failed":0},"new_failures":[],"resolved_failures":[],"existing_failures":[]},{"name":"java ant","status":"success","summary":{"total":3,"resolved":0,"failed":0},"new_failures":[],"resolved_failures":[],"existing_failures":[]}]}
\ No newline at end of file diff --git a/spec/javascripts/reports/store/actions_spec.js b/spec/javascripts/reports/store/actions_spec.js index c714c5af156..41137b50847 100644 --- a/spec/javascripts/reports/store/actions_spec.js +++ b/spec/javascripts/reports/store/actions_spec.js @@ -8,6 +8,8 @@ import { clearEtagPoll, receiveReportsSuccess, receiveReportsError, + openModal, + setModalData, } from '~/reports/store/actions'; import state from '~/reports/store/state'; import * as types from '~/reports/store/mutation_types'; @@ -56,7 +58,9 @@ describe('Reports Store Actions', () => { describe('success', () => { it('dispatches requestReports and receiveReportsSuccess ', done => { - mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { summary: {}, suites: [{ name: 'rspec' }] }); + mock + .onGet(`${TEST_HOST}/endpoint.json`) + .replyOnce(200, { summary: {}, suites: [{ name: 'rspec' }] }); testAction( fetchReports, @@ -68,7 +72,7 @@ describe('Reports Store Actions', () => { type: 'requestReports', }, { - payload: { summary: {}, suites: [{ name: 'rspec' }] }, + payload: { data: { summary: {}, suites: [{ name: 'rspec' }] }, status: 200 }, type: 'receiveReportsSuccess', }, ], @@ -103,16 +107,27 @@ describe('Reports Store Actions', () => { }); describe('receiveReportsSuccess', () => { - it('should commit RECEIVE_REPORTS_SUCCESS mutation', done => { + it('should commit RECEIVE_REPORTS_SUCCESS mutation with 200', done => { testAction( receiveReportsSuccess, - { summary: {} }, + { data: { summary: {} }, status: 200 }, mockedState, [{ type: types.RECEIVE_REPORTS_SUCCESS, payload: { summary: {} } }], [], done, ); }); + + it('should not commit RECEIVE_REPORTS_SUCCESS mutation with 204', done => { + testAction( + receiveReportsSuccess, + { data: { summary: {} }, status: 204 }, + mockedState, + [], + [], + done, + ); + }); }); describe('receiveReportsError', () => { @@ -127,4 +142,30 @@ describe('Reports Store Actions', () => { ); }); }); + + describe('openModal', () => { + it('should dispatch setModalData', done => { + testAction( + openModal, + { name: 'foo' }, + mockedState, + [], + [{ type: 'setModalData', payload: { name: 'foo' } }], + done, + ); + }); + }); + + describe('setModalData', () => { + it('should commit SET_ISSUE_MODAL_DATA', done => { + testAction( + setModalData, + { name: 'foo' }, + mockedState, + [{ type: types.SET_ISSUE_MODAL_DATA, payload: { name: 'foo' } }], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/reports/store/mutations_spec.js b/spec/javascripts/reports/store/mutations_spec.js index 3e0b15438c3..8f99d2675a5 100644 --- a/spec/javascripts/reports/store/mutations_spec.js +++ b/spec/javascripts/reports/store/mutations_spec.js @@ -1,6 +1,7 @@ import state from '~/reports/store/state'; import mutations from '~/reports/store/mutations'; import * as types from '~/reports/store/mutation_types'; +import { issue } from '../mock_data/mock_data'; describe('Reports Store Mutations', () => { let stateCopy; @@ -42,24 +43,21 @@ describe('Reports Store Mutations', () => { { name: 'StringHelper#concatenate when a is git and b is lab returns summary', execution_time: 0.0092435, - system_output: - 'Failure/Error: is_expected.to eq(\'gitlab\')', + system_output: "Failure/Error: is_expected.to eq('gitlab')", }, ], resolved_failures: [ { name: 'StringHelper#concatenate when a is git and b is lab returns summary', execution_time: 0.009235, - system_output: - 'Failure/Error: is_expected.to eq(\'gitlab\')', + system_output: "Failure/Error: is_expected.to eq('gitlab')", }, ], existing_failures: [ { name: 'StringHelper#concatenate when a is git and b is lab returns summary', execution_time: 1232.08, - system_output: - 'Failure/Error: is_expected.to eq(\'gitlab\')', + system_output: "Failure/Error: is_expected.to eq('gitlab')", }, ], }, @@ -89,6 +87,7 @@ describe('Reports Store Mutations', () => { beforeEach(() => { mutations[types.RECEIVE_REPORTS_ERROR](stateCopy); }); + it('should reset isLoading', () => { expect(stateCopy.isLoading).toEqual(false); }); @@ -97,5 +96,25 @@ describe('Reports Store Mutations', () => { expect(stateCopy.hasError).toEqual(true); }); + it('should reset reports', () => { + expect(stateCopy.reports).toEqual([]); + }); + }); + + describe('SET_ISSUE_MODAL_DATA', () => { + beforeEach(() => { + mutations[types.SET_ISSUE_MODAL_DATA](stateCopy, { + issue, + }); + }); + + it('should set modal title', () => { + expect(stateCopy.modal.title).toEqual(issue.name); + }); + + it('should set modal data', () => { + expect(stateCopy.modal.data.execution_time.value).toEqual(issue.execution_time); + expect(stateCopy.modal.data.system_output.value).toEqual(issue.system_output); + }); }); }); diff --git a/spec/javascripts/reports/store/utils_spec.js b/spec/javascripts/reports/store/utils_spec.js new file mode 100644 index 00000000000..1679d120db2 --- /dev/null +++ b/spec/javascripts/reports/store/utils_spec.js @@ -0,0 +1,138 @@ +import * as utils from '~/reports/store/utils'; +import { + STATUS_FAILED, + STATUS_SUCCESS, + ICON_WARNING, + ICON_SUCCESS, + ICON_NOTFOUND, +} from '~/reports/constants'; + +describe('Reports store utils', () => { + describe('summaryTextbuilder', () => { + it('should render text for no changed results in multiple tests', () => { + const name = 'Test summary'; + const data = { total: 10 }; + const result = utils.summaryTextBuilder(name, data); + + expect(result).toBe('Test summary contained no changed test results out of 10 total tests'); + }); + + it('should render text for no changed results in one test', () => { + const name = 'Test summary'; + const data = { total: 1 }; + const result = utils.summaryTextBuilder(name, data); + + expect(result).toBe('Test summary contained no changed test results out of 1 total test'); + }); + + it('should render text for multiple failed results', () => { + const name = 'Test summary'; + const data = { failed: 3, total: 10 }; + const result = utils.summaryTextBuilder(name, data); + + expect(result).toBe('Test summary contained 3 failed test results out of 10 total tests'); + }); + + it('should render text for multiple fixed results', () => { + const name = 'Test summary'; + const data = { resolved: 4, total: 10 }; + const result = utils.summaryTextBuilder(name, data); + + expect(result).toBe('Test summary contained 4 fixed test results out of 10 total tests'); + }); + + it('should render text for multiple fixed, and multiple failed results', () => { + const name = 'Test summary'; + const data = { failed: 3, resolved: 4, total: 10 }; + const result = utils.summaryTextBuilder(name, data); + + expect(result).toBe( + 'Test summary contained 3 failed test results and 4 fixed test results out of 10 total tests', + ); + }); + + it('should render text for a singular fixed, and a singular failed result', () => { + const name = 'Test summary'; + const data = { failed: 1, resolved: 1, total: 10 }; + const result = utils.summaryTextBuilder(name, data); + + expect(result).toBe( + 'Test summary contained 1 failed test result and 1 fixed test result out of 10 total tests', + ); + }); + }); + + describe('reportTextBuilder', () => { + it('should render text for no changed results in multiple tests', () => { + const name = 'Rspec'; + const data = { total: 10 }; + const result = utils.reportTextBuilder(name, data); + + expect(result).toBe('Rspec found no changed test results out of 10 total tests'); + }); + + it('should render text for no changed results in one test', () => { + const name = 'Rspec'; + const data = { total: 1 }; + const result = utils.reportTextBuilder(name, data); + + expect(result).toBe('Rspec found no changed test results out of 1 total test'); + }); + + it('should render text for multiple failed results', () => { + const name = 'Rspec'; + const data = { failed: 3, total: 10 }; + const result = utils.reportTextBuilder(name, data); + + expect(result).toBe('Rspec found 3 failed test results out of 10 total tests'); + }); + + it('should render text for multiple fixed results', () => { + const name = 'Rspec'; + const data = { resolved: 4, total: 10 }; + const result = utils.reportTextBuilder(name, data); + + expect(result).toBe('Rspec found 4 fixed test results out of 10 total tests'); + }); + + it('should render text for multiple fixed, and multiple failed results', () => { + const name = 'Rspec'; + const data = { failed: 3, resolved: 4, total: 10 }; + const result = utils.reportTextBuilder(name, data); + + expect(result).toBe( + 'Rspec found 3 failed test results and 4 fixed test results out of 10 total tests', + ); + }); + + it('should render text for a singular fixed, and a singular failed result', () => { + const name = 'Rspec'; + const data = { failed: 1, resolved: 1, total: 10 }; + const result = utils.reportTextBuilder(name, data); + + expect(result).toBe( + 'Rspec found 1 failed test result and 1 fixed test result out of 10 total tests', + ); + }); + }); + + describe('statusIcon', () => { + describe('with failed status', () => { + it('returns ICON_WARNING', () => { + expect(utils.statusIcon(STATUS_FAILED)).toEqual(ICON_WARNING); + }); + }); + + describe('with success status', () => { + it('returns ICON_SUCCESS', () => { + expect(utils.statusIcon(STATUS_SUCCESS)).toEqual(ICON_SUCCESS); + }); + }); + + describe('without a status', () => { + it('returns ICON_NOTFOUND', () => { + expect(utils.statusIcon()).toEqual(ICON_NOTFOUND); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/code_block_spec.js b/spec/javascripts/vue_shared/components/code_block_spec.js new file mode 100644 index 00000000000..6b91a20ff76 --- /dev/null +++ b/spec/javascripts/vue_shared/components/code_block_spec.js @@ -0,0 +1,33 @@ +import Vue from 'vue'; +import component from '~/vue_shared/components/code_block.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Code Block', () => { + const Component = Vue.extend(component); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a code block with the provided code', () => { + const code = + "Failure/Error: is_expected.to eq(3)\n\n expected: 3\n got: -1\n\n (compared using ==)\n./spec/test_spec.rb:12:in `block (4 levels) in \u003ctop (required)\u003e'"; + + vm = mountComponent(Component, { + code, + }); + + expect(vm.$el.querySelector('code').textContent).toEqual(code); + }); + + it('escapes XSS injections', () => { + const code = 'CCC<img src=x onerror=alert(document.domain)>'; + + vm = mountComponent(Component, { + code, + }); + + expect(vm.$el.querySelector('code').textContent).toEqual(code); + }); +}); diff --git a/spec/javascripts/vue_shared/components/gl_modal_spec.js b/spec/javascripts/vue_shared/components/gl_modal_spec.js index e4737714312..263824a102a 100644 --- a/spec/javascripts/vue_shared/components/gl_modal_spec.js +++ b/spec/javascripts/vue_shared/components/gl_modal_spec.js @@ -29,7 +29,7 @@ describe('GlModal', () => { describe('without id', () => { beforeEach(() => { - vm = mountComponent(modalComponent, { }); + vm = mountComponent(modalComponent, {}); }); it('does not add an id attribute to the modal', () => { @@ -83,7 +83,7 @@ describe('GlModal', () => { }); }); - it('works with data-toggle="modal"', (done) => { + it('works with data-toggle="modal"', done => { setFixtures(` <button id="modal-button" data-toggle="modal" data-target="#my-modal"></button> <div id="modal-container"></div> @@ -91,9 +91,13 @@ describe('GlModal', () => { const modalContainer = document.getElementById('modal-container'); const modalButton = document.getElementById('modal-button'); - vm = mountComponent(modalComponent, { - id: 'my-modal', - }, modalContainer); + vm = mountComponent( + modalComponent, + { + id: 'my-modal', + }, + modalContainer, + ); $(vm.$el).on('shown.bs.modal', () => done()); modalButton.click(); @@ -103,7 +107,7 @@ describe('GlModal', () => { const dummyEvent = 'not really an event'; beforeEach(() => { - vm = mountComponent(modalComponent, { }); + vm = mountComponent(modalComponent, {}); spyOn(vm, '$emit'); }); @@ -122,11 +126,27 @@ describe('GlModal', () => { expect(vm.$emit).toHaveBeenCalledWith('submit', dummyEvent); }); }); + + describe('opened', () => { + it('emits a open event', () => { + vm.opened(); + + expect(vm.$emit).toHaveBeenCalledWith('open'); + }); + }); + + describe('closed', () => { + it('emits a closed event', () => { + vm.closed(); + + expect(vm.$emit).toHaveBeenCalledWith('closed'); + }); + }); }); describe('slots', () => { const slotContent = 'this should go into the slot'; - const modalWithSlot = (slotName) => { + const modalWithSlot = slotName => { let template; if (slotName) { template = ` diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js index 7e57c51bf29..db665fdaad3 100644 --- a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js +++ b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js @@ -27,7 +27,7 @@ describe('issue placeholder system note component', () => { userDataMock.path, ); expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual( - userDataMock.avatar_url, + `${userDataMock.avatar_url}?width=40`, ); }); }); diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js index 656b57d764e..dc7652c77f7 100644 --- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js @@ -12,7 +12,7 @@ const DEFAULT_PROPS = { tooltipPlacement: 'bottom', }; -describe('User Avatar Image Component', function () { +describe('User Avatar Image Component', function() { let vm; let UserAvatarImage; @@ -20,37 +20,37 @@ describe('User Avatar Image Component', function () { UserAvatarImage = Vue.extend(userAvatarImage); }); - describe('Initialization', function () { - beforeEach(function () { + describe('Initialization', function() { + beforeEach(function() { vm = mountComponent(UserAvatarImage, { ...DEFAULT_PROPS, }).$mount(); }); - it('should return a defined Vue component', function () { + it('should return a defined Vue component', function() { expect(vm).toBeDefined(); }); - it('should have <img> as a child element', function () { + it('should have <img> as a child element', function() { expect(vm.$el.tagName).toBe('IMG'); - expect(vm.$el.getAttribute('src')).toBe(DEFAULT_PROPS.imgSrc); - expect(vm.$el.getAttribute('data-src')).toBe(DEFAULT_PROPS.imgSrc); + expect(vm.$el.getAttribute('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); + expect(vm.$el.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); expect(vm.$el.getAttribute('alt')).toBe(DEFAULT_PROPS.imgAlt); }); - it('should properly compute tooltipContainer', function () { + it('should properly compute tooltipContainer', function() { expect(vm.tooltipContainer).toBe('body'); }); - it('should properly render tooltipContainer', function () { + it('should properly render tooltipContainer', function() { expect(vm.$el.getAttribute('data-container')).toBe('body'); }); - it('should properly compute avatarSizeClass', function () { + it('should properly compute avatarSizeClass', function() { expect(vm.avatarSizeClass).toBe('s99'); }); - it('should properly render img css', function () { + it('should properly render img css', function() { const { classList } = vm.$el; const containsAvatar = classList.contains('avatar'); const containsSizeClass = classList.contains('s99'); @@ -64,21 +64,21 @@ describe('User Avatar Image Component', function () { }); }); - describe('Initialization when lazy', function () { - beforeEach(function () { + describe('Initialization when lazy', function() { + beforeEach(function() { vm = mountComponent(UserAvatarImage, { ...DEFAULT_PROPS, lazy: true, }).$mount(); }); - it('should add lazy attributes', function () { + it('should add lazy attributes', function() { const { classList } = vm.$el; const lazyClass = classList.contains('lazy'); expect(lazyClass).toBe(true); expect(vm.$el.getAttribute('src')).toBe(placeholderImage); - expect(vm.$el.getAttribute('data-src')).toBe(DEFAULT_PROPS.imgSrc); + expect(vm.$el.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); }); }); }); |