diff options
| author | Clement Ho <ClemMakesApps@gmail.com> | 2018-04-19 13:43:06 -0500 |
|---|---|---|
| committer | Clement Ho <ClemMakesApps@gmail.com> | 2018-04-19 13:43:06 -0500 |
| commit | cefeeaea3364c52afc8077c6eaa8914a4bb27852 (patch) | |
| tree | 03c1100e2636510f89982b4a3473e868a3745eab /spec | |
| parent | 59d6161e1ea5f4650fe45a0f6bd2e45bb061f6e1 (diff) | |
| parent | b1e9d98a29605daf7ccdd0e324dd399fdebfee41 (diff) | |
| download | gitlab-ce-cefeeaea3364c52afc8077c6eaa8914a4bb27852.tar.gz | |
Merge branch 'master' into bootstrap4
Diffstat (limited to 'spec')
54 files changed, 1513 insertions, 419 deletions
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index b9a979044fe..f677cec3408 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -190,7 +190,10 @@ describe Projects::JobsController do expect(response).to have_gitlab_http_status(:ok) expect(json_response['id']).to eq job.id expect(json_response['status']).to eq job.status - expect(json_response['html']).to be_nil + end + + it 'returns no job log message' do + expect(json_response['html']).to eq('No job log') end end diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb index c3b71458e38..a102a3a3c8c 100644 --- a/spec/controllers/projects/repositories_controller_spec.rb +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -40,6 +40,30 @@ describe Projects::RepositoriesController do expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") end + it 'handles legacy queries with the ref specified as ref in params' do + get :archive, namespace_id: project.namespace, project_id: project, ref: 'feature', format: 'zip' + + expect(response).to have_gitlab_http_status(200) + expect(assigns(:ref)).to eq('feature') + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") + end + + it 'handles legacy queries with the ref specified as id in params' do + get :archive, namespace_id: project.namespace, project_id: project, id: 'feature', format: 'zip' + + expect(response).to have_gitlab_http_status(200) + expect(assigns(:ref)).to eq('feature') + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") + end + + it 'prioritizes the id param over the ref param when both are specified' do + get :archive, namespace_id: project.namespace, project_id: project, id: 'feature', ref: 'feature_conflict', format: 'zip' + + expect(response).to have_gitlab_http_status(200) + expect(assigns(:ref)).to eq('feature') + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") + end + context "when the service raises an error" do before do allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed") diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 33de637ff7d..4acc008ed38 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -243,5 +243,10 @@ FactoryBot.define do failed failure_reason 1 end + + trait :api_failure do + failed + failure_reason 2 + end end end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 38a190a5668..e3a31a8e669 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -237,6 +237,22 @@ describe 'Issue Boards', :js do end context 'labels' do + it 'shows current labels when editing' do + click_card(card) + + page.within('.labels') do + click_link 'Edit' + + wait_for_requests + + page.within('.value') do + expect(page).to have_selector('.label', count: 2) + expect(page).to have_content(development.title) + expect(page).to have_content(stretch.title) + end + end + end + it 'adds a single label' do click_card(card) @@ -296,7 +312,9 @@ describe 'Issue Boards', :js do wait_for_requests - click_link stretch.title + within('.dropdown-menu-labels') do + click_link stretch.title + end wait_for_requests diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 27551bb70ee..830c794376d 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -5,9 +5,9 @@ feature 'Issue Sidebar' do let(:group) { create(:group, :nested) } let(:project) { create(:project, :public, namespace: group) } - let(:issue) { create(:issue, project: project) } let!(:user) { create(:user)} let!(:label) { create(:label, project: project, title: 'bug') } + let(:issue) { create(:labeled_issue, project: project, labels: [label]) } let!(:xss_label) { create(:label, project: project, title: '<script>alert("xss");</script>') } before do @@ -112,11 +112,18 @@ feature 'Issue Sidebar' do context 'editing issue labels', :js do before do + issue.update_attributes(labels: [label]) page.within('.block.labels') do find('.edit-link').click end end + it 'shows the current set of labels' do + page.within('.issuable-show-labels') do + expect(page).to have_content label.title + end + end + it 'shows option to create a project label' do page.within('.block.labels') do expect(page).to have_content 'Create project' diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb index 8e6493bbd93..4a44ec302fc 100644 --- a/spec/features/issues/todo_spec.rb +++ b/spec/features/issues/todo_spec.rb @@ -14,7 +14,7 @@ feature 'Manually create a todo item from issue', :js do it 'creates todo when clicking button' do page.within '.issuable-sidebar' do click_button 'Add todo' - expect(page).to have_content 'Mark done' + expect(page).to have_content 'Mark todo as done' end page.within '.header-content .todos-count' do @@ -31,7 +31,7 @@ feature 'Manually create a todo item from issue', :js do it 'marks a todo as done' do page.within '.issuable-sidebar' do click_button 'Add todo' - click_button 'Mark done' + click_button 'Mark todo as done' end expect(page).to have_selector('.todos-count', visible: false) diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index a460024542c..a00db6dd161 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -491,16 +491,18 @@ feature 'Jobs' do end end - describe "POST /:project/jobs/:id/retry" do + describe "POST /:project/jobs/:id/retry", :js do context "Job from project", :js do before do job.run! + job.cancel! visit project_job_path(project, job) - find('.js-cancel-job').click() + wait_for_requests + find('.js-retry-button').click end - it 'shows the right status and buttons', :js do + it 'shows the right status and buttons' do page.within('aside.right-sidebar') do expect(page).to have_content 'Cancel' end diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb index d96c7e655ba..b242e41df1c 100644 --- a/spec/features/projects/tree/create_directory_spec.rb +++ b/spec/features/projects/tree/create_directory_spec.rb @@ -44,6 +44,8 @@ feature 'Multi-file editor new directory', :js do wait_for_requests + click_button 'Stage all' + fill_in('commit-message', with: 'commit message ide') click_button('Commit') diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb index a4cbd5cf766..7d65456e049 100644 --- a/spec/features/projects/tree/create_file_spec.rb +++ b/spec/features/projects/tree/create_file_spec.rb @@ -34,6 +34,8 @@ feature 'Multi-file editor new file', :js do wait_for_requests + click_button 'Stage all' + fill_in('commit-message', with: 'commit message ide') click_button('Commit') diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb index 375bcc9087e..796d40cb625 100644 --- a/spec/finders/group_descendants_finder_spec.rb +++ b/spec/finders/group_descendants_finder_spec.rb @@ -35,15 +35,6 @@ describe GroupDescendantsFinder do expect(finder.execute).to contain_exactly(project) end - it 'does not include projects shared with the group' do - project = create(:project, namespace: group) - other_project = create(:project) - other_project.project_group_links.create(group: group, - group_access: ProjectGroupLink::MASTER) - - expect(finder.execute).to contain_exactly(project) - end - context 'when archived is `true`' do let(:params) { { archived: 'true' } } diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index a486ea7a62a..cddb49b320f 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -22,11 +22,15 @@ describe IssuablesHelper do end describe '#issuable_labels_tooltip' do - it 'returns label text' do + it 'returns label text with no labels' do + expect(issuable_labels_tooltip([])).to eq("Labels") + end + + it 'returns label text with labels within max limit' do expect(issuable_labels_tooltip([label])).to eq(label.title) end - it 'returns label text' do + it 'returns label text with labels exceeding max limit' do expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more") end end diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb index 70b4a89cb86..f5185cb2857 100644 --- a/spec/helpers/milestones_helper_spec.rb +++ b/spec/helpers/milestones_helper_spec.rb @@ -83,58 +83,4 @@ describe MilestonesHelper do end end end - - describe '#milestone_remaining_days' do - around do |example| - Timecop.freeze(Time.utc(2017, 3, 17)) { example.run } - end - - context 'when less than 31 days remaining' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) } - - it 'returns days remaining' do - expect(milestone_remaining).to eq("<strong>12</strong> days remaining") - end - end - - context 'when less than 1 year and more than 30 days remaining' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) } - - it 'returns months remaining' do - expect(milestone_remaining).to eq("<strong>2</strong> months remaining") - end - end - - context 'when more than 1 year remaining' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) } - - it 'returns years remaining' do - expect(milestone_remaining).to eq("<strong>1</strong> year remaining") - end - end - - context 'when milestone is expired' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.days.ago.utc)) } - - it 'returns "Past due"' do - expect(milestone_remaining).to eq("<strong>Past due</strong>") - end - end - - context 'when milestone has start_date in the future' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) } - - it 'returns "Upcoming"' do - expect(milestone_remaining).to eq("<strong>Upcoming</strong>") - end - end - - context 'when milestone has start_date in the past' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.ago.utc)) } - - it 'returns days elapsed' do - expect(milestone_remaining).to eq("<strong>2</strong> days elapsed") - end - end - end end diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js index 2abf52a1676..8427e8a0ba7 100644 --- a/spec/javascripts/collapsed_sidebar_todo_spec.js +++ b/spec/javascripts/collapsed_sidebar_todo_spec.js @@ -85,7 +85,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { setTimeout(() => { expect( document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), - ).toBe('Mark done'); + ).toBe('Mark todo as done'); done(); }); @@ -97,7 +97,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { setTimeout(() => { expect( document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'), - ).toBe('Mark done'); + ).toBe('Mark todo as done'); done(); }); @@ -128,13 +128,13 @@ describe('Issuable right sidebar collapsed todo toggle', () => { .catch(done.fail); }); - it('updates aria-label to mark done', (done) => { + it('updates aria-label to mark todo as done', (done) => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); setTimeout(() => { expect( document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), - ).toBe('Mark done'); + ).toBe('Mark todo as done'); done(); }); @@ -147,7 +147,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { .then(() => { expect( document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), - ).toBe('Mark done'); + ).toBe('Mark todo as done'); document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); }) diff --git a/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js new file mode 100644 index 00000000000..b80d08de7b1 --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js @@ -0,0 +1,95 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import emptyState from '~/ide/components/commit_sidebar/empty_state.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { resetStore } from '../../helpers'; + +describe('IDE commit panel empty state', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(emptyState); + + vm = createComponentWithStore(Component, store, { + noChangesStateSvgPath: 'no-changes', + committedStateSvgPath: 'committed-state', + }); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + describe('statusSvg', () => { + it('uses noChangesStateSvgPath when commit message is empty', () => { + expect(vm.statusSvg).toBe('no-changes'); + expect(vm.$el.querySelector('img').getAttribute('src')).toBe( + 'no-changes', + ); + }); + + it('uses committedStateSvgPath when commit message exists', done => { + vm.$store.state.lastCommitMsg = 'testing'; + + Vue.nextTick(() => { + expect(vm.statusSvg).toBe('committed-state'); + expect(vm.$el.querySelector('img').getAttribute('src')).toBe( + 'committed-state', + ); + + done(); + }); + }); + }); + + it('renders no changes text when last commit message is empty', () => { + expect(vm.$el.textContent).toContain('No changes'); + }); + + it('renders last commit message when it exists', done => { + vm.$store.state.lastCommitMsg = 'testing commit message'; + + Vue.nextTick(() => { + expect(vm.$el.textContent).toContain('testing commit message'); + + done(); + }); + }); + + describe('toggle button', () => { + it('calls store action', () => { + spyOn(vm, 'toggleRightPanelCollapsed'); + + vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click(); + + expect(vm.toggleRightPanelCollapsed).toHaveBeenCalled(); + }); + + it('renders collapsed class', done => { + vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click(); + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull(); + + done(); + }); + }); + }); + + describe('collapsed state', () => { + beforeEach(done => { + vm.$store.state.rightPanelCollapsed = true; + + Vue.nextTick(done); + }); + + it('does not render text & svg', () => { + expect(vm.$el.querySelector('img')).toBeNull(); + expect(vm.$el.textContent).not.toContain('No changes'); + }); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js index 32dbc3bf72e..9af3c15a4e3 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js @@ -11,10 +11,17 @@ describe('Multi-file editor commit sidebar list collapsed', () => { beforeEach(() => { const Component = Vue.extend(listCollapsed); - vm = createComponentWithStore(Component, store); - - vm.$store.state.changedFiles.push(file('file1'), file('file2')); - vm.$store.state.changedFiles[0].tempFile = true; + vm = createComponentWithStore(Component, store, { + files: [ + { + ...file('file1'), + tempFile: true, + }, + file('file2'), + ], + iconName: 'staged', + title: 'Staged', + }); vm.$mount(); }); @@ -26,4 +33,40 @@ describe('Multi-file editor commit sidebar list collapsed', () => { it('renders added & modified files count', () => { expect(removeWhitespace(vm.$el.textContent).trim()).toBe('1 1'); }); + + describe('addedFilesLength', () => { + it('returns an length of temp files', () => { + expect(vm.addedFilesLength).toBe(1); + }); + }); + + describe('modifiedFilesLength', () => { + it('returns an length of modified files', () => { + expect(vm.modifiedFilesLength).toBe(1); + }); + }); + + describe('addedFilesIconClass', () => { + it('includes multi-file-addition when addedFiles is not empty', () => { + expect(vm.addedFilesIconClass).toContain('multi-file-addition'); + }); + + it('excludes multi-file-addition when addedFiles is empty', () => { + vm.files = []; + + expect(vm.addedFilesIconClass).not.toContain('multi-file-addition'); + }); + }); + + describe('modifiedFilesClass', () => { + it('includes multi-file-modified when addedFiles is not empty', () => { + expect(vm.modifiedFilesClass).toContain('multi-file-modified'); + }); + + it('excludes multi-file-modified when addedFiles is empty', () => { + vm.files = []; + + expect(vm.modifiedFilesClass).not.toContain('multi-file-modified'); + }); + }); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js index 509434e4300..cc7e0a3f26d 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; +import store from '~/ide/stores'; import listItem from '~/ide/components/commit_sidebar/list_item.vue'; import router from '~/ide/ide_router'; -import store from '~/ide/stores'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { file, resetStore } from '../../helpers'; @@ -18,6 +18,7 @@ describe('Multi-file editor commit sidebar list item', () => { vm = createComponentWithStore(Component, store, { file: f, + actionComponent: 'stage-button', }).$mount(); }); @@ -31,22 +32,18 @@ describe('Multi-file editor commit sidebar list item', () => { expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path); }); - it('calls discardFileChanges when clicking discard button', () => { - spyOn(vm, 'discardFileChanges'); - - vm.$el.querySelector('.multi-file-discard-btn').click(); - - expect(vm.discardFileChanges).toHaveBeenCalled(); + it('renders actionn button', () => { + expect(vm.$el.querySelector('.multi-file-discard-btn')).not.toBeNull(); }); it('opens a closed file in the editor when clicking the file path', done => { - spyOn(vm, 'openFileInEditor').and.callThrough(); + spyOn(vm, 'openPendingTab').and.callThrough(); spyOn(router, 'push'); vm.$el.querySelector('.multi-file-commit-list-path').click(); setTimeout(() => { - expect(vm.openFileInEditor).toHaveBeenCalled(); + expect(vm.openPendingTab).toHaveBeenCalled(); expect(router.push).toHaveBeenCalled(); done(); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_spec.js index 4c273ff2d6e..da1bc102b94 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import store from '~/ide/stores'; import commitSidebarList from '~/ide/components/commit_sidebar/list.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { file } from '../../helpers'; +import { file, resetStore } from '../../helpers'; describe('Multi-file editor commit sidebar list', () => { let vm; @@ -13,6 +13,10 @@ describe('Multi-file editor commit sidebar list', () => { vm = createComponentWithStore(Component, store, { title: 'Staged', fileList: [], + iconName: 'staged', + action: 'stageAllChanges', + actionBtnText: 'stage all', + itemActionComponent: 'stage-button', }); vm.$store.state.rightPanelCollapsed = false; @@ -22,6 +26,8 @@ describe('Multi-file editor commit sidebar list', () => { afterEach(() => { vm.$destroy(); + + resetStore(vm.$store); }); describe('with a list of files', () => { @@ -38,6 +44,12 @@ describe('Multi-file editor commit sidebar list', () => { }); }); + describe('empty files array', () => { + it('renders no changes text when empty', () => { + expect(vm.$el.textContent).toContain('No changes'); + }); + }); + describe('collapsed', () => { beforeEach(done => { vm.$store.state.rightPanelCollapsed = true; @@ -50,4 +62,32 @@ describe('Multi-file editor commit sidebar list', () => { expect(vm.$el.querySelector('.form-text.text-muted')).toBeNull(); }); }); + + describe('with toggle', () => { + beforeEach(done => { + spyOn(vm, 'toggleRightPanelCollapsed'); + + vm.showToggle = true; + + Vue.nextTick(done); + }); + + it('calls setPanelCollapsedStatus when clickin toggle', () => { + vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click(); + + expect(vm.toggleRightPanelCollapsed).toHaveBeenCalled(); + }); + }); + + describe('action button', () => { + beforeEach(() => { + spyOn(vm, 'stageAllChanges'); + }); + + it('calls store action when clicked', () => { + vm.$el.querySelector('.ide-staged-action-btn').click(); + + expect(vm.stageAllChanges).toHaveBeenCalled(); + }); + }); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js new file mode 100644 index 00000000000..6bf8710bda7 --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js @@ -0,0 +1,46 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import stageButton from '~/ide/components/commit_sidebar/stage_button.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { file, resetStore } from '../../helpers'; + +describe('IDE stage file button', () => { + let vm; + let f; + + beforeEach(() => { + const Component = Vue.extend(stageButton); + f = file(); + + vm = createComponentWithStore(Component, store, { + path: f.path, + }); + + spyOn(vm, 'stageChange'); + spyOn(vm, 'discardFileChanges'); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders button to discard & stage', () => { + expect(vm.$el.querySelectorAll('.btn').length).toBe(2); + }); + + it('calls store with stage button', () => { + vm.$el.querySelectorAll('.btn')[0].click(); + + expect(vm.stageChange).toHaveBeenCalledWith(f.path); + }); + + it('calls store with discard button', () => { + vm.$el.querySelectorAll('.btn')[1].click(); + + expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js new file mode 100644 index 00000000000..917bbb9fb46 --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import unstageButton from '~/ide/components/commit_sidebar/unstage_button.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { file, resetStore } from '../../helpers'; + +describe('IDE unstage file button', () => { + let vm; + let f; + + beforeEach(() => { + const Component = Vue.extend(unstageButton); + f = file(); + + vm = createComponentWithStore(Component, store, { + path: f.path, + }); + + spyOn(vm, 'unstageChange'); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders button to unstage', () => { + expect(vm.$el.querySelectorAll('.btn').length).toBe(1); + }); + + it('calls store with unnstage button', () => { + vm.$el.querySelector('.btn').click(); + + expect(vm.unstageChange).toHaveBeenCalledWith(f.path); + }); +}); diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js index 5184b743640..f2b9c14c11c 100644 --- a/spec/javascripts/ide/components/repo_commit_section_spec.js +++ b/spec/javascripts/ide/components/repo_commit_section_spec.js @@ -28,16 +28,34 @@ describe('RepoCommitSection', () => { }, }; + const files = [file('file1'), file('file2')].map(f => + Object.assign(f, { + type: 'blob', + }), + ); + vm.$store.state.rightPanelCollapsed = false; vm.$store.state.currentBranch = 'master'; - vm.$store.state.changedFiles = [file('file1'), file('file2')]; + vm.$store.state.changedFiles = [...files]; vm.$store.state.changedFiles.forEach(f => Object.assign(f, { changed: true, + content: 'changedFile testing', + }), + ); + + vm.$store.state.stagedFiles = [{ ...files[0] }, { ...files[1] }]; + vm.$store.state.stagedFiles.forEach(f => + Object.assign(f, { + changed: true, content: 'testing', }), ); + vm.$store.state.changedFiles.forEach(f => { + vm.$store.state.entries[f.path] = f; + }); + return vm.$mount(); } @@ -94,20 +112,93 @@ describe('RepoCommitSection', () => { ...vm.$el.querySelectorAll('.multi-file-commit-list li'), ]; const submitCommit = vm.$el.querySelector('form .btn'); + const allFiles = vm.$store.state.changedFiles.concat( + vm.$store.state.stagedFiles, + ); expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull(); - expect(changedFileElements.length).toEqual(2); + expect(changedFileElements.length).toEqual(4); changedFileElements.forEach((changedFile, i) => { - expect(changedFile.textContent.trim()).toContain( - vm.$store.state.changedFiles[i].path, - ); + expect(changedFile.textContent.trim()).toContain(allFiles[i].path); }); expect(submitCommit.disabled).toBeTruthy(); expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull(); }); + it('adds changed files into staged files', done => { + vm.$el.querySelector('.ide-staged-action-btn').click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.ide-commit-list-container').textContent, + ).toContain('No changes'); + + done(); + }); + }); + + it('stages a single file', done => { + vm.$el.querySelector('.multi-file-discard-btn .btn').click(); + + Vue.nextTick(() => { + expect( + vm.$el + .querySelector('.ide-commit-list-container') + .querySelectorAll('li').length, + ).toBe(1); + + done(); + }); + }); + + it('discards a single file', done => { + vm.$el.querySelectorAll('.multi-file-discard-btn .btn')[1].click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.ide-commit-list-container').textContent, + ).not.toContain('file1'); + expect( + vm.$el + .querySelector('.ide-commit-list-container') + .querySelectorAll('li').length, + ).toBe(1); + + done(); + }); + }); + + it('removes all staged files', done => { + vm.$el.querySelectorAll('.ide-staged-action-btn')[1].click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelectorAll('.ide-commit-list-container')[1].textContent, + ).toContain('No changes'); + + done(); + }); + }); + + it('unstages a single file', done => { + vm.$el + .querySelectorAll('.multi-file-discard-btn')[2] + .querySelector('.btn') + .click(); + + Vue.nextTick(() => { + expect( + vm.$el + .querySelectorAll('.ide-commit-list-container')[1] + .querySelectorAll('li').length, + ).toBe(1); + + done(); + }); + }); + it('updates commitMessage in store on input', done => { const textarea = vm.$el.querySelector('textarea'); diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js index 310d222377f..b06a6c62a1c 100644 --- a/spec/javascripts/ide/components/repo_editor_spec.js +++ b/spec/javascripts/ide/components/repo_editor_spec.js @@ -200,7 +200,7 @@ describe('RepoEditor', () => { vm.setupEditor(); - expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file); + expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file, null); expect(vm.model).not.toBeNull(); }); @@ -222,7 +222,7 @@ describe('RepoEditor', () => { vm.setupEditor(); expect(vm.editor.onPositionChange).toHaveBeenCalled(); - expect(vm.model.events.size).toBe(1); + expect(vm.model.events.size).toBe(2); }); it('updates state when model content changed', done => { @@ -234,6 +234,20 @@ describe('RepoEditor', () => { done(); }); }); + + it('sets head model as staged file', () => { + spyOn(vm.editor, 'createModel').and.callThrough(); + + Editor.editorInstance.modelManager.dispose(); + + vm.$store.state.stagedFiles.push({ ...vm.file, key: 'staged' }); + vm.file.staged = true; + vm.file.key = `unstaged-${vm.file.key}`; + + vm.setupEditor(); + + expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file, vm.$store.state.stagedFiles[0]); + }); }); describe('editor updateDimensions', () => { diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js index 8fc2fccb64c..7a6c22b6d27 100644 --- a/spec/javascripts/ide/lib/common/model_spec.js +++ b/spec/javascripts/ide/lib/common/model_spec.js @@ -30,6 +30,19 @@ describe('Multi-file editor library model', () => { expect(model.baseModel).not.toBeNull(); }); + it('creates model with head file to compare against', () => { + const f = file('path'); + model.dispose(); + + model = new Model(monaco, f, { + ...f, + content: '123 testing', + }); + + expect(model.head).not.toBeNull(); + expect(model.getOriginalModel().getValue()).toBe('123 testing'); + }); + it('adds eventHub listener', () => { expect(eventHub.$on).toHaveBeenCalledWith( `editor.update.model.dispose.${model.file.key}`, @@ -70,13 +83,6 @@ describe('Multi-file editor library model', () => { }); describe('onChange', () => { - it('caches event by path', () => { - model.onChange(() => {}); - - expect(model.events.size).toBe(1); - expect(model.events.keys().next().value).toBe(model.file.key); - }); - it('calls callback on change', done => { const spy = jasmine.createSpy(); model.onChange(spy); @@ -119,5 +125,15 @@ describe('Multi-file editor library model', () => { jasmine.anything(), ); }); + + it('calls onDispose callback', () => { + const disposeSpy = jasmine.createSpy(); + + model.onDispose(disposeSpy); + + model.dispose(); + + expect(disposeSpy).toHaveBeenCalled(); + }); }); }); diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js index aec325e26a9..e1c4ca570b6 100644 --- a/spec/javascripts/ide/lib/decorations/controller_spec.js +++ b/spec/javascripts/ide/lib/decorations/controller_spec.js @@ -117,4 +117,33 @@ describe('Multi-file editor library decorations controller', () => { expect(controller.editorDecorations.size).toBe(0); }); }); + + describe('hasDecorations', () => { + it('returns true when decorations are cached', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + expect(controller.hasDecorations(model)).toBe(true); + }); + + it('returns false when no model decorations exist', () => { + expect(controller.hasDecorations(model)).toBe(false); + }); + }); + + describe('removeDecorations', () => { + beforeEach(() => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + controller.decorate(model); + }); + + it('removes cached decorations', () => { + expect(controller.decorations.size).not.toBe(0); + expect(controller.editorDecorations.size).not.toBe(0); + + controller.removeDecorations(model); + + expect(controller.decorations.size).toBe(0); + expect(controller.editorDecorations.size).toBe(0); + }); + }); }); diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js index ff73240734e..fd8ab3b4f1d 100644 --- a/spec/javascripts/ide/lib/diff/controller_spec.js +++ b/spec/javascripts/ide/lib/diff/controller_spec.js @@ -3,10 +3,7 @@ import monacoLoader from '~/ide/monaco_loader'; import editor from '~/ide/lib/editor'; import ModelManager from '~/ide/lib/common/model_manager'; import DecorationsController from '~/ide/lib/decorations/controller'; -import DirtyDiffController, { - getDiffChangeType, - getDecorator, -} from '~/ide/lib/diff/controller'; +import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller'; import { computeDiff } from '~/ide/lib/diff/diff'; import { file } from '../../helpers'; @@ -90,6 +87,14 @@ describe('Multi-file editor library dirty diff controller', () => { expect(model.onChange).toHaveBeenCalled(); }); + it('adds dispose event callback', () => { + spyOn(model, 'onDispose'); + + controller.attachModel(model); + + expect(model.onDispose).toHaveBeenCalled(); + }); + it('calls throttledComputeDiff on change', () => { spyOn(controller, 'throttledComputeDiff'); @@ -99,6 +104,12 @@ describe('Multi-file editor library dirty diff controller', () => { expect(controller.throttledComputeDiff).toHaveBeenCalled(); }); + + it('caches model', () => { + controller.attachModel(model); + + expect(controller.models.has(model.url)).toBe(true); + }); }); describe('computeDiff', () => { @@ -116,14 +127,22 @@ describe('Multi-file editor library dirty diff controller', () => { }); describe('reDecorate', () => { - it('calls decorations controller decorate', () => { + it('calls computeDiff when no decorations are cached', () => { + spyOn(controller, 'computeDiff'); + + controller.reDecorate(model); + + expect(controller.computeDiff).toHaveBeenCalledWith(model); + }); + + it('calls decorate when decorations are cached', () => { spyOn(controller.decorationsController, 'decorate'); + controller.decorationsController.decorations.set(model.url, 'test'); + controller.reDecorate(model); - expect(controller.decorationsController.decorate).toHaveBeenCalledWith( - model, - ); + expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model); }); }); @@ -133,16 +152,15 @@ describe('Multi-file editor library dirty diff controller', () => { controller.decorate({ data: { changes: [], path: model.path } }); - expect( - controller.decorationsController.addDecorations, - ).toHaveBeenCalledWith(model, 'dirtyDiff', jasmine.anything()); + expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith( + model, + 'dirtyDiff', + jasmine.anything(), + ); }); it('adds decorations into editor', () => { - const spy = spyOn( - controller.decorationsController.editor.instance, - 'deltaDecorations', - ); + const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations'); controller.decorate({ data: { changes: computeDiff('123', '1234'), path: model.path }, @@ -181,16 +199,22 @@ describe('Multi-file editor library dirty diff controller', () => { }); it('removes worker event listener', () => { - spyOn( - controller.dirtyDiffWorker, - 'removeEventListener', - ).and.callThrough(); + spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough(); controller.dispose(); - expect( - controller.dirtyDiffWorker.removeEventListener, - ).toHaveBeenCalledWith('message', jasmine.anything()); + expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith( + 'message', + jasmine.anything(), + ); + }); + + it('clears cached models', () => { + controller.attachModel(model); + + model.dispose(); + + expect(controller.models.size).toBe(0); }); }); }); diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js index 75e6f0f54ec..530bdfa2759 100644 --- a/spec/javascripts/ide/lib/editor_spec.js +++ b/spec/javascripts/ide/lib/editor_spec.js @@ -88,7 +88,7 @@ describe('Multi-file editor library', () => { instance.createModel('FILE'); - expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE'); + expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE', null); }); }); diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js index 479ed7ce49e..ce5c525bed7 100644 --- a/spec/javascripts/ide/stores/actions/file_spec.js +++ b/spec/javascripts/ide/stores/actions/file_spec.js @@ -1,9 +1,12 @@ import Vue from 'vue'; import store from '~/ide/stores'; +import * as actions from '~/ide/stores/actions/file'; +import * as types from '~/ide/stores/mutation_types'; import service from '~/ide/services'; import router from '~/ide/ide_router'; import eventHub from '~/ide/eventhub'; import { file, resetStore } from '../../helpers'; +import testAction from '../../../helpers/vuex_action_helper'; describe('IDE store file actions', () => { beforeEach(() => { @@ -402,6 +405,7 @@ describe('IDE store file actions', () => { beforeEach(() => { spyOn(eventHub, '$on'); + spyOn(eventHub, '$emit'); tmpFile = file(); tmpFile.content = 'testing'; @@ -460,6 +464,57 @@ describe('IDE store file actions', () => { }) .catch(done.fail); }); + + it('pushes route for active file', done => { + tmpFile.active = true; + store.state.openFiles.push(tmpFile); + + store + .dispatch('discardFileChanges', tmpFile.path) + .then(() => { + expect(router.push).toHaveBeenCalledWith(`/project${tmpFile.url}`); + + done(); + }) + .catch(done.fail); + }); + + it('emits eventHub event to dispose cached model', done => { + store + .dispatch('discardFileChanges', tmpFile.path) + .then(() => { + expect(eventHub.$emit).toHaveBeenCalled(); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('stageChange', () => { + it('calls STAGE_CHANGE with file path', done => { + testAction( + actions.stageChange, + 'path', + store.state, + [{ type: types.STAGE_CHANGE, payload: 'path' }], + [], + done, + ); + }); + }); + + describe('unstageChange', () => { + it('calls UNSTAGE_CHANGE with file path', done => { + testAction( + actions.unstageChange, + 'path', + store.state, + [{ type: types.UNSTAGE_CHANGE, payload: 'path' }], + [], + done, + ); + }); }); describe('openPendingTab', () => { @@ -476,7 +531,7 @@ describe('IDE store file actions', () => { it('makes file pending in openFiles', done => { store - .dispatch('openPendingTab', f) + .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) .then(() => { expect(store.state.openFiles[0].pending).toBe(true); }) @@ -486,7 +541,7 @@ describe('IDE store file actions', () => { it('returns true when opened', done => { store - .dispatch('openPendingTab', f) + .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) .then(added => { expect(added).toBe(true); }) @@ -498,7 +553,7 @@ describe('IDE store file actions', () => { store.state.currentBranchId = 'master'; store - .dispatch('openPendingTab', f) + .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) .then(() => { expect(router.push).toHaveBeenCalledWith('/project/123/tree/master/'); }) @@ -512,7 +567,7 @@ describe('IDE store file actions', () => { store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line store - .dispatch('openPendingTab', f) + .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) .then(() => { expect(scrollToTabSpy).toHaveBeenCalled(); store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line @@ -527,7 +582,7 @@ describe('IDE store file actions', () => { store.state.viewer = 'diff'; store - .dispatch('openPendingTab', f) + .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) .then(added => { expect(added).toBe(false); }) diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js index cec572f4507..22a7441ba92 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -1,7 +1,10 @@ import * as urlUtils from '~/lib/utils/url_utility'; import store from '~/ide/stores'; +import * as actions from '~/ide/stores/actions'; +import * as types from '~/ide/stores/mutation_types'; import router from '~/ide/ide_router'; import { resetStore, file } from '../helpers'; +import testAction from '../../helpers/vuex_action_helper'; describe('Multi-file store actions', () => { beforeEach(() => { @@ -191,9 +194,7 @@ describe('Multi-file store actions', () => { }) .then(f => { expect(f.tempFile).toBeTruthy(); - expect(store.state.trees['abcproject/mybranch'].tree.length).toBe( - 1, - ); + expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1); done(); }) @@ -292,6 +293,42 @@ describe('Multi-file store actions', () => { }); }); + describe('stageAllChanges', () => { + it('adds all files from changedFiles to stagedFiles', done => { + store.state.changedFiles.push(file(), file('new')); + + testAction( + actions.stageAllChanges, + null, + store.state, + [ + { type: types.STAGE_CHANGE, payload: store.state.changedFiles[0].path }, + { type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path }, + ], + [], + done, + ); + }); + }); + + describe('unstageAllChanges', () => { + it('removes all files from stagedFiles after unstaging', done => { + store.state.stagedFiles.push(file(), file('new')); + + testAction( + actions.unstageAllChanges, + null, + store.state, + [ + { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[0].path }, + { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[1].path }, + ], + [], + done, + ); + }); + }); + describe('updateViewer', () => { it('updates viewer state', done => { store diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js index 33733b97dff..8d04b83928c 100644 --- a/spec/javascripts/ide/stores/getters_spec.js +++ b/spec/javascripts/ide/stores/getters_spec.js @@ -37,19 +37,11 @@ describe('IDE store getters', () => { expect(modifiedFiles.length).toBe(1); expect(modifiedFiles[0].name).toBe('changed'); }); - }); - describe('addedFiles', () => { - it('returns a list of added files', () => { - localState.openFiles.push(file()); - localState.changedFiles.push(file('added')); - localState.changedFiles[0].changed = true; - localState.changedFiles[0].tempFile = true; + it('returns angle left when collapsed', () => { + localState.rightPanelCollapsed = true; - const modifiedFiles = getters.addedFiles(localState); - - expect(modifiedFiles.length).toBe(1); - expect(modifiedFiles[0].name).toBe('added'); + expect(getters.collapseButtonIcon(localState)).toBe('angle-double-left'); }); }); diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index 1946a0c547c..116967208e0 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -209,14 +209,14 @@ describe('IDE commit module actions', () => { }, }, }; - store.state.changedFiles.push(f, { + store.state.stagedFiles.push(f, { ...file('changedFile2'), changed: true, }); - store.state.openFiles = store.state.changedFiles; + store.state.openFiles = store.state.stagedFiles; - store.state.changedFiles.forEach(changedFile => { - store.state.entries[changedFile.path] = changedFile; + store.state.stagedFiles.forEach(stagedFile => { + store.state.entries[stagedFile.path] = stagedFile; }); }); @@ -248,19 +248,6 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); - it('removes all changed files', done => { - store - .dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }) - .then(() => { - expect(store.state.changedFiles.length).toBe(0); - }) - .then(done) - .catch(done.fail); - }); - it('sets files commit data', done => { store .dispatch('commit/updateFilesAfterCommit', { @@ -294,10 +281,10 @@ describe('IDE commit module actions', () => { branch, }) .then(() => { - expect(eventHub.$emit).toHaveBeenCalledWith( - `editor.update.model.content.${f.path}`, - f.content, - ); + expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.content.${f.key}`, { + content: f.content, + changed: false, + }); }) .then(done) .catch(done.fail); @@ -335,12 +322,22 @@ describe('IDE commit module actions', () => { }, }, }; - store.state.changedFiles.push(file('changed')); - store.state.changedFiles[0].active = true; + + const f = { + ...file('changed'), + type: 'blob', + active: true, + }; + store.state.stagedFiles.push(f); + store.state.changedFiles = [ + { + ...f, + }, + ]; store.state.openFiles = store.state.changedFiles; - store.state.openFiles.forEach(f => { - store.state.entries[f.path] = f; + store.state.openFiles.forEach(localF => { + store.state.entries[localF.path] = localF; }); store.state.commit.commitAction = '2'; @@ -420,11 +417,13 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); - it('adds commit data to changed files', done => { + it('adds commit data to files', done => { store .dispatch('commit/commitChanges') .then(() => { - expect(store.state.openFiles[0].lastCommit.message).toBe('test message'); + expect(store.state.entries[store.state.openFiles[0].path].lastCommit.message).toBe( + 'test message', + ); done(); }) @@ -443,6 +442,16 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); + it('removes all staged files', done => { + store + .dispatch('commit/commitChanges') + .then(() => { + expect(store.state.stagedFiles.length).toBe(0); + }) + .then(done) + .catch(done.fail); + }); + describe('merge request', () => { it('redirects to new merge request page', done => { spyOn(eventHub, '$on'); @@ -471,7 +480,7 @@ describe('IDE commit module actions', () => { store .dispatch('commit/commitChanges') .then(() => { - expect(store.state.changedFiles.length).toBe(0); + expect(store.state.stagedFiles.length).toBe(0); done(); }) diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js index e396284ec2c..55580f046ad 100644 --- a/spec/javascripts/ide/stores/modules/commit/getters_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js @@ -34,17 +34,17 @@ describe('IDE commit module getters', () => { discardDraftButtonDisabled: false, }; const rootState = { - changedFiles: ['a'], + stagedFiles: ['a'], }; - it('returns false when discardDraftButtonDisabled is false & changedFiles is not empty', () => { + it('returns false when discardDraftButtonDisabled is false & stagedFiles is not empty', () => { expect( getters.commitButtonDisabled(state, localGetters, rootState), ).toBeFalsy(); }); - it('returns true when discardDraftButtonDisabled is false & changedFiles is empty', () => { - rootState.changedFiles.length = 0; + it('returns true when discardDraftButtonDisabled is false & stagedFiles is empty', () => { + rootState.stagedFiles.length = 0; expect( getters.commitButtonDisabled(state, localGetters, rootState), @@ -61,7 +61,7 @@ describe('IDE commit module getters', () => { it('returns true when discardDraftButtonDisabled is false & changedFiles is not empty', () => { localGetters.discardDraftButtonDisabled = false; - rootState.changedFiles.length = 0; + rootState.stagedFiles.length = 0; expect( getters.commitButtonDisabled(state, localGetters, rootState), diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js index bf9d5166d0a..6fba934810d 100644 --- a/spec/javascripts/ide/stores/mutations/file_spec.js +++ b/spec/javascripts/ide/stores/mutations/file_spec.js @@ -8,7 +8,10 @@ describe('IDE store file mutations', () => { beforeEach(() => { localState = state(); - localFile = file(); + localFile = { + ...file(), + type: 'blob', + }; localState.entries[localFile.path] = localFile; }); @@ -183,6 +186,49 @@ describe('IDE store file mutations', () => { }); }); + describe('STAGE_CHANGE', () => { + it('adds file into stagedFiles array', () => { + mutations.STAGE_CHANGE(localState, localFile.path); + + expect(localState.stagedFiles.length).toBe(1); + expect(localState.stagedFiles[0]).toEqual(localFile); + }); + + it('updates stagedFile if it is already staged', () => { + mutations.STAGE_CHANGE(localState, localFile.path); + + localFile.raw = 'testing 123'; + + mutations.STAGE_CHANGE(localState, localFile.path); + + expect(localState.stagedFiles.length).toBe(1); + expect(localState.stagedFiles[0].raw).toEqual('testing 123'); + }); + }); + + describe('UNSTAGE_CHANGE', () => { + let f; + + beforeEach(() => { + f = { + ...file(), + type: 'blob', + staged: true, + }; + + localState.stagedFiles.push(f); + localState.changedFiles.push(f); + localState.entries[f.path] = f; + }); + + it('removes from stagedFiles array', () => { + mutations.UNSTAGE_CHANGE(localState, f.path); + + expect(localState.stagedFiles.length).toBe(0); + expect(localState.changedFiles.length).toBe(1); + }); + }); + describe('TOGGLE_FILE_CHANGED', () => { it('updates file changed status', () => { mutations.TOGGLE_FILE_CHANGED(localState, { diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js index 38162a470ad..26e7ed4535e 100644 --- a/spec/javascripts/ide/stores/mutations_spec.js +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -69,6 +69,16 @@ describe('Multi-file store mutations', () => { }); }); + describe('CLEAR_STAGED_CHANGES', () => { + it('clears stagedFiles array', () => { + localState.stagedFiles.push('a'); + + mutations.CLEAR_STAGED_CHANGES(localState); + + expect(localState.stagedFiles.length).toBe(0); + }); + }); + describe('UPDATE_VIEWER', () => { it('sets viewer state', () => { mutations.UPDATE_VIEWER(localState, 'diff'); diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js index 0961605ce5c..4f861c39d3f 100644 --- a/spec/javascripts/jobs/header_spec.js +++ b/spec/javascripts/jobs/header_spec.js @@ -36,14 +36,28 @@ describe('Job details header', () => { }, isLoading: false, }; - - vm = mountComponent(HeaderComponent, props); }); afterEach(() => { vm.$destroy(); }); + describe('job reason', () => { + it('should not render the reason when reason is absent', () => { + vm = mountComponent(HeaderComponent, props); + + expect(vm.shouldRenderReason).toBe(false); + }); + + it('should render the reason when reason is present', () => { + props.job.callout_message = 'There is an unknown failure, please try again'; + + vm = mountComponent(HeaderComponent, props); + + expect(vm.shouldRenderReason).toBe(true); + }); + }); + describe('triggered job', () => { beforeEach(() => { vm = mountComponent(HeaderComponent, props); @@ -51,14 +65,17 @@ describe('Job details header', () => { it('should render provided job information', () => { expect( - vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(), + vm.$el + .querySelector('.header-main-content') + .textContent.replace(/\s+/g, ' ') + .trim(), ).toEqual('failed Job #123 triggered 3 weeks ago by Foo'); }); it('should render new issue link', () => { - expect( - vm.$el.querySelector('.js-new-issue').getAttribute('href'), - ).toEqual(props.job.new_issue_path); + expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual( + props.job.new_issue_path, + ); }); }); @@ -68,7 +85,10 @@ describe('Job details header', () => { vm = mountComponent(HeaderComponent, props); expect( - vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(), + vm.$el + .querySelector('.header-main-content') + .textContent.replace(/\s+/g, ' ') + .trim(), ).toEqual('failed Job #123 created 3 weeks ago by Foo'); }); }); diff --git a/spec/javascripts/jobs/sidebar_details_block_spec.js b/spec/javascripts/jobs/sidebar_details_block_spec.js index 602dae514b1..6b397c22fb9 100644 --- a/spec/javascripts/jobs/sidebar_details_block_spec.js +++ b/spec/javascripts/jobs/sidebar_details_block_spec.js @@ -31,10 +31,25 @@ describe('Sidebar details block', () => { }); }); + describe("when user can't retry", () => { + it('should not render a retry button', () => { + vm = new SidebarComponent({ + propsData: { + job: {}, + canUserRetry: false, + isLoading: true, + }, + }).$mount(); + + expect(vm.$el.querySelector('.js-retry-job')).toBeNull(); + }); + }); + beforeEach(() => { vm = new SidebarComponent({ propsData: { job, + canUserRetry: true, isLoading: false, }, }).$mount(); @@ -42,7 +57,9 @@ describe('Sidebar details block', () => { describe('actions', () => { it('should render link to new issue', () => { - expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(job.new_issue_path); + expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual( + job.new_issue_path, + ); expect(vm.$el.querySelector('.js-new-issue').textContent.trim()).toEqual('New issue'); }); @@ -57,43 +74,35 @@ describe('Sidebar details block', () => { describe('information', () => { it('should render merge request link', () => { - expect( - trimWhitespace(vm.$el.querySelector('.js-job-mr')), - ).toEqual('Merge Request: !2'); + expect(trimWhitespace(vm.$el.querySelector('.js-job-mr'))).toEqual('Merge Request: !2'); - expect( - vm.$el.querySelector('.js-job-mr a').getAttribute('href'), - ).toEqual(job.merge_request.path); + expect(vm.$el.querySelector('.js-job-mr a').getAttribute('href')).toEqual( + job.merge_request.path, + ); }); it('should render job duration', () => { - expect( - trimWhitespace(vm.$el.querySelector('.js-job-duration')), - ).toEqual('Duration: 6 seconds'); + expect(trimWhitespace(vm.$el.querySelector('.js-job-duration'))).toEqual( + 'Duration: 6 seconds', + ); }); it('should render erased date', () => { - expect( - trimWhitespace(vm.$el.querySelector('.js-job-erased')), - ).toEqual('Erased: 3 weeks ago'); + expect(trimWhitespace(vm.$el.querySelector('.js-job-erased'))).toEqual('Erased: 3 weeks ago'); }); it('should render finished date', () => { - expect( - trimWhitespace(vm.$el.querySelector('.js-job-finished')), - ).toEqual('Finished: 3 weeks ago'); + expect(trimWhitespace(vm.$el.querySelector('.js-job-finished'))).toEqual( + 'Finished: 3 weeks ago', + ); }); it('should render queued date', () => { - expect( - trimWhitespace(vm.$el.querySelector('.js-job-queued')), - ).toEqual('Queued: 9 seconds'); + expect(trimWhitespace(vm.$el.querySelector('.js-job-queued'))).toEqual('Queued: 9 seconds'); }); it('should render runner ID', () => { - expect( - trimWhitespace(vm.$el.querySelector('.js-job-runner')), - ).toEqual('Runner: #1'); + expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual('Runner: #1'); }); it('should render timeout information', () => { @@ -103,15 +112,11 @@ describe('Sidebar details block', () => { }); it('should render coverage', () => { - expect( - trimWhitespace(vm.$el.querySelector('.js-job-coverage')), - ).toEqual('Coverage: 20%'); + expect(trimWhitespace(vm.$el.querySelector('.js-job-coverage'))).toEqual('Coverage: 20%'); }); it('should render tags', () => { - expect( - trimWhitespace(vm.$el.querySelector('.js-job-tags')), - ).toEqual('Tags: tag'); + expect(trimWhitespace(vm.$el.querySelector('.js-job-tags'))).toEqual('Tags: tag'); }); }); }); diff --git a/spec/javascripts/vue_shared/components/callout_spec.js b/spec/javascripts/vue_shared/components/callout_spec.js new file mode 100644 index 00000000000..e62bd86f4ca --- /dev/null +++ b/spec/javascripts/vue_shared/components/callout_spec.js @@ -0,0 +1,45 @@ +import Vue from 'vue'; +import callout from '~/vue_shared/components/callout.vue'; +import createComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('Callout Component', () => { + let CalloutComponent; + let vm; + const exampleMessage = 'This is a callout message!'; + + beforeEach(() => { + CalloutComponent = Vue.extend(callout); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render the appropriate variant of callout', () => { + vm = createComponent(CalloutComponent, { + category: 'info', + message: exampleMessage, + }); + + expect(vm.$el.getAttribute('class')).toEqual('bs-callout bs-callout-info'); + + expect(vm.$el.tagName).toEqual('DIV'); + }); + + it('should render accessibility attributes', () => { + vm = createComponent(CalloutComponent, { + message: exampleMessage, + }); + + expect(vm.$el.getAttribute('role')).toEqual('alert'); + expect(vm.$el.getAttribute('aria-live')).toEqual('assertive'); + }); + + it('should render the provided message', () => { + vm = createComponent(CalloutComponent, { + message: exampleMessage, + }); + + expect(vm.$el.innerHTML.trim()).toEqual(exampleMessage); + }); +}); diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb index 761f7732036..722d697c28e 100644 --- a/spec/lib/gitlab/git/wiki_spec.rb +++ b/spec/lib/gitlab/git/wiki_spec.rb @@ -30,7 +30,7 @@ describe Gitlab::Git::Wiki do end def commit_details(name) - Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "created page #{name}") + Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "created page #{name}") end def destroy_page(title, dir = '') diff --git a/spec/lib/gitlab/view/presenter/base_spec.rb b/spec/lib/gitlab/view/presenter/base_spec.rb index 32a946ca034..4eca53032a2 100644 --- a/spec/lib/gitlab/view/presenter/base_spec.rb +++ b/spec/lib/gitlab/view/presenter/base_spec.rb @@ -48,4 +48,11 @@ describe Gitlab::View::Presenter::Base do end end end + + describe '#present' do + it 'returns self' do + presenter = presenter_class.new(build_stubbed(:project)) + expect(presenter.present).to eq(presenter) + end + end end diff --git a/spec/lib/gitlab/wiki/committer_with_hooks_spec.rb b/spec/lib/gitlab/wiki/committer_with_hooks_spec.rb new file mode 100644 index 00000000000..830fb8a8598 --- /dev/null +++ b/spec/lib/gitlab/wiki/committer_with_hooks_spec.rb @@ -0,0 +1,154 @@ +require 'spec_helper' + +describe Gitlab::Wiki::CommitterWithHooks, seed_helper: true do + shared_examples 'calling wiki hooks' do + let(:project) { create(:project) } + let(:user) { project.owner } + let(:project_wiki) { ProjectWiki.new(project, user) } + let(:wiki) { project_wiki.wiki } + let(:options) do + { + id: user.id, + username: user.username, + name: user.name, + email: user.email, + message: 'commit message' + } + end + + subject { described_class.new(wiki, options) } + + before do + project_wiki.create_page('home', 'test content') + end + + shared_examples 'failing pre-receive hook' do + before do + expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([false, '']) + expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('update') + expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive') + end + + it 'raises exception' do + expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError) + end + + it 'does not create a new commit inside the repository' do + current_rev = find_current_rev + + expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError) + + expect(current_rev).to eq find_current_rev + end + end + + shared_examples 'failing update hook' do + before do + expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, '']) + expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([false, '']) + expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive') + end + + it 'raises exception' do + expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError) + end + + it 'does not create a new commit inside the repository' do + current_rev = find_current_rev + + expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError) + + expect(current_rev).to eq find_current_rev + end + end + + shared_examples 'failing post-receive hook' do + before do + expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, '']) + expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([true, '']) + expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('post-receive').and_return([false, '']) + end + + it 'does not raise exception' do + expect { subject.commit }.not_to raise_error + end + + it 'creates the commit' do + current_rev = find_current_rev + + subject.commit + + expect(current_rev).not_to eq find_current_rev + end + end + + shared_examples 'when hooks call succceeds' do + let(:hook) { double(:hook) } + + it 'calls the three hooks' do + expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) + expect(hook).to receive(:trigger).exactly(3).times.and_return([true, nil]) + + subject.commit + end + + it 'creates the commit' do + current_rev = find_current_rev + + subject.commit + + expect(current_rev).not_to eq find_current_rev + end + end + + context 'when creating a page' do + before do + project_wiki.create_page('index', 'test content') + end + + it_behaves_like 'failing pre-receive hook' + it_behaves_like 'failing update hook' + it_behaves_like 'failing post-receive hook' + it_behaves_like 'when hooks call succceeds' + end + + context 'when updating a page' do + before do + project_wiki.update_page(find_page('home'), content: 'some other content', format: :markdown) + end + + it_behaves_like 'failing pre-receive hook' + it_behaves_like 'failing update hook' + it_behaves_like 'failing post-receive hook' + it_behaves_like 'when hooks call succceeds' + end + + context 'when deleting a page' do + before do + project_wiki.delete_page(find_page('home')) + end + + it_behaves_like 'failing pre-receive hook' + it_behaves_like 'failing update hook' + it_behaves_like 'failing post-receive hook' + it_behaves_like 'when hooks call succceeds' + end + + def find_current_rev + wiki.gollum_wiki.repo.commits.first&.sha + end + + def find_page(name) + wiki.page(title: name) + end + end + + # TODO: Uncomment once Gitaly updates the ruby vendor code + # context 'when Gitaly is enabled' do + # it_behaves_like 'calling wiki hooks' + # end + + context 'when Gitaly is disabled', :skip_gitaly_mock do + it_behaves_like 'calling wiki hooks' + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index d7075d4a6b4..fcdc31c8984 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1384,29 +1384,51 @@ describe Ci::Build do end end - describe '#update_project_statistics' do - let!(:build) { create(:ci_build, artifacts_size: 23) } - - it 'updates project statistics when the artifact size changes' do - expect(ProjectCacheWorker).to receive(:perform_async) - .with(build.project_id, [], [:build_artifacts_size]) + context 'when updating the build' do + let(:build) { create(:ci_build, artifacts_size: 23) } + it 'updates project statistics' do build.artifacts_size = 42 - build.save! + + expect(build).to receive(:update_project_statistics_after_save).and_call_original + + expect { build.save! } + .to change { build.project.statistics.reload.build_artifacts_size } + .by(19) end - it 'does not update project statistics when the artifact size stays the same' do - expect(ProjectCacheWorker).not_to receive(:perform_async) + context 'when the artifact size stays the same' do + it 'does not update project statistics' do + build.name = 'changed' - build.name = 'changed' - build.save! + expect(build).not_to receive(:update_project_statistics_after_save) + + build.save! + end end + end - it 'updates project statistics when the build is destroyed' do - expect(ProjectCacheWorker).to receive(:perform_async) - .with(build.project_id, [], [:build_artifacts_size]) + context 'when destroying the build' do + let!(:build) { create(:ci_build, artifacts_size: 23) } + + it 'updates project statistics' do + expect(ProjectStatistics) + .to receive(:increment_statistic) + .and_call_original + + expect { build.destroy! } + .to change { build.project.statistics.reload.build_artifacts_size } + .by(-23) + end + + context 'when the build is destroyed due to the project being destroyed' do + it 'does not update the project statistics' do + expect(ProjectStatistics) + .not_to receive(:increment_statistic) - build.destroy + build.project.update_attributes(pending_delete: true) + build.project.destroy! + end end end @@ -1472,7 +1494,7 @@ describe Ci::Build do { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false }, { key: 'CI', value: 'true', public: true }, { key: 'GITLAB_CI', value: 'true', public: true }, - { key: 'GITLAB_FEATURES', value: project.namespace.features.join(','), public: true }, + { key: 'GITLAB_FEATURES', value: project.licensed_features.join(','), public: true }, { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 1aa28434879..a3e119cbc27 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::JobArtifact do - set(:artifact) { create(:ci_job_artifact, :archive) } + let(:artifact) { create(:ci_job_artifact, :archive) } describe "Associations" do it { is_expected.to belong_to(:project) } @@ -59,10 +59,32 @@ describe Ci::JobArtifact do end end - describe '#set_size' do - it 'sets the size' do + context 'creating the artifact' do + let(:project) { create(:project) } + let(:artifact) { create(:ci_job_artifact, :archive, project: project) } + + it 'sets the size from the file size' do expect(artifact.size).to eq(106365) end + + it 'updates the project statistics' do + expect { artifact } + .to change { project.statistics.reload.build_artifacts_size } + .by(106365) + end + end + + context 'updating the artifact file' do + it 'updates the artifact size' do + artifact.update!(file: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) + expect(artifact.size).to eq(1062) + end + + it 'updates the project statistics' do + expect { artifact.update!(file: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) } + .to change { artifact.project.statistics.reload.build_artifacts_size } + .by(1062 - 106365) + end end describe '#file' do @@ -118,4 +140,71 @@ describe Ci::JobArtifact do is_expected.to be_nil end end + + context 'when destroying the artifact' do + let(:project) { create(:project, :repository) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + + it 'updates the project statistics' do + artifact = build.job_artifacts.first + + expect(ProjectStatistics) + .to receive(:increment_statistic) + .and_call_original + + expect { artifact.destroy } + .to change { project.statistics.reload.build_artifacts_size } + .by(-106365) + end + + context 'when it is destroyed from the project level' do + it 'does not update the project statistics' do + expect(ProjectStatistics) + .not_to receive(:increment_statistic) + + project.update_attributes(pending_delete: true) + project.destroy! + end + end + end + + describe 'file is being stored' do + subject { create(:ci_job_artifact, :archive) } + + context 'when object has nil store' do + before do + subject.update_column(:file_store, nil) + subject.reload + end + + it 'is stored locally' do + expect(subject.file_store).to be(nil) + expect(subject.file).to be_file_storage + expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL) + end + end + + context 'when existing object has local store' do + it 'is stored locally' do + expect(subject.file_store).to be(ObjectStorage::Store::LOCAL) + expect(subject.file).to be_file_storage + expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL) + end + end + + context 'when direct upload is enabled' do + before do + stub_artifacts_object_storage(direct_upload: true) + end + + context 'when file is stored' do + it 'is stored remotely' do + expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE) + expect(subject.file).not_to be_file_storage + expect(subject.file.object_store).to eq(ObjectStorage::Store::REMOTE) + end + end + end + end end diff --git a/spec/models/concerns/group_descendant_spec.rb b/spec/models/concerns/group_descendant_spec.rb index c163fb01a81..28352d8c961 100644 --- a/spec/models/concerns/group_descendant_spec.rb +++ b/spec/models/concerns/group_descendant_spec.rb @@ -79,9 +79,24 @@ describe GroupDescendant, :nested_groups do expect(described_class.build_hierarchy(groups)).to eq(expected_hierarchy) end + it 'tracks the exception when a parent was not preloaded' do + expect(Gitlab::Sentry).to receive(:track_exception).and_call_original + + expect { GroupDescendant.build_hierarchy([subsub_group]) }.to raise_error(ArgumentError) + end + + it 'recovers if a parent was not reloaded by querying for the parent' do + expected_hierarchy = { parent => { subgroup => subsub_group } } + + # this does not raise in production, so stubbing it here. + allow(Gitlab::Sentry).to receive(:track_exception) + + expect(GroupDescendant.build_hierarchy([subsub_group])).to eq(expected_hierarchy) + end + it 'raises an error if not all elements were preloaded' do expect { described_class.build_hierarchy([subsub_group]) } - .to raise_error('parent was not preloaded') + .to raise_error(/was not preloaded/) end end end diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb index a182116d637..ba06ff42d87 100644 --- a/spec/models/lfs_object_spec.rb +++ b/spec/models/lfs_object_spec.rb @@ -81,5 +81,44 @@ describe LfsObject do end end end + + describe 'file is being stored' do + let(:lfs_object) { create(:lfs_object, :with_file) } + + context 'when object has nil store' do + before do + lfs_object.update_column(:file_store, nil) + lfs_object.reload + end + + it 'is stored locally' do + expect(lfs_object.file_store).to be(nil) + expect(lfs_object.file).to be_file_storage + expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::LOCAL) + end + end + + context 'when existing object has local store' do + it 'is stored locally' do + expect(lfs_object.file_store).to be(ObjectStorage::Store::LOCAL) + expect(lfs_object.file).to be_file_storage + expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::LOCAL) + end + end + + context 'when direct upload is enabled' do + before do + stub_lfs_object_storage(direct_upload: true) + end + + context 'when file is stored' do + it 'is stored remotely' do + expect(lfs_object.file_store).to eq(ObjectStorage::Store::REMOTE) + expect(lfs_object.file).not_to be_file_storage + expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::REMOTE) + end + end + end + end end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 47f4a792e5c..c7460981a32 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -96,7 +96,9 @@ describe Milestone do allow(milestone).to receive(:due_date).and_return(Date.today.prev_year) end - it { expect(milestone.expired?).to be_truthy } + it 'returns true when due_date is in the past' do + expect(milestone.expired?).to be_truthy + end end context "not expired" do @@ -104,17 +106,19 @@ describe Milestone do allow(milestone).to receive(:due_date).and_return(Date.today.next_year) end - it { expect(milestone.expired?).to be_falsey } + it 'returns false when due_date is in the future' do + expect(milestone.expired?).to be_falsey + end end end describe '#upcoming?' do - it 'returns true' do + it 'returns true when start_date is in the future' do milestone = build(:milestone, start_date: Time.now + 1.month) expect(milestone.upcoming?).to be_truthy end - it 'returns false' do + it 'returns false when start_date is in the past' do milestone = build(:milestone, start_date: Date.today.prev_year) expect(milestone.upcoming?).to be_falsey end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 86962cd8d61..6a6c71e6c82 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -91,6 +91,23 @@ describe Note do it "keeps the commit around" do expect(note.project.repository.kept_around?(commit.id)).to be_truthy end + + it 'does not generate N+1 queries for participants', :request_store do + def retrieve_participants + commit.notes_with_associations.map(&:participants).to_a + end + + # Project authorization checks are cached, establish a baseline + retrieve_participants + + control_count = ActiveRecord::QueryRecorder.new do + retrieve_participants + end + + create(:note_on_commit, project: note.project, note: 'another note', noteable_id: commit.id) + + expect { retrieve_participants }.not_to exceed_query_limit(control_count) + end end describe 'authorization' do diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb index 5cff2af4aca..38a3590ad12 100644 --- a/spec/models/project_statistics_spec.rb +++ b/spec/models/project_statistics_spec.rb @@ -4,26 +4,6 @@ describe ProjectStatistics do let(:project) { create :project } let(:statistics) { project.statistics } - describe 'constants' do - describe 'STORAGE_COLUMNS' do - it 'is an array of symbols' do - expect(described_class::STORAGE_COLUMNS).to be_kind_of Array - expect(described_class::STORAGE_COLUMNS.map(&:class).uniq).to eq [Symbol] - end - end - - describe 'STATISTICS_COLUMNS' do - it 'is an array of symbols' do - expect(described_class::STATISTICS_COLUMNS).to be_kind_of Array - expect(described_class::STATISTICS_COLUMNS.map(&:class).uniq).to eq [Symbol] - end - - it 'includes all storage columns' do - expect(described_class::STATISTICS_COLUMNS & described_class::STORAGE_COLUMNS).to eq described_class::STORAGE_COLUMNS - end - end - end - describe 'associations' do it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:namespace) } @@ -63,7 +43,6 @@ describe ProjectStatistics do allow(statistics).to receive(:update_commit_count) allow(statistics).to receive(:update_repository_size) allow(statistics).to receive(:update_lfs_objects_size) - allow(statistics).to receive(:update_build_artifacts_size) allow(statistics).to receive(:update_storage_size) end @@ -76,7 +55,6 @@ describe ProjectStatistics do expect(statistics).to have_received(:update_commit_count) expect(statistics).to have_received(:update_repository_size) expect(statistics).to have_received(:update_lfs_objects_size) - expect(statistics).to have_received(:update_build_artifacts_size) end end @@ -89,7 +67,6 @@ describe ProjectStatistics do expect(statistics).to have_received(:update_lfs_objects_size) expect(statistics).not_to have_received(:update_commit_count) expect(statistics).not_to have_received(:update_repository_size) - expect(statistics).not_to have_received(:update_build_artifacts_size) end end end @@ -131,40 +108,6 @@ describe ProjectStatistics do end end - describe '#update_build_artifacts_size' do - let!(:pipeline) { create(:ci_pipeline, project: project) } - - context 'when new job artifacts are calculated' do - let(:ci_build) { create(:ci_build, pipeline: pipeline) } - - before do - create(:ci_job_artifact, :archive, project: pipeline.project, job: ci_build) - end - - it "stores the size of related build artifacts" do - statistics.update_build_artifacts_size - - expect(statistics.build_artifacts_size).to be(106365) - end - - it 'calculates related build artifacts by project' do - expect(Ci::JobArtifact).to receive(:artifacts_size_for).with(project) { 0 } - - statistics.update_build_artifacts_size - end - end - - context 'when legacy artifacts are used' do - let!(:ci_build) { create(:ci_build, pipeline: pipeline, artifacts_size: 10.megabytes) } - - it "stores the size of related build artifacts" do - statistics.update_build_artifacts_size - - expect(statistics.build_artifacts_size).to eq(10.megabytes) - end - end - end - describe '#update_storage_size' do it "sums all storage counters" do statistics.update!( @@ -177,4 +120,27 @@ describe ProjectStatistics do expect(statistics.storage_size).to eq 5 end end + + describe '.increment_statistic' do + it 'increases the statistic by that amount' do + expect { described_class.increment_statistic(project.id, :build_artifacts_size, 13) } + .to change { statistics.reload.build_artifacts_size } + .by(13) + end + + context 'when the amount is 0' do + it 'does not execute a query' do + project + expect { described_class.increment_statistic(project.id, :build_artifacts_size, 0) } + .not_to exceed_query_limit(0) + end + end + + context 'when using an invalid column' do + it 'raises an error' do + expect { described_class.increment_statistic(project.id, :id, 13) } + .to raise_error(ArgumentError, "Cannot increment attribute: id") + end + end + end end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 374a157bec0..4e83f4353cf 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -377,7 +377,7 @@ describe ProjectWiki do end def commit_details - Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit") + Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "test commit") end def create_page(name, content) diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index b2b7721674c..90b7e7715a8 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -561,7 +561,7 @@ describe WikiPage do end def commit_details - Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit") + Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "test commit") end def create_page(name, content) diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb index cc16d0f156b..4bc005df2fc 100644 --- a/spec/presenters/ci/build_presenter_spec.rb +++ b/spec/presenters/ci/build_presenter_spec.rb @@ -217,4 +217,39 @@ describe Ci::BuildPresenter do end end end + + describe '#callout_failure_message' do + let(:build) { create(:ci_build, :failed, :script_failure) } + + it 'returns a verbose failure reason' do + description = subject.callout_failure_message + expect(description).to eq('There has been a script failure. Check the job log for more information') + end + end + + describe '#recoverable?' do + let(:build) { create(:ci_build, :failed, :script_failure) } + + context 'when is a script or missing dependency failure' do + let(:failure_reasons) { %w(script_failure missing_dependency_failure) } + + it 'should return false' do + failure_reasons.each do |failure_reason| + build.update_attribute(:failure_reason, failure_reason) + expect(presenter.recoverable?).to be_falsy + end + end + end + + context 'when is any other failure type' do + let(:failure_reasons) { %w(unknown_failure api_failure stuck_or_timeout_failure runner_system_failure) } + + it 'should return true' do + failure_reasons.each do |failure_reason| + build.update_attribute(:failure_reason, failure_reason) + expect(presenter.recoverable?).to be_truthy + end + end + end + end end diff --git a/spec/rubocop/cop/gitlab/has_many_through_scope_spec.rb b/spec/rubocop/cop/gitlab/has_many_through_scope_spec.rb deleted file mode 100644 index 6d769c8e6fd..00000000000 --- a/spec/rubocop/cop/gitlab/has_many_through_scope_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -require 'spec_helper' - -require 'rubocop' -require 'rubocop/rspec/support' - -require_relative '../../../../rubocop/cop/gitlab/has_many_through_scope' - -describe RuboCop::Cop::Gitlab::HasManyThroughScope do # rubocop:disable RSpec/FilePath - include CopHelper - - subject(:cop) { described_class.new } - - context 'in a model file' do - before do - allow(cop).to receive(:in_model?).and_return(true) - end - - context 'when the model does not use has_many :through' do - it 'does not register an offense' do - expect_no_offenses(<<-RUBY) - class User < ActiveRecord::Base - has_many :tags, source: 'UserTag' - end - RUBY - end - end - - context 'when the model uses has_many :through' do - context 'when the association has no scope defined' do - it 'registers an offense on the association' do - expect_offense(<<-RUBY) - class User < ActiveRecord::Base - has_many :tags, through: :user_tags - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG} - end - RUBY - end - end - - context 'when the association has a scope defined' do - context 'when the scope does not disable auto-loading' do - it 'registers an offense on the scope' do - expect_offense(<<-RUBY) - class User < ActiveRecord::Base - has_many :tags, -> { where(active: true) }, through: :user_tags - ^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG} - end - RUBY - end - end - - context 'when the scope has auto_include(false)' do - it 'does not register an offense' do - expect_no_offenses(<<-RUBY) - class User < ActiveRecord::Base - has_many :tags, -> { where(active: true).auto_include(false).reorder(nil) }, through: :user_tags - end - RUBY - end - end - end - end - end - - context 'outside of a migration spec file' do - it 'does not register an offense' do - expect_no_offenses(<<-RUBY) - class User < ActiveRecord::Base - has_many :tags, through: :user_tags - end - RUBY - end - end -end diff --git a/spec/serializers/entity_date_helper_spec.rb b/spec/serializers/entity_date_helper_spec.rb index b9cc2f64831..36da8d33a44 100644 --- a/spec/serializers/entity_date_helper_spec.rb +++ b/spec/serializers/entity_date_helper_spec.rb @@ -32,6 +32,7 @@ describe EntityDateHelper do end it 'converts 86560 seconds' do + Rails.logger.debug date_helper_class.inspect expect(date_helper_class.distance_of_time_as_hash(86560)).to eq(days: 1, mins: 2, seconds: 40) end @@ -42,4 +43,58 @@ describe EntityDateHelper do it 'converts 986760 seconds' do expect(date_helper_class.distance_of_time_as_hash(986760)).to eq(days: 11, hours: 10, mins: 6) end + + describe '#remaining_days_in_words' do + around do |example| + Timecop.freeze(Time.utc(2017, 3, 17)) { example.run } + end + + context 'when less than 31 days remaining' do + let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) } + + it 'returns days remaining' do + expect(milestone_remaining).to eq("<strong>12</strong> days remaining") + end + end + + context 'when less than 1 year and more than 30 days remaining' do + let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) } + + it 'returns months remaining' do + expect(milestone_remaining).to eq("<strong>2</strong> months remaining") + end + end + + context 'when more than 1 year remaining' do + let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) } + + it 'returns years remaining' do + expect(milestone_remaining).to eq("<strong>1</strong> year remaining") + end + end + + context 'when milestone is expired' do + let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 2.days.ago.utc)) } + + it 'returns "Past due"' do + expect(milestone_remaining).to eq("<strong>Past due</strong>") + end + end + + context 'when milestone has start_date in the future' do + let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) } + + it 'returns "Upcoming"' do + expect(milestone_remaining).to eq("<strong>Upcoming</strong>") + end + end + + context 'when milestone has start_date in the past' do + let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, start_date: 2.days.ago.utc)) } + + it 'returns days elapsed' do + expect(milestone_remaining).to eq("<strong>2</strong> days elapsed") + end + end + end end diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb index 24a6f1a2a8a..c90396ebb28 100644 --- a/spec/serializers/job_entity_spec.rb +++ b/spec/serializers/job_entity_spec.rb @@ -133,22 +133,65 @@ describe JobEntity do context 'when job failed' do let(:job) { create(:ci_build, :script_failure) } - describe 'status' do - it 'should contain the failure reason inside label' do - expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip - expect(subject[:status][:label]).to eq('failed') - expect(subject[:status][:tooltip]).to eq('failed <br> (script failure)') - end + it 'contains details' do + expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip + end + + it 'states that it failed' do + expect(subject[:status][:label]).to eq('failed') + end + + it 'should indicate the failure reason on tooltip' do + expect(subject[:status][:tooltip]).to eq('failed <br> (script failure)') + end + + it 'should include a callout message with a verbose output' do + expect(subject[:callout_message]).to eq('There has been a script failure. Check the job log for more information') + end + + it 'should state that it is not recoverable' do + expect(subject[:recoverable]).to be_falsy + end + end + + context 'when job is allowed to fail' do + let(:job) { create(:ci_build, :allowed_to_fail, :script_failure) } + + it 'contains details' do + expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip + end + + it 'states that it failed' do + expect(subject[:status][:label]).to eq('failed (allowed to fail)') + end + + it 'should indicate the failure reason on tooltip' do + expect(subject[:status][:tooltip]).to eq('failed <br> (script failure) (allowed to fail)') + end + + it 'should include a callout message with a verbose output' do + expect(subject[:callout_message]).to eq('There has been a script failure. Check the job log for more information') + end + + it 'should state that it is not recoverable' do + expect(subject[:recoverable]).to be_falsy + end + end + + context 'when job failed and is recoverable' do + let(:job) { create(:ci_build, :api_failure) } + + it 'should state it is recoverable' do + expect(subject[:recoverable]).to be_truthy end end context 'when job passed' do let(:job) { create(:ci_build, :success) } - describe 'status' do - it 'should not contain the failure reason inside label' do - expect(subject[:status][:label]).to eq('passed') - end + it 'should not include callout message or recoverable keys' do + expect(subject).not_to include('callout_message') + expect(subject).not_to include('recoverable') end end end diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb index ae819c011de..80bac590a11 100644 --- a/spec/services/labels/transfer_service_spec.rb +++ b/spec/services/labels/transfer_service_spec.rb @@ -8,6 +8,7 @@ describe Labels::TransferService do let(:group_3) { create(:group) } let(:project_1) { create(:project, namespace: group_2) } let(:project_2) { create(:project, namespace: group_3) } + let(:project_3) { create(:project, namespace: group_1) } let(:group_label_1) { create(:group_label, group: group_1, name: 'Group Label 1') } let(:group_label_2) { create(:group_label, group: group_1, name: 'Group Label 2') } @@ -23,6 +24,7 @@ describe Labels::TransferService do create(:labeled_issue, project: project_1, labels: [group_label_4]) create(:labeled_issue, project: project_1, labels: [project_label_1]) create(:labeled_issue, project: project_2, labels: [group_label_5]) + create(:labeled_issue, project: project_3, labels: [group_label_1]) create(:labeled_merge_request, source_project: project_1, labels: [group_label_1, group_label_2]) create(:labeled_merge_request, source_project: project_2, labels: [group_label_5]) end @@ -52,5 +54,13 @@ describe Labels::TransferService do expect(project_1.labels.where(title: group_label_4.title)).to be_empty end + + it 'updates only label links in the given project' do + service.execute + + targets = LabelLink.where(label_id: group_label_1.id).map(&:target) + + expect(targets).to eq(project_3.issues) + end end end diff --git a/spec/services/projects/create_from_template_service_spec.rb b/spec/services/projects/create_from_template_service_spec.rb index 609d678caea..d40e6f1449d 100644 --- a/spec/services/projects/create_from_template_service_spec.rb +++ b/spec/services/projects/create_from_template_service_spec.rb @@ -7,7 +7,7 @@ describe Projects::CreateFromTemplateService do path: user.to_param, template_name: 'rails', description: 'project description', - visibility_level: Gitlab::VisibilityLevel::PRIVATE + visibility_level: Gitlab::VisibilityLevel::PUBLIC } end @@ -24,7 +24,23 @@ describe Projects::CreateFromTemplateService do expect(project).to be_saved expect(project.scheduled?).to be(true) - expect(project.description).to match('project description') - expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + + context 'the result project' do + before do + Sidekiq::Testing.inline! do + @project = subject.execute + end + + @project.reload + end + + it 'overrides template description' do + expect(@project.description).to match('project description') + end + + it 'overrides template visibility_level' do + expect(@project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end end end diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb index 16455e2517b..e7277b337f6 100644 --- a/spec/uploaders/object_storage_spec.rb +++ b/spec/uploaders/object_storage_spec.rb @@ -75,36 +75,8 @@ describe ObjectStorage do expect(object).to receive(:file_store).and_return(nil) end - context 'when object storage is enabled' do - context 'when direct uploads are enabled' do - before do - stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: true) - end - - it "uses Store::REMOTE" do - is_expected.to eq(described_class::Store::REMOTE) - end - end - - context 'when direct uploads are disabled' do - before do - stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: false) - end - - it "uses Store::LOCAL" do - is_expected.to eq(described_class::Store::LOCAL) - end - end - end - - context 'when object storage is disabled' do - before do - stub_uploads_object_storage(uploader_class, enabled: false) - end - - it "uses Store::LOCAL" do - is_expected.to eq(described_class::Store::LOCAL) - end + it "uses Store::LOCAL" do + is_expected.to eq(described_class::Store::LOCAL) end end @@ -537,6 +509,72 @@ describe ObjectStorage do end end + context 'when local file is used' do + let(:temp_file) { Tempfile.new("test") } + + before do + FileUtils.touch(temp_file) + end + + after do + FileUtils.rm_f(temp_file) + end + + context 'when valid file is used' do + context 'when valid file is specified' do + let(:uploaded_file) { temp_file } + + context 'when object storage and direct upload is specified' do + before do + stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: true) + end + + context 'when file is stored' do + subject do + uploader.store!(uploaded_file) + end + + it 'file to be remotely stored in permament location' do + subject + + expect(uploader).to be_exists + expect(uploader).not_to be_cached + expect(uploader).not_to be_file_storage + expect(uploader.path).not_to be_nil + expect(uploader.path).not_to include('tmp/upload') + expect(uploader.path).not_to include('tmp/cache') + expect(uploader.object_store).to eq(described_class::Store::REMOTE) + end + end + end + + context 'when object storage and direct upload is not used' do + before do + stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: false) + end + + context 'when file is stored' do + subject do + uploader.store!(uploaded_file) + end + + it 'file to be remotely stored in permament location' do + subject + + expect(uploader).to be_exists + expect(uploader).not_to be_cached + expect(uploader).to be_file_storage + expect(uploader.path).not_to be_nil + expect(uploader.path).not_to include('tmp/upload') + expect(uploader.path).not_to include('tmp/cache') + expect(uploader.object_store).to eq(described_class::Store::LOCAL) + end + end + end + end + end + end + context 'when remote file is used' do let(:temp_file) { Tempfile.new("test") } @@ -590,9 +628,9 @@ describe ObjectStorage do expect(uploader).to be_exists expect(uploader).to be_cached + expect(uploader).not_to be_file_storage expect(uploader.path).not_to be_nil expect(uploader.path).not_to include('tmp/cache') - expect(uploader.url).not_to be_nil expect(uploader.path).not_to include('tmp/cache') expect(uploader.object_store).to eq(described_class::Store::REMOTE) end @@ -607,6 +645,7 @@ describe ObjectStorage do expect(uploader).to be_exists expect(uploader).not_to be_cached + expect(uploader).not_to be_file_storage expect(uploader.path).not_to be_nil expect(uploader.path).not_to include('tmp/upload') expect(uploader.path).not_to include('tmp/cache') |
