diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-06 18:09:42 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-06 18:09:42 +0300 |
commit | a7beadc83470bd9ce23757a019795f49f95a6fff (patch) | |
tree | cb9ddaa8ea3eaf03b75184e682aef520ff46fc3f /spec | |
parent | 4279f24a19836d3e74e4aae8bea7acc2dd8222cc (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
31 files changed, 648 insertions, 602 deletions
diff --git a/spec/controllers/projects/alert_management_controller_spec.rb b/spec/controllers/projects/alert_management_controller_spec.rb index 4a6ebc46311..ccb9bfc3001 100644 --- a/spec/controllers/projects/alert_management_controller_spec.rb +++ b/spec/controllers/projects/alert_management_controller_spec.rb @@ -40,9 +40,9 @@ describe Projects::AlertManagementController do end describe 'GET #details' do - context 'when alert_management_minimal is enabled' do + context 'when alert_management_detail is enabled' do before do - stub_feature_flags(alert_management_minimal: true) + stub_feature_flags(alert_management_detail: true) end it 'shows the page' do @@ -52,9 +52,9 @@ describe Projects::AlertManagementController do end end - context 'when alert_management_minimal is disabled' do + context 'when alert_management_detail is disabled' do before do - stub_feature_flags(alert_management_minimal: false) + stub_feature_flags(alert_management_detail: false) end it 'shows 404' do @@ -64,4 +64,12 @@ describe Projects::AlertManagementController do end end end + + describe 'set_alert_id' do + it 'sets alert id from the route' do + get :details, params: { namespace_id: project.namespace, project_id: project, id: id } + + expect(assigns(:alert_id)).to eq(id.to_s) + end + end end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 875371d26c9..26786aab12c 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -320,6 +320,12 @@ FactoryBot.define do end end + trait :accessibility_reports do + after(:build) do |build| + build.job_artifacts << create(:ci_job_artifact, :accessibility, job: build) + end + end + trait :coverage_reports do after(:build) do |build| build.job_artifacts << create(:ci_job_artifact, :cobertura, job: build) diff --git a/spec/factories/ci/daily_report_results.rb b/spec/factories/ci/daily_build_group_report_results.rb index e2255e8a134..7f72991b3eb 100644 --- a/spec/factories/ci/daily_report_results.rb +++ b/spec/factories/ci/daily_build_group_report_results.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true FactoryBot.define do - factory :ci_daily_report_result, class: 'Ci::DailyReportResult' do + factory :ci_daily_build_group_report_result, class: 'Ci::DailyBuildGroupReportResult' do ref_path { Gitlab::Git::BRANCH_REF_PREFIX + 'master' } date { Time.zone.now.to_date } project last_pipeline factory: :ci_pipeline - param_type { Ci::DailyReportResult.param_types[:coverage] } - title { 'rspec' } - value { 77.0 } + group_name { 'rspec' } + data do + { coverage: 77.0 } + end end end diff --git a/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb b/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb new file mode 100644 index 00000000000..c3f17227701 --- /dev/null +++ b/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Issues > Real-time sidebar', :js do + let_it_be(:project) { create(:project, :public) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:user) { create(:user) } + + before_all do + project.add_developer(user) + end + + it 'updates the assignee in real-time' do + Capybara::Session.new(:other_session) + + using_session :other_session do + visit project_issue_path(project, issue) + expect(page.find('.assignee')).to have_content 'None' + end + + gitlab_sign_in(user) + visit project_issue_path(project, issue) + expect(page.find('.assignee')).to have_content 'None' + + click_button 'assign yourself' + + using_session :other_session do + expect(page.find('.assignee')).to have_content user.name + end + end +end diff --git a/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js index 03279fc56a4..91bcef5cb62 100644 --- a/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js +++ b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js @@ -10,6 +10,9 @@ export const Editor = { initialEditType: { type: String, }, + height: { + type: String, + }, }, render(h) { return h('div'); diff --git a/spec/frontend/alert_management/components/alert_management_detail_spec.js b/spec/frontend/alert_management/components/alert_management_detail_spec.js index 994de8d4d39..f5b5d669742 100644 --- a/spec/frontend/alert_management/components/alert_management_detail_spec.js +++ b/spec/frontend/alert_management/components/alert_management_detail_spec.js @@ -4,14 +4,18 @@ import AlertDetails from '~/alert_management/components/alert_details.vue'; describe('AlertDetails', () => { let wrapper; - function mountComponent() { - wrapper = shallowMount(AlertDetails); + function mountComponent(alert = {}) { + wrapper = shallowMount(AlertDetails, { + propsData: { + alertId: 'alertId', + projectPath: 'projectPath', + }, + data() { + return { alert }; + }, + }); } - beforeEach(() => { - mountComponent(); - }); - afterEach(() => { if (wrapper) { wrapper.destroy(); @@ -19,20 +23,42 @@ describe('AlertDetails', () => { }); describe('Alert details', () => { - it('renders a tab with overview information', () => { - expect(wrapper.find('[data-testid="overviewTab"]').exists()).toBe(true); - }); + describe('when alert is null', () => { + beforeEach(() => { + mountComponent(null); + }); - it('renders a tab with full alert information', () => { - expect(wrapper.find('[data-testid="fullDetailsTab"]').exists()).toBe(true); + describe('when alert is null', () => { + beforeEach(() => { + mountComponent(null); + }); + + it('shows an empty state', () => { + expect(wrapper.find('[data-testid="alertDetailsTabs"]').exists()).toBe(false); + }); + }); }); - it('renders alert details', () => { - expect(wrapper.find('[data-testid="startTimeItem"]').exists()).toBe(true); + describe('when alert is present', () => { + beforeEach(() => { + mountComponent(); + }); + + it('renders a tab with overview information', () => { + expect(wrapper.find('[data-testid="overviewTab"]').exists()).toBe(true); + }); + + it('renders a tab with full alert information', () => { + expect(wrapper.find('[data-testid="fullDetailsTab"]').exists()).toBe(true); + }); + + it('renders alert details', () => { + expect(wrapper.find('[data-testid="startTimeItem"]').exists()).toBe(true); + }); }); - it('renders a status dropdown', () => { - expect(wrapper.find('[data-testid="statusDropdownItem"]').exists()).toBe(true); + it('renders a status dropdown containing three items', () => { + expect(wrapper.findAll('[data-testid="statusDropdownItem"]').length).toBe(3); }); }); }); diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index 3b6021c8014..970bc99f8ff 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -44,6 +44,7 @@ import { setExpandedDiffLines, setSuggestPopoverDismissed, changeCurrentCommit, + moveToNeighboringCommit, } from '~/diffs/store/actions'; import eventHub from '~/notes/event_hub'; import * as types from '~/diffs/store/mutation_types'; @@ -1406,4 +1407,44 @@ describe('DiffsStoreActions', () => { }, ); }); + + describe('moveToNeighboringCommit', () => { + it.each` + direction | expected | currentCommit + ${'next'} | ${'NEXTSHA'} | ${{ next_commit_id: 'NEXTSHA' }} + ${'previous'} | ${'PREVIOUSSHA'} | ${{ prev_commit_id: 'PREVIOUSSHA' }} + `( + 'for the direction "$direction", dispatches the action to move to the SHA "$expected"', + ({ direction, expected, currentCommit }) => { + return testAction( + moveToNeighboringCommit, + { direction }, + { commit: currentCommit }, + [], + [{ type: 'changeCurrentCommit', payload: { commitId: expected } }], + ); + }, + ); + + it.each` + direction | diffsAreLoading | currentCommit + ${'next'} | ${false} | ${{ prev_commit_id: 'PREVIOUSSHA' }} + ${'next'} | ${true} | ${{ prev_commit_id: 'PREVIOUSSHA' }} + ${'next'} | ${false} | ${undefined} + ${'previous'} | ${false} | ${{ next_commit_id: 'NEXTSHA' }} + ${'previous'} | ${true} | ${{ next_commit_id: 'NEXTSHA' }} + ${'previous'} | ${false} | ${undefined} + `( + 'given `{ "isloading": $diffsAreLoading, "commit": $currentCommit }` in state, no actions are dispatched', + ({ direction, diffsAreLoading, currentCommit }) => { + return testAction( + moveToNeighboringCommit, + { direction }, + { commit: currentCommit, isLoading: diffsAreLoading }, + [], + [], + ); + }, + ); + }); }); diff --git a/spec/frontend/helpers/event_hub_factory_spec.js b/spec/frontend/helpers/event_hub_factory_spec.js new file mode 100644 index 00000000000..ff00e29a40a --- /dev/null +++ b/spec/frontend/helpers/event_hub_factory_spec.js @@ -0,0 +1,36 @@ +import createEventHub from '~/helpers/event_hub_factory'; +import mitt from 'mitt'; + +jest.mock('mitt'); + +mitt.mockReturnValue({ + on: () => {}, + off: () => {}, + emit: () => {}, +}); + +describe('event bus factory', () => { + let eventBus; + + beforeEach(() => { + eventBus = createEventHub(); + }); + + afterEach(() => { + eventBus = null; + }); + + it('creates an emitter', () => { + expect(mitt).toHaveBeenCalled(); + }); + + it.each` + method + ${'on'} + ${'off'} + ${'emit'} + `('binds $$method to $method ', ({ method }) => { + expect(typeof eventBus[method]).toBe('function'); + expect(eventBus[method]).toBe(eventBus[`$${method}`]); + }); +}); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js index 2d4d3ea28ff..42076e8da5c 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/frontend/issue_show/components/app_spec.js @@ -1,9 +1,8 @@ -/* eslint-disable no-unused-vars */ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; -import setTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; -import GLDropdown from '~/gl_dropdown'; +import { TEST_HOST } from 'helpers/test_constants'; import axios from '~/lib/utils/axios_utils'; +import { visitUrl } from '~/lib/utils/url_utility'; import '~/behaviors/markdown/render_gfm'; import issuableApp from '~/issue_show/components/app.vue'; import eventHub from '~/issue_show/event_hub'; @@ -13,6 +12,9 @@ function formatText(text) { return text.trim().replace(/\s\s+/g, ' '); } +jest.mock('~/lib/utils/url_utility'); +jest.mock('~/issue_show/event_hub'); + const REALTIME_REQUEST_STACK = [initialRequest, secondRequest]; describe('Issuable output', () => { @@ -20,9 +22,10 @@ describe('Issuable output', () => { let realtimeRequestCount = 0; let vm; - beforeEach(done => { + beforeEach(() => { setFixtures(` <div> + <title>Title</title> <div class="detail-page-description content-block"> <details open> <summary>One</summary> @@ -35,7 +38,6 @@ describe('Issuable output', () => { <span id="task_status"></span> </div> `); - spyOn(eventHub, '$emit'); const IssuableDescriptionComponent = Vue.extend(issuableApp); @@ -53,7 +55,7 @@ describe('Issuable output', () => { canUpdate: true, canDestroy: true, endpoint: '/gitlab-org/gitlab-shell/-/issues/9/realtime_changes', - updateEndpoint: gl.TEST_HOST, + updateEndpoint: TEST_HOST, issuableRef: '#1', initialTitleHtml: '', initialTitleText: '', @@ -67,8 +69,6 @@ describe('Issuable output', () => { issuableTemplateNamesPath: '/issuable-templates-path', }, }).$mount(); - - setTimeout(done); }); afterEach(() => { @@ -79,9 +79,10 @@ describe('Issuable output', () => { vm.$destroy(); }); - it('should render a title/description/edited and update title/description/edited on update', done => { + it('should render a title/description/edited and update title/description/edited on update', () => { let editedText; - Vue.nextTick() + return axios + .waitForAll() .then(() => { editedText = vm.$el.querySelector('.edited-text'); }) @@ -100,8 +101,8 @@ describe('Issuable output', () => { }) .then(() => { vm.poll.makeRequest(); + return axios.waitForAll(); }) - .then(() => new Promise(resolve => setTimeout(resolve))) .then(() => { expect(document.querySelector('title').innerText).toContain('2 (#1)'); expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>'); @@ -115,312 +116,239 @@ describe('Issuable output', () => { expect(editedText.querySelector('.author-link').href).toMatch(/\/other_user$/); expect(editedText.querySelector('time')).toBeTruthy(); expect(vm.state.lock_version).toEqual(2); - }) - .then(done) - .catch(done.fail); + }); }); - it('shows actions if permissions are correct', done => { + it('shows actions if permissions are correct', () => { vm.showForm = true; - Vue.nextTick(() => { + return vm.$nextTick().then(() => { expect(vm.$el.querySelector('.btn')).not.toBeNull(); - - done(); }); }); - it('does not show actions if permissions are incorrect', done => { + it('does not show actions if permissions are incorrect', () => { vm.showForm = true; vm.canUpdate = false; - Vue.nextTick(() => { + return vm.$nextTick().then(() => { expect(vm.$el.querySelector('.btn')).toBeNull(); - - done(); }); }); - it('does not update formState if form is already open', done => { + it('does not update formState if form is already open', () => { vm.updateAndShowForm(); vm.state.titleText = 'testing 123'; vm.updateAndShowForm(); - Vue.nextTick(() => { + return vm.$nextTick().then(() => { expect(vm.store.formState.title).not.toBe('testing 123'); + }); + }); + + it('opens reCAPTCHA modal if update rejected as spam', () => { + let modal; - done(); + jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + data: { + recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>', + }, }); + + vm.canUpdate = true; + vm.showForm = true; + + return vm + .$nextTick() + .then(() => { + vm.$refs.recaptchaModal.scriptSrc = '//scriptsrc'; + return vm.updateIssuable(); + }) + .then(() => { + modal = vm.$el.querySelector('.js-recaptcha-modal'); + + expect(modal.style.display).not.toEqual('none'); + expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html'); + expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc'); + }) + .then(() => { + modal.querySelector('.close').click(); + return vm.$nextTick(); + }) + .then(() => { + expect(modal.style.display).toEqual('none'); + expect(document.body.querySelector('.js-recaptcha-script')).toBeNull(); + }); }); describe('updateIssuable', () => { - it('fetches new data after update', done => { - spyOn(vm, 'updateStoreState').and.callThrough(); - spyOn(vm.service, 'getData').and.callThrough(); - spyOn(vm.service, 'updateIssuable').and.returnValue( - Promise.resolve({ - data: { web_url: window.location.pathname }, - }), - ); - - vm.updateIssuable() - .then(() => { - expect(vm.updateStoreState).toHaveBeenCalled(); - expect(vm.service.getData).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); + it('fetches new data after update', () => { + const updateStoreSpy = jest.spyOn(vm, 'updateStoreState'); + const getDataSpy = jest.spyOn(vm.service, 'getData'); + jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + data: { web_url: window.location.pathname }, + }); + + return vm.updateIssuable().then(() => { + expect(updateStoreSpy).toHaveBeenCalled(); + expect(getDataSpy).toHaveBeenCalled(); + }); }); - it('correctly updates issuable data', done => { - spyOn(vm.service, 'updateIssuable').and.returnValue( - Promise.resolve({ - data: { web_url: window.location.pathname }, - }), - ); + it('correctly updates issuable data', () => { + const spy = jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + data: { web_url: window.location.pathname }, + }); - vm.updateIssuable() - .then(() => { - expect(vm.service.updateIssuable).toHaveBeenCalledWith(vm.formState); - expect(eventHub.$emit).toHaveBeenCalledWith('close.form'); - }) - .then(done) - .catch(done.fail); + return vm.updateIssuable().then(() => { + expect(spy).toHaveBeenCalledWith(vm.formState); + expect(eventHub.$emit).toHaveBeenCalledWith('close.form'); + }); }); - it('does not redirect if issue has not moved', done => { - const visitUrl = spyOnDependency(issuableApp, 'visitUrl'); - spyOn(vm.service, 'updateIssuable').and.returnValue( - Promise.resolve({ - data: { - web_url: window.location.pathname, - confidential: vm.isConfidential, - }, - }), - ); - - vm.updateIssuable(); + it('does not redirect if issue has not moved', () => { + jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + data: { + web_url: window.location.pathname, + confidential: vm.isConfidential, + }, + }); - setTimeout(() => { + return vm.updateIssuable().then(() => { expect(visitUrl).not.toHaveBeenCalled(); - done(); }); }); - it('redirects if returned web_url has changed', done => { - const visitUrl = spyOnDependency(issuableApp, 'visitUrl'); - spyOn(vm.service, 'updateIssuable').and.returnValue( - Promise.resolve({ - data: { - web_url: '/testing-issue-move', - confidential: vm.isConfidential, - }, - }), - ); + it('redirects if returned web_url has changed', () => { + jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + data: { + web_url: '/testing-issue-move', + confidential: vm.isConfidential, + }, + }); vm.updateIssuable(); - setTimeout(() => { + return vm.updateIssuable().then(() => { expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move'); - done(); }); }); describe('shows dialog when issue has unsaved changed', () => { - it('confirms on title change', done => { + it('confirms on title change', () => { vm.showForm = true; vm.state.titleText = 'title has changed'; const e = { returnValue: null }; vm.handleBeforeUnloadEvent(e); - Vue.nextTick(() => { + return vm.$nextTick().then(() => { expect(e.returnValue).not.toBeNull(); - - done(); }); }); - it('confirms on description change', done => { + it('confirms on description change', () => { vm.showForm = true; vm.state.descriptionText = 'description has changed'; const e = { returnValue: null }; vm.handleBeforeUnloadEvent(e); - Vue.nextTick(() => { + return vm.$nextTick().then(() => { expect(e.returnValue).not.toBeNull(); - - done(); }); }); - it('does nothing when nothing has changed', done => { + it('does nothing when nothing has changed', () => { const e = { returnValue: null }; vm.handleBeforeUnloadEvent(e); - Vue.nextTick(() => { + return vm.$nextTick().then(() => { expect(e.returnValue).toBeNull(); - - done(); }); }); }); describe('error when updating', () => { - it('closes form on error', done => { - spyOn(vm.service, 'updateIssuable').and.callFake(() => Promise.reject()); - vm.updateIssuable(); - - setTimeout(() => { + it('closes form on error', () => { + jest.spyOn(vm.service, 'updateIssuable').mockRejectedValue(); + return vm.updateIssuable().then(() => { expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( `Error updating issue`, ); - - done(); }); }); - it('returns the correct error message for issuableType', done => { - spyOn(vm.service, 'updateIssuable').and.callFake(() => Promise.reject()); + it('returns the correct error message for issuableType', () => { + jest.spyOn(vm.service, 'updateIssuable').mockRejectedValue(); vm.issuableType = 'merge request'; - Vue.nextTick(() => { - vm.updateIssuable(); - - setTimeout(() => { + return vm + .$nextTick() + .then(vm.updateIssuable) + .then(() => { expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( `Error updating merge request`, ); - - done(); }); - }); }); - it('shows error message from backend if exists', done => { + it('shows error message from backend if exists', () => { const msg = 'Custom error message from backend'; - spyOn(vm.service, 'updateIssuable').and.callFake( - // eslint-disable-next-line prefer-promise-reject-errors - () => Promise.reject({ response: { data: { errors: [msg] } } }), - ); + jest + .spyOn(vm.service, 'updateIssuable') + .mockRejectedValue({ response: { data: { errors: [msg] } } }); - vm.updateIssuable(); - setTimeout(() => { + return vm.updateIssuable().then(() => { expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( `${vm.defaultErrorMessage}. ${msg}`, ); - - done(); }); }); }); }); - it('opens reCAPTCHA modal if update rejected as spam', done => { - function mockScriptSrc() { - const recaptchaChild = vm.$children.find( - // eslint-disable-next-line no-underscore-dangle - child => child.$options._componentTag === 'recaptcha-modal', - ); - - recaptchaChild.scriptSrc = '//scriptsrc'; - } - - let modal; - const promise = new Promise(resolve => { - resolve({ + describe('deleteIssuable', () => { + it('changes URL when deleted', () => { + jest.spyOn(vm.service, 'deleteIssuable').mockResolvedValue({ data: { - recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>', + web_url: '/test', }, }); - }); - - spyOn(vm.service, 'updateIssuable').and.returnValue(promise); - - vm.canUpdate = true; - vm.showForm = true; - - vm.$nextTick() - .then(() => mockScriptSrc()) - .then(() => vm.updateIssuable()) - .then(promise) - .then(() => setTimeoutPromise()) - .then(() => { - modal = vm.$el.querySelector('.js-recaptcha-modal'); - expect(modal.style.display).not.toEqual('none'); - expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html'); - expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc'); - }) - .then(() => modal.querySelector('.close').click()) - .then(() => vm.$nextTick()) - .then(() => { - expect(modal.style.display).toEqual('none'); - expect(document.body.querySelector('.js-recaptcha-script')).toBeNull(); - }) - .then(done) - .catch(done.fail); - }); - - describe('deleteIssuable', () => { - it('changes URL when deleted', done => { - const visitUrl = spyOnDependency(issuableApp, 'visitUrl'); - spyOn(vm.service, 'deleteIssuable').and.returnValue( - Promise.resolve({ - data: { - web_url: '/test', - }, - }), - ); - - vm.deleteIssuable(); - - setTimeout(() => { + return vm.deleteIssuable().then(() => { expect(visitUrl).toHaveBeenCalledWith('/test'); - - done(); }); }); - it('stops polling when deleting', done => { - spyOnDependency(issuableApp, 'visitUrl'); - spyOn(vm.poll, 'stop').and.callThrough(); - spyOn(vm.service, 'deleteIssuable').and.returnValue( - Promise.resolve({ - data: { - web_url: '/test', - }, - }), - ); - - vm.deleteIssuable(); - - setTimeout(() => { - expect(vm.poll.stop).toHaveBeenCalledWith(); + it('stops polling when deleting', () => { + const spy = jest.spyOn(vm.poll, 'stop'); + jest.spyOn(vm.service, 'deleteIssuable').mockResolvedValue({ + data: { + web_url: '/test', + }, + }); - done(); + return vm.deleteIssuable().then(() => { + expect(spy).toHaveBeenCalledWith(); }); }); - it('closes form on error', done => { - spyOn(vm.service, 'deleteIssuable').and.returnValue(Promise.reject()); + it('closes form on error', () => { + jest.spyOn(vm.service, 'deleteIssuable').mockRejectedValue(); - vm.deleteIssuable(); - - setTimeout(() => { + return vm.deleteIssuable().then(() => { expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( 'Error deleting issue', ); - - done(); }); }); }); describe('updateAndShowForm', () => { - it('shows locked warning if form is open & data is different', done => { - vm.$nextTick() + it('shows locked warning if form is open & data is different', () => { + return vm + .$nextTick() .then(() => { vm.updateAndShowForm(); @@ -436,44 +364,38 @@ describe('Issuable output', () => { expect(vm.formState.lockedWarningVisible).toEqual(true); expect(vm.formState.lock_version).toEqual(1); expect(vm.$el.querySelector('.alert')).not.toBeNull(); - }) - .then(done) - .catch(done.fail); + }); }); }); describe('requestTemplatesAndShowForm', () => { + let formSpy; + beforeEach(() => { - spyOn(vm, 'updateAndShowForm'); + formSpy = jest.spyOn(vm, 'updateAndShowForm'); }); - it('shows the form if template names request is successful', done => { + it('shows the form if template names request is successful', () => { const mockData = [{ name: 'Bug' }]; mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData])); - vm.requestTemplatesAndShowForm() - .then(() => { - expect(vm.updateAndShowForm).toHaveBeenCalledWith(mockData); - }) - .then(done) - .catch(done.fail); + return vm.requestTemplatesAndShowForm().then(() => { + expect(formSpy).toHaveBeenCalledWith(mockData); + }); }); - it('shows the form if template names request failed', done => { + it('shows the form if template names request failed', () => { mock .onGet('/issuable-templates-path') .reply(() => Promise.reject(new Error('something went wrong'))); - vm.requestTemplatesAndShowForm() - .then(() => { - expect(document.querySelector('.flash-container .flash-text').textContent).toContain( - 'Error updating issue', - ); + return vm.requestTemplatesAndShowForm().then(() => { + expect(document.querySelector('.flash-container .flash-text').textContent).toContain( + 'Error updating issue', + ); - expect(vm.updateAndShowForm).toHaveBeenCalledWith(); - }) - .then(done) - .catch(done.fail); + expect(formSpy).toHaveBeenCalledWith(); + }); }); }); @@ -490,32 +412,26 @@ describe('Issuable output', () => { }); describe('updateStoreState', () => { - it('should make a request and update the state of the store', done => { + it('should make a request and update the state of the store', () => { const data = { foo: 1 }; - spyOn(vm.store, 'updateState'); - spyOn(vm.service, 'getData').and.returnValue(Promise.resolve({ data })); + const getDataSpy = jest.spyOn(vm.service, 'getData').mockResolvedValue({ data }); + const updateStateSpy = jest.spyOn(vm.store, 'updateState').mockImplementation(jest.fn); - vm.updateStoreState() - .then(() => { - expect(vm.service.getData).toHaveBeenCalled(); - expect(vm.store.updateState).toHaveBeenCalledWith(data); - }) - .then(done) - .catch(done.fail); + return vm.updateStoreState().then(() => { + expect(getDataSpy).toHaveBeenCalled(); + expect(updateStateSpy).toHaveBeenCalledWith(data); + }); }); - it('should show error message if store update fails', done => { - spyOn(vm.service, 'getData').and.returnValue(Promise.reject()); + it('should show error message if store update fails', () => { + jest.spyOn(vm.service, 'getData').mockRejectedValue(); vm.issuableType = 'merge request'; - vm.updateStoreState() - .then(() => { - expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( - `Error updating ${vm.issuableType}`, - ); - }) - .then(done) - .catch(done.fail); + return vm.updateStoreState().then(() => { + expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( + `Error updating ${vm.issuableType}`, + ); + }); }); }); diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/frontend/issue_show/components/description_spec.js index 194f177d837..9c448c498e2 100644 --- a/spec/javascripts/issue_show/components/description_spec.js +++ b/spec/frontend/issue_show/components/description_spec.js @@ -1,8 +1,12 @@ import $ from 'jquery'; import Vue from 'vue'; import '~/behaviors/markdown/render_gfm'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import { TEST_HOST } from 'helpers/test_constants'; import Description from '~/issue_show/components/description.vue'; +import TaskList from '~/task_list'; + +jest.mock('~/task_list'); describe('Description component', () => { let vm; @@ -13,7 +17,7 @@ describe('Description component', () => { descriptionText: 'test', updatedAt: new Date().toString(), taskStatus: '', - updateUrl: gl.TEST_HOST, + updateUrl: TEST_HOST, }; beforeEach(() => { @@ -39,25 +43,26 @@ describe('Description component', () => { $('.issuable-meta .flash-container').remove(); }); - it('animates description changes', done => { + it('animates description changes', () => { vm.descriptionHtml = 'changed'; - Vue.nextTick(() => { - expect( - vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'), - ).toBeTruthy(); - - setTimeout(() => { + return vm + .$nextTick() + .then(() => { + expect( + vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'), + ).toBeTruthy(); + jest.runAllTimers(); + return vm.$nextTick(); + }) + .then(() => { expect( vm.$el.querySelector('.md').classList.contains('issue-realtime-trigger-pulse'), ).toBeTruthy(); - - done(); }); - }); }); - it('opens reCAPTCHA dialog if update rejected as spam', done => { + it('opens reCAPTCHA dialog if update rejected as spam', () => { let modal; const recaptchaChild = vm.$children.find( // eslint-disable-next-line no-underscore-dangle @@ -70,7 +75,8 @@ describe('Description component', () => { recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>', }); - vm.$nextTick() + return vm + .$nextTick() .then(() => { modal = vm.$el.querySelector('.js-recaptcha-modal'); @@ -83,128 +89,105 @@ describe('Description component', () => { .then(() => { expect(modal.style.display).toEqual('none'); expect(document.body.querySelector('.js-recaptcha-script')).toBeNull(); - }) - .then(done) - .catch(done.fail); + }); }); - describe('TaskList', () => { - let TaskList; + it('applies syntax highlighting and math when description changed', () => { + const vmSpy = jest.spyOn(vm, 'renderGFM'); + const prototypeSpy = jest.spyOn($.prototype, 'renderGFM'); + vm.descriptionHtml = 'changed'; + return vm.$nextTick().then(() => { + expect(vm.$refs['gfm-content']).toBeDefined(); + expect(vmSpy).toHaveBeenCalled(); + expect(prototypeSpy).toHaveBeenCalled(); + expect($.prototype.renderGFM).toHaveBeenCalled(); + }); + }); + + it('sets data-update-url', () => { + expect(vm.$el.querySelector('textarea').dataset.updateUrl).toEqual(TEST_HOST); + }); + + describe('TaskList', () => { beforeEach(() => { vm.$destroy(); + TaskList.mockClear(); vm = mountComponent( DescriptionComponent, Object.assign({}, props, { issuableType: 'issuableType', }), ); - TaskList = spyOnDependency(Description, 'TaskList'); }); - it('re-inits the TaskList when description changed', done => { + it('re-inits the TaskList when description changed', () => { vm.descriptionHtml = 'changed'; - setTimeout(() => { - expect(TaskList).toHaveBeenCalled(); - done(); - }); + expect(TaskList).toHaveBeenCalled(); }); - it('does not re-init the TaskList when canUpdate is false', done => { + it('does not re-init the TaskList when canUpdate is false', () => { vm.canUpdate = false; vm.descriptionHtml = 'changed'; - setTimeout(() => { - expect(TaskList).not.toHaveBeenCalled(); - done(); - }); + expect(TaskList).toHaveBeenCalledTimes(1); }); - it('calls with issuableType dataType', done => { + it('calls with issuableType dataType', () => { vm.descriptionHtml = 'changed'; - setTimeout(() => { - expect(TaskList).toHaveBeenCalledWith({ - dataType: 'issuableType', - fieldName: 'description', - selector: '.detail-page-description', - onSuccess: jasmine.any(Function), - onError: jasmine.any(Function), - lockVersion: 0, - }); - - done(); + expect(TaskList).toHaveBeenCalledWith({ + dataType: 'issuableType', + fieldName: 'description', + selector: '.detail-page-description', + onSuccess: expect.any(Function), + onError: expect.any(Function), + lockVersion: 0, }); }); }); describe('taskStatus', () => { - it('adds full taskStatus', done => { + it('adds full taskStatus', () => { vm.taskStatus = '1 of 1'; - setTimeout(() => { + return vm.$nextTick().then(() => { expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe( '1 of 1', ); - - done(); }); }); - it('adds short taskStatus', done => { + it('adds short taskStatus', () => { vm.taskStatus = '1 of 1'; - setTimeout(() => { + return vm.$nextTick().then(() => { expect(document.querySelector('.issuable-meta #task_status_short').textContent.trim()).toBe( '1/1 task', ); - - done(); }); }); - it('clears task status text when no tasks are present', done => { + it('clears task status text when no tasks are present', () => { vm.taskStatus = '0 of 0'; - setTimeout(() => { + return vm.$nextTick().then(() => { expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe(''); - - done(); }); }); }); - it('applies syntax highlighting and math when description changed', done => { - spyOn(vm, 'renderGFM').and.callThrough(); - spyOn($.prototype, 'renderGFM').and.callThrough(); - vm.descriptionHtml = 'changed'; - - Vue.nextTick(() => { - setTimeout(() => { - expect(vm.$refs['gfm-content']).toBeDefined(); - expect(vm.renderGFM).toHaveBeenCalled(); - expect($.prototype.renderGFM).toHaveBeenCalled(); - - done(); - }); - }); - }); - - it('sets data-update-url', () => { - expect(vm.$el.querySelector('textarea').dataset.updateUrl).toEqual(gl.TEST_HOST); - }); - describe('taskListUpdateError', () => { it('should create flash notification and emit an event to parent', () => { const msg = 'Someone edited this issue at the same time you did. The description has been updated and you will need to make your changes again.'; - spyOn(vm, '$emit'); + const spy = jest.spyOn(vm, '$emit'); vm.taskListUpdateError(); expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(msg); - expect(vm.$emit).toHaveBeenCalledWith('taskListUpdateFailed'); + expect(spy).toHaveBeenCalledWith('taskListUpdateFailed'); }); }); }); diff --git a/spec/javascripts/issue_show/components/edited_spec.js b/spec/frontend/issue_show/components/edited_spec.js index a1683f060c0..a1683f060c0 100644 --- a/spec/javascripts/issue_show/components/edited_spec.js +++ b/spec/frontend/issue_show/components/edited_spec.js diff --git a/spec/javascripts/issue_show/components/fields/description_template_spec.js b/spec/frontend/issue_show/components/fields/description_template_spec.js index 8d77a620d76..9ebab31f1ad 100644 --- a/spec/javascripts/issue_show/components/fields/description_template_spec.js +++ b/spec/frontend/issue_show/components/fields/description_template_spec.js @@ -5,7 +5,7 @@ describe('Issue description template component', () => { let vm; let formState; - beforeEach(done => { + beforeEach(() => { const Component = Vue.extend(descriptionTemplate); formState = { description: 'test', @@ -19,8 +19,6 @@ describe('Issue description template component', () => { projectNamespace: '/', }, }).$mount(); - - Vue.nextTick(done); }); it('renders templates as JSON array in data attribute', () => { diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/frontend/issue_show/components/form_spec.js index a111333ac80..b06a3a89d3b 100644 --- a/spec/javascripts/issue_show/components/form_spec.js +++ b/spec/frontend/issue_show/components/form_spec.js @@ -1,8 +1,11 @@ import Vue from 'vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; import formComponent from '~/issue_show/components/form.vue'; +import Autosave from '~/autosave'; import eventHub from '~/issue_show/event_hub'; +jest.mock('~/autosave'); + describe('Inline edit form component', () => { let vm; const defaultProps = { @@ -65,18 +68,16 @@ describe('Inline edit form component', () => { }); describe('autosave', () => { - let autosaveObj; - let autosave; + let spy; beforeEach(() => { - autosaveObj = { reset: jasmine.createSpy() }; - autosave = spyOnDependency(formComponent, 'Autosave').and.returnValue(autosaveObj); + spy = jest.spyOn(Autosave.prototype, 'reset'); }); it('initialized Autosave on mount', () => { createComponent(); - expect(autosave).toHaveBeenCalledTimes(2); + expect(Autosave).toHaveBeenCalledTimes(2); }); it('calls reset on autosave when eventHub emits appropriate events', () => { @@ -84,15 +85,15 @@ describe('Inline edit form component', () => { eventHub.$emit('close.form'); - expect(autosaveObj.reset).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledTimes(2); eventHub.$emit('delete.issuable'); - expect(autosaveObj.reset).toHaveBeenCalledTimes(4); + expect(spy).toHaveBeenCalledTimes(4); eventHub.$emit('update.issuable'); - expect(autosaveObj.reset).toHaveBeenCalledTimes(6); + expect(spy).toHaveBeenCalledTimes(6); }); }); }); diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/frontend/issue_show/components/title_spec.js index 9754c8a6755..c274048fdd5 100644 --- a/spec/javascripts/issue_show/components/title_spec.js +++ b/spec/frontend/issue_show/components/title_spec.js @@ -5,8 +5,9 @@ import eventHub from '~/issue_show/event_hub'; describe('Title component', () => { let vm; - beforeEach(() => { + setFixtures(`<title />`); + const Component = Vue.extend(titleComponent); const store = new Store({ titleHtml: '', @@ -28,51 +29,39 @@ describe('Title component', () => { expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>'); }); - it('updates page title when changing titleHtml', done => { - spyOn(vm, 'setPageTitle'); + it('updates page title when changing titleHtml', () => { + const spy = jest.spyOn(vm, 'setPageTitle'); vm.titleHtml = 'test'; - Vue.nextTick(() => { - expect(vm.setPageTitle).toHaveBeenCalled(); - - done(); + return vm.$nextTick().then(() => { + expect(spy).toHaveBeenCalled(); }); }); - it('animates title changes', done => { + it('animates title changes', () => { vm.titleHtml = 'test'; - - Vue.nextTick(() => { - expect( - vm.$el.querySelector('.title').classList.contains('issue-realtime-pre-pulse'), - ).toBeTruthy(); - - setTimeout(() => { - expect( - vm.$el.querySelector('.title').classList.contains('issue-realtime-trigger-pulse'), - ).toBeTruthy(); - - done(); + return vm + .$nextTick() + .then(() => { + expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-pre-pulse'); + jest.runAllTimers(); + return vm.$nextTick(); + }) + .then(() => { + expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-trigger-pulse'); }); - }); }); - it('updates page title after changing title', done => { + it('updates page title after changing title', () => { vm.titleHtml = 'changed'; vm.titleText = 'changed'; - Vue.nextTick(() => { + return vm.$nextTick().then(() => { expect(document.querySelector('title').textContent.trim()).toContain('changed'); - - done(); }); }); describe('inline edit button', () => { - beforeEach(() => { - spyOn(eventHub, '$emit'); - }); - it('should not show by default', () => { expect(vm.$el.querySelector('.btn-edit')).toBeNull(); }); @@ -92,6 +81,7 @@ describe('Title component', () => { }); it('should trigger open.form event when clicked', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); vm.showInlineEditButton = true; vm.canUpdate = true; diff --git a/spec/frontend/static_site_editor/pages/home_spec.js b/spec/frontend/static_site_editor/pages/home_spec.js index 82e39447ae6..ca03abfe6bb 100644 --- a/spec/frontend/static_site_editor/pages/home_spec.js +++ b/spec/frontend/static_site_editor/pages/home_spec.js @@ -5,7 +5,7 @@ import { GlSkeletonLoader } from '@gitlab/ui'; import createState from '~/static_site_editor/store/state'; import Home from '~/static_site_editor/pages/home.vue'; -import EditArea from '~/static_site_editor/components/edit_area.vue'; +import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue'; import EditHeader from '~/static_site_editor/components/edit_header.vue'; import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue'; import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue'; @@ -71,10 +71,13 @@ describe('static_site_editor/pages/home', () => { wrapper = shallowMount(Home, { localVue, store, + provide: { + glFeatures: { richContentEditor: true }, + }, }); }; - const findEditArea = () => wrapper.find(EditArea); + const findRichContentEditor = () => wrapper.find(RichContentEditor); const findEditHeader = () => wrapper.find(EditHeader); const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage); const findPublishToolbar = () => wrapper.find(PublishToolbar); @@ -103,8 +106,8 @@ describe('static_site_editor/pages/home', () => { }); describe('when content is not loaded', () => { - it('does not render edit area', () => { - expect(findEditArea().exists()).toBe(false); + it('does not render rich content editor', () => { + expect(findRichContentEditor().exists()).toBe(false); }); it('does not render edit header', () => { @@ -129,8 +132,8 @@ describe('static_site_editor/pages/home', () => { buildWrapper(); }); - it('renders the edit area', () => { - expect(findEditArea().exists()).toBe(true); + it('renders the rich content editor', () => { + expect(findRichContentEditor().exists()).toBe(true); }); it('renders the edit header', () => { @@ -141,8 +144,8 @@ describe('static_site_editor/pages/home', () => { expect(findSkeletonLoader().exists()).toBe(false); }); - it('passes page content to edit area', () => { - expect(findEditArea().props('value')).toBe(content); + it('passes page content to the rich content editor', () => { + expect(findRichContentEditor().props('value')).toBe(content); }); it('passes page title to edit header', () => { @@ -228,11 +231,11 @@ describe('static_site_editor/pages/home', () => { expect(loadContentActionMock).toHaveBeenCalled(); }); - it('dispatches setContent action when edit area emits input event', () => { + it('dispatches setContent action when rich content editor emits input event', () => { buildContentLoadedStore(); buildWrapper(); - findEditArea().vm.$emit('input', sourceContent); + findRichContentEditor().vm.$emit('input', sourceContent); expect(setContentActionMock).toHaveBeenCalledWith(expect.anything(), sourceContent, undefined); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js index 933609c3072..774fe25387a 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js @@ -50,6 +50,10 @@ describe('Rich Content Editor', () => { it('has the correct initial edit type', () => { expect(findEditor().props().initialEditType).toBe('wysiwyg'); }); + + it('has the correct height', () => { + expect(findEditor().props().height).toBe('100%'); + }); }); describe('when content is changed', () => { diff --git a/spec/helpers/projects/alert_management_helper_spec.rb b/spec/helpers/projects/alert_management_helper_spec.rb index 177dcb4ec2e..41f0f54b5ac 100644 --- a/spec/helpers/projects/alert_management_helper_spec.rb +++ b/spec/helpers/projects/alert_management_helper_spec.rb @@ -7,11 +7,11 @@ describe Projects::AlertManagementHelper do let_it_be(:project, reload: true) { create(:project) } let_it_be(:current_user) { create(:user) } + let_it_be(:project_path) { project.full_path } describe '#alert_management_data' do let(:user_can_enable_alert_management) { false } let(:setting_path) { project_settings_operations_path(project) } - let(:project_path) { project.full_path } before do allow(helper) @@ -21,7 +21,7 @@ describe Projects::AlertManagementHelper do end context 'without alert_managements_setting' do - it 'returns frontend configuration' do + it 'returns index page configuration' do expect(alert_management_data(current_user, project)).to eq( 'project-path' => project_path, 'enable-alert-management-path' => setting_path, @@ -32,4 +32,15 @@ describe Projects::AlertManagementHelper do end end end + + describe '#alert_management_detail_data' do + let(:alert_id) { 1 } + + it 'returns detail page configuration' do + expect(alert_management_detail_data(project_path, alert_id)).to eq( + 'alert-id' => alert_id, + 'project-path' => project_path + ) + end + end end diff --git a/spec/javascripts/issue_show/helpers.js b/spec/javascripts/issue_show/helpers.js deleted file mode 100644 index 951acfd4e10..00000000000 --- a/spec/javascripts/issue_show/helpers.js +++ /dev/null @@ -1 +0,0 @@ -export * from '../../frontend/issue_show/helpers.js'; diff --git a/spec/javascripts/issue_show/mock_data.js b/spec/javascripts/issue_show/mock_data.js deleted file mode 100644 index 1b391bd1588..00000000000 --- a/spec/javascripts/issue_show/mock_data.js +++ /dev/null @@ -1 +0,0 @@ -export * from '../../frontend/issue_show/mock_data'; diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index d2790a6b858..03930c6c1a7 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -217,7 +217,7 @@ ci_pipelines: - vulnerability_findings - pipeline_config - security_scans -- daily_report_results +- daily_build_group_report_results pipeline_variables: - pipeline stages: @@ -484,7 +484,7 @@ project: - status_page_setting - requirements - export_jobs -- daily_report_results +- daily_build_group_report_results - jira_imports - compliance_framework_setting - metrics_users_starred_dashboards diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 266d39a0dfb..3a608391b2b 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -3834,6 +3834,61 @@ describe Ci::Build do end end + describe '#collect_accessibility_reports!' do + subject { build.collect_accessibility_reports!(accessibility_report) } + + let(:accessibility_report) { Gitlab::Ci::Reports::AccessibilityReports.new } + + it { expect(accessibility_report.urls).to eq({}) } + + context 'when build has an accessibility report' do + context 'when there is an accessibility report with errors' do + before do + create(:ci_job_artifact, :accessibility, job: build, project: build.project) + end + + it 'parses blobs and add the results to the accessibility report' do + expect { subject }.not_to raise_error + + expect(accessibility_report.urls.keys).to match_array(['https://about.gitlab.com/']) + expect(accessibility_report.errors_count).to eq(10) + expect(accessibility_report.scans_count).to eq(1) + expect(accessibility_report.passes_count).to eq(0) + end + end + + context 'when there is an accessibility report without errors' do + before do + create(:ci_job_artifact, :accessibility_without_errors, job: build, project: build.project) + end + + it 'parses blobs and add the results to the accessibility report' do + expect { subject }.not_to raise_error + + expect(accessibility_report.urls.keys).to match_array(['https://pa11y.org/']) + expect(accessibility_report.errors_count).to eq(0) + expect(accessibility_report.scans_count).to eq(1) + expect(accessibility_report.passes_count).to eq(1) + end + end + + context 'when there is an accessibility report with an invalid url' do + before do + create(:ci_job_artifact, :accessibility_with_invalid_url, job: build, project: build.project) + end + + it 'parses blobs and add the results to the accessibility report' do + expect { subject }.not_to raise_error + + expect(accessibility_report.urls).to be_empty + expect(accessibility_report.errors_count).to eq(0) + expect(accessibility_report.scans_count).to eq(0) + expect(accessibility_report.passes_count).to eq(0) + end + end + end + end + describe '#collect_coverage_reports!' do subject { build.collect_coverage_reports!(coverage_report) } diff --git a/spec/models/ci/daily_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb index 61aa58c6692..d4c305c649a 100644 --- a/spec/models/ci/daily_report_result_spec.rb +++ b/spec/models/ci/daily_build_group_report_result_spec.rb @@ -2,14 +2,14 @@ require 'spec_helper' -describe Ci::DailyReportResult do +describe Ci::DailyBuildGroupReportResult do describe '.upsert_reports' do let!(:rspec_coverage) do create( - :ci_daily_report_result, - title: 'rspec', + :ci_daily_build_group_report_result, + group_name: 'rspec', date: '2020-03-09', - value: 71.2 + data: { coverage: 71.2 } ) end let!(:new_pipeline) { create(:ci_pipeline) } @@ -19,20 +19,18 @@ describe Ci::DailyReportResult do { project_id: rspec_coverage.project_id, ref_path: rspec_coverage.ref_path, - param_type: described_class.param_types[rspec_coverage.param_type], last_pipeline_id: new_pipeline.id, date: rspec_coverage.date, - title: 'rspec', - value: 81.0 + group_name: 'rspec', + data: { 'coverage' => 81.0 } }, { project_id: rspec_coverage.project_id, ref_path: rspec_coverage.ref_path, - param_type: described_class.param_types[rspec_coverage.param_type], last_pipeline_id: new_pipeline.id, date: rspec_coverage.date, - title: 'karma', - value: 87.0 + group_name: 'karma', + data: { 'coverage' => 87.0 } } ]) @@ -40,16 +38,15 @@ describe Ci::DailyReportResult do expect(rspec_coverage).to have_attributes( last_pipeline_id: new_pipeline.id, - value: 81.0 + data: { 'coverage' => 81.0 } ) - expect(described_class.find_by_title('karma')).to have_attributes( + expect(described_class.find_by_group_name('karma')).to have_attributes( project_id: rspec_coverage.project_id, ref_path: rspec_coverage.ref_path, - param_type: rspec_coverage.param_type, last_pipeline_id: new_pipeline.id, date: rspec_coverage.date, - value: 87.0 + data: { 'coverage' => 87.0 } ) end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 3df87e5d2b4..b8e10f43ef4 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1163,8 +1163,8 @@ describe Ci::Pipeline, :mailer do context "from #{status}" do let(:from_status) { status } - it 'schedules pipeline success worker' do - expect(Ci::DailyReportResultsWorker).to receive(:perform_in).with(10.minutes, pipeline.id) + it 'schedules daily build group report results worker' do + expect(Ci::DailyBuildGroupReportResultsWorker).to receive(:perform_in).with(10.minutes, pipeline.id) pipeline.succeed end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 13f1bcb389a..5a9ca9f7b7e 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -655,26 +655,4 @@ describe GroupPolicy do end end end - - it_behaves_like 'model with wiki policies' do - let(:container) { create(:group) } - - def set_access_level(access_level) - allow(container).to receive(:wiki_access_level).and_return(access_level) - end - - before do - stub_feature_flags(group_wiki: true) - end - - context 'when the feature flag is disabled' do - before do - stub_feature_flags(group_wiki: false) - end - - it 'does not include the wiki permissions' do - expect_disallowed(*permissions) - end - end - end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index f214b1ccf17..4e15af7e0b5 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -124,6 +124,7 @@ describe ProjectPolicy do it_behaves_like 'model with wiki policies' do let(:container) { project } + let_it_be(:user) { owner } def set_access_level(access_level) project.project_feature.update_attribute(:wiki_access_level, access_level) diff --git a/spec/services/ci/daily_report_result_service_spec.rb b/spec/services/ci/daily_build_group_report_result_service_spec.rb index 240709bab0b..f0b72b8fd86 100644 --- a/spec/services/ci/daily_report_result_service_spec.rb +++ b/spec/services/ci/daily_build_group_report_result_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Ci::DailyReportResultService, '#execute' do +describe Ci::DailyBuildGroupReportResultService, '#execute' do let!(:pipeline) { create(:ci_pipeline, created_at: '2020-02-06 00:01:10') } let!(:rspec_job) { create(:ci_build, pipeline: pipeline, name: '3/3 rspec', coverage: 80) } let!(:karma_job) { create(:ci_build, pipeline: pipeline, name: '2/2 karma', coverage: 90) } @@ -11,31 +11,29 @@ describe Ci::DailyReportResultService, '#execute' do it 'creates daily code coverage record for each job in the pipeline that has coverage value' do described_class.new.execute(pipeline) - Ci::DailyReportResult.find_by(title: 'rspec').tap do |coverage| + Ci::DailyBuildGroupReportResult.find_by(group_name: 'rspec').tap do |coverage| expect(coverage).to have_attributes( project_id: pipeline.project.id, last_pipeline_id: pipeline.id, ref_path: pipeline.source_ref_path, - param_type: 'coverage', - title: rspec_job.group_name, - value: rspec_job.coverage, + group_name: rspec_job.group_name, + data: { 'coverage' => rspec_job.coverage }, date: pipeline.created_at.to_date ) end - Ci::DailyReportResult.find_by(title: 'karma').tap do |coverage| + Ci::DailyBuildGroupReportResult.find_by(group_name: 'karma').tap do |coverage| expect(coverage).to have_attributes( project_id: pipeline.project.id, last_pipeline_id: pipeline.id, ref_path: pipeline.source_ref_path, - param_type: 'coverage', - title: karma_job.group_name, - value: karma_job.coverage, + group_name: karma_job.group_name, + data: { 'coverage' => karma_job.coverage }, date: pipeline.created_at.to_date ) end - expect(Ci::DailyReportResult.find_by(title: 'extra')).to be_nil + expect(Ci::DailyBuildGroupReportResult.find_by(group_name: 'extra')).to be_nil end context 'when there are multiple builds with the same group name that report coverage' do @@ -45,14 +43,13 @@ describe Ci::DailyReportResultService, '#execute' do it 'creates daily code coverage record with the average as the value' do described_class.new.execute(pipeline) - Ci::DailyReportResult.find_by(title: 'test').tap do |coverage| + Ci::DailyBuildGroupReportResult.find_by(group_name: 'test').tap do |coverage| expect(coverage).to have_attributes( project_id: pipeline.project.id, last_pipeline_id: pipeline.id, ref_path: pipeline.source_ref_path, - param_type: 'coverage', - title: test_job_2.group_name, - value: 75, + group_name: test_job_2.group_name, + data: { 'coverage' => 75.0 }, date: pipeline.created_at.to_date ) end @@ -77,8 +74,8 @@ describe Ci::DailyReportResultService, '#execute' do end it "updates the existing record's coverage value and last_pipeline_id" do - rspec_coverage = Ci::DailyReportResult.find_by(title: 'rspec') - karma_coverage = Ci::DailyReportResult.find_by(title: 'karma') + rspec_coverage = Ci::DailyBuildGroupReportResult.find_by(group_name: 'rspec') + karma_coverage = Ci::DailyBuildGroupReportResult.find_by(group_name: 'karma') # Bump up the coverage values described_class.new.execute(new_pipeline) @@ -88,12 +85,12 @@ describe Ci::DailyReportResultService, '#execute' do expect(rspec_coverage).to have_attributes( last_pipeline_id: new_pipeline.id, - value: new_rspec_job.coverage + data: { 'coverage' => new_rspec_job.coverage } ) expect(karma_coverage).to have_attributes( last_pipeline_id: new_pipeline.id, - value: new_karma_job.coverage + data: { 'coverage' => new_karma_job.coverage } ) end end @@ -117,8 +114,8 @@ describe Ci::DailyReportResultService, '#execute' do end it 'updates the existing daily code coverage records' do - rspec_coverage = Ci::DailyReportResult.find_by(title: 'rspec') - karma_coverage = Ci::DailyReportResult.find_by(title: 'karma') + rspec_coverage = Ci::DailyBuildGroupReportResult.find_by(group_name: 'rspec') + karma_coverage = Ci::DailyBuildGroupReportResult.find_by(group_name: 'karma') # Run another one but for the older pipeline. # This simulates the scenario wherein the success worker @@ -135,12 +132,12 @@ describe Ci::DailyReportResultService, '#execute' do expect(rspec_coverage).to have_attributes( last_pipeline_id: pipeline.id, - value: rspec_job.coverage + data: { 'coverage' => rspec_job.coverage } ) expect(karma_coverage).to have_attributes( last_pipeline_id: pipeline.id, - value: karma_job.coverage + data: { 'coverage' => karma_job.coverage } ) end end diff --git a/spec/services/groups/import_export/export_service_spec.rb b/spec/services/groups/import_export/export_service_spec.rb index f77b5a2e5b9..e9e356ab4f6 100644 --- a/spec/services/groups/import_export/export_service_spec.rb +++ b/spec/services/groups/import_export/export_service_spec.rb @@ -50,7 +50,7 @@ describe Groups::ImportExport::ExportService do end it 'saves the models using ndjson tree saver' do - stub_feature_flags(group_import_export_ndjson: true) + stub_feature_flags(group_export_ndjson: true) expect(Gitlab::ImportExport::Group::TreeSaver).to receive(:new).and_call_original @@ -58,7 +58,7 @@ describe Groups::ImportExport::ExportService do end it 'saves the models using legacy tree saver' do - stub_feature_flags(group_import_export_ndjson: false) + stub_feature_flags(group_export_ndjson: false) expect(Gitlab::ImportExport::Group::LegacyTreeSaver).to receive(:new).and_call_original diff --git a/spec/services/groups/import_export/import_service_spec.rb b/spec/services/groups/import_export/import_service_spec.rb index cd7ad1a1cfa..256e0a1b3c5 100644 --- a/spec/services/groups/import_export/import_service_spec.rb +++ b/spec/services/groups/import_export/import_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Groups::ImportExport::ImportService do - context 'with group_import_export_ndjson feature flag disabled' do + context 'with group_import_ndjson feature flag disabled' do let(:user) { create(:admin) } let(:group) { create(:group) } let(:import_logger) { instance_double(Gitlab::Import::Logger) } @@ -11,7 +11,7 @@ describe Groups::ImportExport::ImportService do subject(:service) { described_class.new(group: group, user: user) } before do - stub_feature_flags(group_import_export_ndjson: false) + stub_feature_flags(group_import_ndjson: false) ImportExportUpload.create(group: group, import_file: import_file) @@ -39,9 +39,9 @@ describe Groups::ImportExport::ImportService do end end - context 'with group_import_export_ndjson feature flag enabled' do + context 'with group_import_ndjson feature flag enabled' do before do - stub_feature_flags(group_import_export_ndjson: true) + stub_feature_flags(group_import_ndjson: true) end context 'when importing a ndjson export' do diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb index de64cea6474..a0d54666dff 100644 --- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -14,17 +14,16 @@ RSpec.shared_context 'GroupPolicy context' do %i[ read_label read_group upload_file read_namespace read_group_activity read_group_issues read_group_boards read_group_labels read_group_milestones - read_group_merge_requests read_wiki + read_group_merge_requests ] end let(:read_group_permissions) { %i[read_label read_list read_milestone read_board] } - let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation download_wiki_code] } - let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation create_wiki] } + let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation] } + let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation] } let(:maintainer_permissions) do %i[ create_projects read_cluster create_cluster update_cluster admin_cluster add_cluster - admin_wiki ] end let(:owner_permissions) do diff --git a/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb b/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb index b91500ffd9c..bd9e3a26f1e 100644 --- a/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb +++ b/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb @@ -1,152 +1,114 @@ # frozen_string_literal: true RSpec.shared_examples 'model with wiki policies' do - let(:container) { raise NotImplementedError } - let(:permissions) { %i(read_wiki create_wiki update_wiki admin_wiki download_wiki_code) } - - # TODO: Remove this helper once we implement group features - # https://gitlab.com/gitlab-org/gitlab/-/issues/208412 - def set_access_level(access_level) - raise NotImplementedError - end - - subject { described_class.new(owner, container) } - - context 'when the feature is disabled' do - before do - set_access_level(ProjectFeature::DISABLED) - end + include ProjectHelpers - it 'does not include the wiki permissions' do - expect_disallowed(*permissions) - end + let(:container) { raise NotImplementedError } + let(:user) { raise NotImplementedError } - context 'when there is an external wiki' do - it 'does not include the wiki permissions' do - allow(container).to receive(:has_external_wiki?).and_return(true) + subject { described_class.new(user, container) } - expect_disallowed(*permissions) - end + let_it_be(:wiki_permissions) do + {}.tap do |permissions| + permissions[:guest] = %i[read_wiki] + permissions[:reporter] = permissions[:guest] + %i[download_wiki_code] + permissions[:developer] = permissions[:reporter] + %i[create_wiki] + permissions[:maintainer] = permissions[:developer] + %i[admin_wiki] + permissions[:all] = permissions[:maintainer] end end - describe 'read_wiki' do - subject { described_class.new(user, container) } - - member_roles = %i[guest developer] - stranger_roles = %i[anonymous non_member] - - user_roles = stranger_roles + member_roles + using RSpec::Parameterized::TableSyntax + + where(:container_level, :access_level, :membership, :access) do + :public | :enabled | :admin | :all + :public | :enabled | :maintainer | :maintainer + :public | :enabled | :developer | :developer + :public | :enabled | :reporter | :reporter + :public | :enabled | :guest | :guest + :public | :enabled | :non_member | :guest + :public | :enabled | :anonymous | :guest + + :public | :private | :admin | :all + :public | :private | :maintainer | :maintainer + :public | :private | :developer | :developer + :public | :private | :reporter | :reporter + :public | :private | :guest | :guest + :public | :private | :non_member | nil + :public | :private | :anonymous | nil + + :public | :disabled | :admin | nil + :public | :disabled | :maintainer | nil + :public | :disabled | :developer | nil + :public | :disabled | :reporter | nil + :public | :disabled | :guest | nil + :public | :disabled | :non_member | nil + :public | :disabled | :anonymous | nil + + :internal | :enabled | :admin | :all + :internal | :enabled | :maintainer | :maintainer + :internal | :enabled | :developer | :developer + :internal | :enabled | :reporter | :reporter + :internal | :enabled | :guest | :guest + :internal | :enabled | :non_member | :guest + :internal | :enabled | :anonymous | nil + + :internal | :private | :admin | :all + :internal | :private | :maintainer | :maintainer + :internal | :private | :developer | :developer + :internal | :private | :reporter | :reporter + :internal | :private | :guest | :guest + :internal | :private | :non_member | nil + :internal | :private | :anonymous | nil + + :internal | :disabled | :admin | nil + :internal | :disabled | :maintainer | nil + :internal | :disabled | :developer | nil + :internal | :disabled | :reporter | nil + :internal | :disabled | :guest | nil + :internal | :disabled | :non_member | nil + :internal | :disabled | :anonymous | nil + + :private | :private | :admin | :all + :private | :private | :maintainer | :maintainer + :private | :private | :developer | :developer + :private | :private | :reporter | :reporter + :private | :private | :guest | :guest + :private | :private | :non_member | nil + :private | :private | :anonymous | nil + + :private | :disabled | :admin | nil + :private | :disabled | :maintainer | nil + :private | :disabled | :developer | nil + :private | :disabled | :reporter | nil + :private | :disabled | :guest | nil + :private | :disabled | :non_member | nil + :private | :disabled | :anonymous | nil + end - # When a user is anonymous, their `current_user == nil` - let(:user) { create(:user) unless user_role == :anonymous } + with_them do + let(:user) { create_user_from_membership(container, membership) } + let(:allowed_permissions) { wiki_permissions[access].dup || [] } + let(:disallowed_permissions) { wiki_permissions[:all] - allowed_permissions } before do - container.visibility = container_visibility - set_access_level(wiki_access_level) - container.add_user(user, user_role) if member_roles.include?(user_role) - end - - title = ->(container_visibility, wiki_access_level, user_role) do - [ - "container is #{Gitlab::VisibilityLevel.level_name container_visibility}", - "wiki is #{ProjectFeature.str_from_access_level wiki_access_level}", - "user is #{user_role}" - ].join(', ') - end - - describe 'Situations where :read_wiki is always false' do - where(case_names: title, - container_visibility: Gitlab::VisibilityLevel.options.values, - wiki_access_level: [ProjectFeature::DISABLED], - user_role: user_roles) - - with_them do - it { is_expected.to be_disallowed(:read_wiki) } - end - end - - describe 'Situations where :read_wiki is always true' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::PUBLIC], - wiki_access_level: [ProjectFeature::ENABLED], - user_role: user_roles) + container.visibility = container_level.to_s + set_access_level(ProjectFeature.access_level_from_str(access_level.to_s)) - with_them do - it { is_expected.to be_allowed(:read_wiki) } + if allowed_permissions.any? && [container_level, access_level, membership] != [:private, :private, :guest] + allowed_permissions << :download_wiki_code end end - describe 'Situations where :read_wiki requires membership' do - context 'the wiki is private, and the user is a member' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::PUBLIC, - Gitlab::VisibilityLevel::INTERNAL], - wiki_access_level: [ProjectFeature::PRIVATE], - user_role: member_roles) - - with_them do - it { is_expected.to be_allowed(:read_wiki) } - end - end - - context 'the wiki is private, and the user is not member' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::PUBLIC, - Gitlab::VisibilityLevel::INTERNAL], - wiki_access_level: [ProjectFeature::PRIVATE], - user_role: stranger_roles) - - with_them do - it { is_expected.to be_disallowed(:read_wiki) } - end - end - - context 'the wiki is enabled, and the user is a member' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::PRIVATE], - wiki_access_level: [ProjectFeature::ENABLED], - user_role: member_roles) - - with_them do - it { is_expected.to be_allowed(:read_wiki) } - end - end - - context 'the wiki is enabled, and the user is not a member' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::PRIVATE], - wiki_access_level: [ProjectFeature::ENABLED], - user_role: stranger_roles) - - with_them do - it { is_expected.to be_disallowed(:read_wiki) } - end - end + it 'allows actions based on membership' do + expect_allowed(*allowed_permissions) + expect_disallowed(*disallowed_permissions) end + end - describe 'Situations where :read_wiki prohibits anonymous access' do - context 'the user is not anonymous' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::INTERNAL], - wiki_access_level: [ProjectFeature::ENABLED, ProjectFeature::PUBLIC], - user_role: user_roles.reject { |u| u == :anonymous }) - - with_them do - it { is_expected.to be_allowed(:read_wiki) } - end - end - - context 'the user is anonymous' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::INTERNAL], - wiki_access_level: [ProjectFeature::ENABLED, ProjectFeature::PUBLIC], - user_role: %i[anonymous]) - - with_them do - it { is_expected.to be_disallowed(:read_wiki) } - end - end - end + # TODO: Remove this helper once we implement group features + # https://gitlab.com/gitlab-org/gitlab/-/issues/208412 + def set_access_level(access_level) + raise NotImplementedError end end diff --git a/spec/workers/ci/daily_report_results_worker_spec.rb b/spec/workers/ci/daily_build_group_report_results_worker_spec.rb index b6543b32b09..d9706982a62 100644 --- a/spec/workers/ci/daily_report_results_worker_spec.rb +++ b/spec/workers/ci/daily_build_group_report_results_worker_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Ci::DailyReportResultsWorker do +describe Ci::DailyBuildGroupReportResultsWorker do describe '#perform' do let!(:pipeline) { create(:ci_pipeline) } @@ -12,7 +12,7 @@ describe Ci::DailyReportResultsWorker do let(:pipeline_id) { pipeline.id } it 'executes service' do - expect_any_instance_of(Ci::DailyReportResultService) + expect_any_instance_of(Ci::DailyBuildGroupReportResultService) .to receive(:execute).with(pipeline) subject @@ -23,7 +23,7 @@ describe Ci::DailyReportResultsWorker do let(:pipeline_id) { 123 } it 'does not execute service' do - expect_any_instance_of(Ci::DailyReportResultService) + expect_any_instance_of(Ci::DailyBuildGroupReportResultService) .not_to receive(:execute) expect { subject } |