diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-15 15:10:08 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-15 15:10:08 +0000 |
commit | 06c127aa72cff78235426341081837cff0b6f78b (patch) | |
tree | 614c9d9e029adcac31f290d4f953fe8b02aaf0f1 /spec | |
parent | 33212c8ff1f99cdb896e8fc6f6450882287e0de5 (diff) | |
download | gitlab-ce-06c127aa72cff78235426341081837cff0b6f78b.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
19 files changed, 832 insertions, 126 deletions
diff --git a/spec/features/merge_request/user_expands_diff_spec.rb b/spec/features/merge_request/user_expands_diff_spec.rb index fc17ef011c2..b0cfa8d0d54 100644 --- a/spec/features/merge_request/user_expands_diff_spec.rb +++ b/spec/features/merge_request/user_expands_diff_spec.rb @@ -17,11 +17,11 @@ RSpec.describe 'User expands diff', :js do it 'allows user to expand diff' do page.within find('[id="19763941ab80e8c09871c0a425f0560d9053bcb3"]') do - click_link 'Click to expand it.' + find('[data-testid="expandButton"]').click wait_for_requests - expect(page).not_to have_content('Click to expand it.') + expect(page).not_to have_content('Expand File') expect(page).to have_selector('.code') end end diff --git a/spec/features/projects/releases/user_views_releases_spec.rb b/spec/features/projects/releases/user_views_releases_spec.rb index 962d5551631..993d3371904 100644 --- a/spec/features/projects/releases/user_views_releases_spec.rb +++ b/spec/features/projects/releases/user_views_releases_spec.rb @@ -13,37 +13,25 @@ RSpec.describe 'User views releases', :js do project.add_guest(guest) end - context('when the user is a maintainer') do - before do - gitlab_sign_in(maintainer) - end - - it 'sees the release' do - visit project_releases_path(project) - - expect(page).to have_content(release.name) - expect(page).to have_content(release.tag) - expect(page).not_to have_content('Upcoming Release') - end - - shared_examples 'asset link tests' do - context 'when there is a link as an asset' do - let!(:release_link) { create(:release_link, release: release, url: url ) } - let(:url) { "#{project.web_url}/-/jobs/1/artifacts/download" } - let(:direct_asset_link) { Gitlab::Routing.url_helpers.project_release_url(project, release) << release_link.filepath } + shared_examples 'releases page' do + context('when the user is a maintainer') do + before do + gitlab_sign_in(maintainer) + end - it 'sees the link' do - visit project_releases_path(project) + it 'sees the release' do + visit project_releases_path(project) - page.within('.js-assets-list') do - expect(page).to have_link release_link.name, href: direct_asset_link - expect(page).not_to have_css('[data-testid="external-link-indicator"]') - end - end + expect(page).to have_content(release.name) + expect(page).to have_content(release.tag) + expect(page).not_to have_content('Upcoming Release') + end - context 'when there is a link redirect' do - let!(:release_link) { create(:release_link, release: release, name: 'linux-amd64 binaries', filepath: '/binaries/linux-amd64', url: url) } + shared_examples 'asset link tests' do + context 'when there is a link as an asset' do + let!(:release_link) { create(:release_link, release: release, url: url ) } let(:url) { "#{project.web_url}/-/jobs/1/artifacts/download" } + let(:direct_asset_link) { Gitlab::Routing.url_helpers.project_release_url(project, release) << release_link.filepath } it 'sees the link' do visit project_releases_path(project) @@ -53,77 +41,103 @@ RSpec.describe 'User views releases', :js do expect(page).not_to have_css('[data-testid="external-link-indicator"]') end end - end - context 'when url points to external resource' do - let(:url) { 'http://google.com/download' } + context 'when there is a link redirect' do + let!(:release_link) { create(:release_link, release: release, name: 'linux-amd64 binaries', filepath: '/binaries/linux-amd64', url: url) } + let(:url) { "#{project.web_url}/-/jobs/1/artifacts/download" } - it 'sees that the link is external resource' do - visit project_releases_path(project) + it 'sees the link' do + visit project_releases_path(project) - page.within('.js-assets-list') do - expect(page).to have_css('[data-testid="external-link-indicator"]') + page.within('.js-assets-list') do + expect(page).to have_link release_link.name, href: direct_asset_link + expect(page).not_to have_css('[data-testid="external-link-indicator"]') + end + end + end + + context 'when url points to external resource' do + let(:url) { 'http://google.com/download' } + + it 'sees that the link is external resource' do + visit project_releases_path(project) + + page.within('.js-assets-list') do + expect(page).to have_css('[data-testid="external-link-indicator"]') + end end end end end - end - context 'when the release_asset_link_type feature flag is enabled' do - before do - stub_feature_flags(release_asset_link_type: true) + context 'when the release_asset_link_type feature flag is enabled' do + before do + stub_feature_flags(release_asset_link_type: true) + end + + it_behaves_like 'asset link tests' end - it_behaves_like 'asset link tests' - end + context 'when the release_asset_link_type feature flag is disabled' do + before do + stub_feature_flags(release_asset_link_type: false) + end - context 'when the release_asset_link_type feature flag is disabled' do - before do - stub_feature_flags(release_asset_link_type: false) + it_behaves_like 'asset link tests' end - it_behaves_like 'asset link tests' - end + context 'with an upcoming release' do + let(:tomorrow) { Time.zone.now + 1.day } + let!(:release) { create(:release, project: project, released_at: tomorrow ) } - context 'with an upcoming release' do - let(:tomorrow) { Time.zone.now + 1.day } - let!(:release) { create(:release, project: project, released_at: tomorrow ) } + it 'sees the upcoming tag' do + visit project_releases_path(project) - it 'sees the upcoming tag' do - visit project_releases_path(project) + expect(page).to have_content('Upcoming Release') + end + end + + context 'with a tag containing a slash' do + it 'sees the release' do + release = create :release, project: project, tag: 'debian/2.4.0-1' + visit project_releases_path(project) - expect(page).to have_content('Upcoming Release') + expect(page).to have_content(release.name) + expect(page).to have_content(release.tag) + end end end - context 'with a tag containing a slash' do - it 'sees the release' do - release = create :release, project: project, tag: 'debian/2.4.0-1' + context('when the user is a guest') do + before do + gitlab_sign_in(guest) + end + + it 'renders release info except for Git-related data' do visit project_releases_path(project) - expect(page).to have_content(release.name) - expect(page).to have_content(release.tag) + within('.release-block') do + expect(page).to have_content(release.description) + + # The following properties (sometimes) include Git info, + # so they are not rendered for Guest users + expect(page).not_to have_content(release.name) + expect(page).not_to have_content(release.tag) + expect(page).not_to have_content(release.commit.short_id) + end end end end - context('when the user is a guest') do + context 'when the graphql_releases_page feature flag is enabled' do + it_behaves_like 'releases page' + end + + context 'when the graphql_releases_page feature flag is disabled' do before do - gitlab_sign_in(guest) + stub_feature_flags(graphql_releases_page: false) end - it 'renders release info except for Git-related data' do - visit project_releases_path(project) - - within('.release-block') do - expect(page).to have_content(release.description) - - # The following properties (sometimes) include Git info, - # so they are not rendered for Guest users - expect(page).not_to have_content(release.name) - expect(page).not_to have_content(release.tag) - expect(page).not_to have_content(release.commit.short_id) - end - end + it_behaves_like 'releases page' end end diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js index 0b0a7f966c5..3c39dd2d385 100644 --- a/spec/frontend/diffs/components/diff_file_spec.js +++ b/spec/frontend/diffs/components/diff_file_spec.js @@ -90,8 +90,8 @@ describe('DiffFile', () => { vm.isCollapsed = true; vm.$nextTick(() => { - expect(vm.$el.innerText).toContain('This diff is collapsed'); - expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1); + expect(vm.$el.innerText).toContain('This file is collapsed.'); + expect(vm.$el.querySelector('[data-testid="expandButton"]')).not.toBeFalsy(); done(); }); @@ -102,8 +102,8 @@ describe('DiffFile', () => { vm.isCollapsed = true; vm.$nextTick(() => { - expect(vm.$el.innerText).toContain('This diff is collapsed'); - expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1); + expect(vm.$el.innerText).toContain('This file is collapsed.'); + expect(vm.$el.querySelector('[data-testid="expandButton"]')).not.toBeFalsy(); done(); }); @@ -121,8 +121,8 @@ describe('DiffFile', () => { vm.isCollapsed = true; vm.$nextTick(() => { - expect(vm.$el.innerText).toContain('This diff is collapsed'); - expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1); + expect(vm.$el.innerText).toContain('This file is collapsed.'); + expect(vm.$el.querySelector('[data-testid="expandButton"]')).not.toBeFalsy(); done(); }); @@ -135,7 +135,7 @@ describe('DiffFile', () => { vm.file.viewer.name = diffViewerModes.renamed; vm.$nextTick(() => { - expect(vm.$el.innerText).not.toContain('This diff is collapsed'); + expect(vm.$el.innerText).not.toContain('This file is collapsed.'); done(); }); @@ -148,7 +148,7 @@ describe('DiffFile', () => { vm.file.viewer.name = diffViewerModes.mode_changed; vm.$nextTick(() => { - expect(vm.$el.innerText).not.toContain('This diff is collapsed'); + expect(vm.$el.innerText).not.toContain('This file is collapsed.'); done(); }); diff --git a/spec/frontend/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js index 1eabd3083ab..f4095d4de96 100644 --- a/spec/frontend/issue_show/components/app_spec.js +++ b/spec/frontend/issue_show/components/app_spec.js @@ -78,6 +78,8 @@ describe('Issuable output', () => { }); mountComponent(); + + jest.advanceTimersByTime(2); }); afterEach(() => { diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js index fbfba2efb1d..c6034639a4a 100644 --- a/spec/frontend/notes/components/notes_app_spec.js +++ b/spec/frontend/notes/components/notes_app_spec.js @@ -330,6 +330,8 @@ describe('note_app', () => { wrapper.vm.$parent.$el.dispatchEvent(toggleAwardEvent); + jest.advanceTimersByTime(2); + expect(toggleAwardAction).toHaveBeenCalledTimes(1); const [, payload] = toggleAwardAction.mock.calls[0]; diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js index 52eea99ce8c..4681f3aa429 100644 --- a/spec/frontend/notes/stores/actions_spec.js +++ b/spec/frontend/notes/stores/actions_spec.js @@ -335,6 +335,9 @@ describe('Actions Notes Store', () => { it('calls service with last fetched state', done => { store .dispatch('poll') + .then(() => { + jest.advanceTimersByTime(2); + }) .then(() => new Promise(resolve => requestAnimationFrame(resolve))) .then(() => { expect(store.state.lastFetchedAt).toBe('123456'); diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js index 04934fb93b0..b7bc8d08a0f 100644 --- a/spec/frontend/pipelines/time_ago_spec.js +++ b/spec/frontend/pipelines/time_ago_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { GlIcon } from '@gitlab/ui'; import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue'; describe('Timeago component', () => { @@ -22,14 +23,19 @@ describe('Timeago component', () => { wrapper = null; }); + const duration = () => wrapper.find('.duration'); + const finishedAt = () => wrapper.find('.finished-at'); + describe('with duration', () => { beforeEach(() => { createComponent({ duration: 10, finishedTime: '' }); }); it('should render duration and timer svg', () => { - expect(wrapper.find('.duration').exists()).toBe(true); - expect(wrapper.find('.duration svg').exists()).toBe(true); + const icon = duration().find(GlIcon); + + expect(duration().exists()).toBe(true); + expect(icon.props('name')).toBe('timer'); }); }); @@ -39,7 +45,7 @@ describe('Timeago component', () => { }); it('should not render duration and timer svg', () => { - expect(wrapper.find('.duration').exists()).toBe(false); + expect(duration().exists()).toBe(false); }); }); @@ -49,9 +55,12 @@ describe('Timeago component', () => { }); it('should render time and calendar icon', () => { - expect(wrapper.find('.finished-at').exists()).toBe(true); - expect(wrapper.find('.finished-at i.fa-calendar').exists()).toBe(true); - expect(wrapper.find('.finished-at time').exists()).toBe(true); + const icon = finishedAt().find(GlIcon); + const time = finishedAt().find('time'); + + expect(finishedAt().exists()).toBe(true); + expect(icon.props('name')).toBe('calendar'); + expect(time.exists()).toBe(true); }); }); @@ -61,7 +70,7 @@ describe('Timeago component', () => { }); it('should not render time and calendar icon', () => { - expect(wrapper.find('.finished-at').exists()).toBe(false); + expect(finishedAt().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap new file mode 100644 index 00000000000..f56e296d106 --- /dev/null +++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap @@ -0,0 +1,113 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`releases/util.js convertGraphQLResponse matches snapshot 1`] = ` +Object { + "data": Array [ + Object { + "_links": Object { + "editUrl": "http://0.0.0.0:3000/root/release-test/-/releases/v5.10/edit", + "issuesUrl": null, + "mergeRequestsUrl": null, + "self": "http://0.0.0.0:3000/root/release-test/-/releases/v5.10", + "selfUrl": "http://0.0.0.0:3000/root/release-test/-/releases/v5.10", + }, + "assets": Object { + "count": 7, + "links": Array [ + Object { + "directAssetUrl": "http://0.0.0.0:3000/root/release-test/-/releases/v5.32/permanent/path/to/runbook", + "external": true, + "id": "gid://gitlab/Releases::Link/69", + "linkType": "other", + "name": "An example link", + "url": "https://example.com/link", + }, + Object { + "directAssetUrl": "https://example.com/package", + "external": true, + "id": "gid://gitlab/Releases::Link/68", + "linkType": "package", + "name": "An example package link", + "url": "https://example.com/package", + }, + Object { + "directAssetUrl": "https://example.com/image", + "external": true, + "id": "gid://gitlab/Releases::Link/67", + "linkType": "image", + "name": "An example image", + "url": "https://example.com/image", + }, + ], + "sources": Array [ + Object { + "format": "zip", + "url": "http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.zip", + }, + Object { + "format": "tar.gz", + "url": "http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar.gz", + }, + Object { + "format": "tar.bz2", + "url": "http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar.bz2", + }, + Object { + "format": "tar", + "url": "http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar", + }, + ], + }, + "author": Object { + "avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png", + "username": "root", + "webUrl": "http://0.0.0.0:3000/root", + }, + "commit": Object { + "shortId": "92e7ea2e", + "title": "Testing a change.", + }, + "commitPath": "http://0.0.0.0:3000/root/release-test/-/commit/92e7ea2ee4496fe0d00ff69830ba0564d3d1e5a7", + "descriptionHtml": "<p data-sourcepos=\\"1:1-1:24\\" dir=\\"auto\\">This is version <strong>1.0</strong>!</p>", + "evidences": Array [ + Object { + "collectedAt": "2020-08-21T20:15:19Z", + "filepath": "http://0.0.0.0:3000/root/release-test/-/releases/v5.10/evidences/34.json", + "sha": "22bde8e8b93d870a29ddc339287a1fbb598f45d1396d", + }, + ], + "milestones": Array [ + Object { + "description": "", + "id": "gid://gitlab/Milestone/60", + "issueStats": Object { + "closed": 0, + "total": 0, + }, + "stats": undefined, + "title": "12.4", + "webPath": undefined, + "webUrl": "/root/release-test/-/milestones/2", + }, + Object { + "description": "Milestone 12.3", + "id": "gid://gitlab/Milestone/59", + "issueStats": Object { + "closed": 1, + "total": 2, + }, + "stats": undefined, + "title": "12.3", + "webPath": undefined, + "webUrl": "/root/release-test/-/milestones/1", + }, + ], + "name": "Release 1.0", + "releasedAt": "2020-08-21T20:15:18Z", + "tagName": "v5.10", + "tagPath": "/root/release-test/-/tags/v5.10", + "upcomingRelease": false, + }, + ], +} +`; diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js index 0ffece36d90..1f485cb276e 100644 --- a/spec/frontend/releases/components/app_index_spec.js +++ b/spec/frontend/releases/components/app_index_spec.js @@ -20,6 +20,7 @@ localVue.use(Vuex); describe('Releases App ', () => { let wrapper; + let fetchReleaseSpy; const releasesPagination = rge(21).map(index => ({ ...convertObjectPropsToCamelCase(release, { deep: true }), @@ -28,12 +29,22 @@ describe('Releases App ', () => { const defaultProps = { projectId: 'gitlab-ce', + projectPath: 'gitlab-org/gitlab-ce', documentationPath: 'help/releases', illustrationPath: 'illustration/path', }; const createComponent = (propsData = defaultProps) => { - const store = createStore({ modules: { list: listModule } }); + fetchReleaseSpy = jest.spyOn(listModule.actions, 'fetchReleases'); + + const store = createStore({ + modules: { list: listModule }, + featureFlags: { + graphqlReleaseData: true, + graphqlReleasesPage: false, + graphqlMilestoneStats: true, + }, + }); wrapper = shallowMount(ReleasesApp, { store, @@ -46,6 +57,25 @@ describe('Releases App ', () => { wrapper.destroy(); }); + describe('on startup', () => { + beforeEach(() => { + jest + .spyOn(api, 'releases') + .mockResolvedValue({ data: releases, headers: pageInfoHeadersWithoutPagination }); + + createComponent(); + }); + + it('calls fetchRelease with the page, project ID, and project path', () => { + expect(fetchReleaseSpy).toHaveBeenCalledTimes(1); + expect(fetchReleaseSpy).toHaveBeenCalledWith(expect.anything(), { + page: null, + projectId: defaultProps.projectId, + projectPath: defaultProps.projectPath, + }); + }); + }); + describe('while loading', () => { beforeEach(() => { jest diff --git a/spec/frontend/releases/mock_data.js b/spec/frontend/releases/mock_data.js index b97385154bd..58cd69a2f6a 100644 --- a/spec/frontend/releases/mock_data.js +++ b/spec/frontend/releases/mock_data.js @@ -222,3 +222,131 @@ export const release2 = { }; export const releases = [release, release2]; + +export const graphqlReleasesResponse = { + data: { + project: { + releases: { + count: 39, + nodes: [ + { + name: 'Release 1.0', + tagName: 'v5.10', + tagPath: '/root/release-test/-/tags/v5.10', + descriptionHtml: + '<p data-sourcepos="1:1-1:24" dir="auto">This is version <strong>1.0</strong>!</p>', + releasedAt: '2020-08-21T20:15:18Z', + upcomingRelease: false, + assets: { + count: 7, + sources: { + nodes: [ + { + format: 'zip', + url: + 'http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.zip', + }, + { + format: 'tar.gz', + url: + 'http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar.gz', + }, + { + format: 'tar.bz2', + url: + 'http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar.bz2', + }, + { + format: 'tar', + url: + 'http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar', + }, + ], + }, + links: { + nodes: [ + { + id: 'gid://gitlab/Releases::Link/69', + name: 'An example link', + url: 'https://example.com/link', + directAssetUrl: + 'http://0.0.0.0:3000/root/release-test/-/releases/v5.32/permanent/path/to/runbook', + linkType: 'OTHER', + external: true, + }, + { + id: 'gid://gitlab/Releases::Link/68', + name: 'An example package link', + url: 'https://example.com/package', + directAssetUrl: 'https://example.com/package', + linkType: 'PACKAGE', + external: true, + }, + { + id: 'gid://gitlab/Releases::Link/67', + name: 'An example image', + url: 'https://example.com/image', + directAssetUrl: 'https://example.com/image', + linkType: 'IMAGE', + external: true, + }, + ], + }, + }, + evidences: { + nodes: [ + { + filepath: + 'http://0.0.0.0:3000/root/release-test/-/releases/v5.10/evidences/34.json', + collectedAt: '2020-08-21T20:15:19Z', + sha: '22bde8e8b93d870a29ddc339287a1fbb598f45d1396d', + }, + ], + }, + links: { + editUrl: 'http://0.0.0.0:3000/root/release-test/-/releases/v5.10/edit', + issuesUrl: null, + mergeRequestsUrl: null, + selfUrl: 'http://0.0.0.0:3000/root/release-test/-/releases/v5.10', + }, + commit: { + sha: '92e7ea2ee4496fe0d00ff69830ba0564d3d1e5a7', + webUrl: + 'http://0.0.0.0:3000/root/release-test/-/commit/92e7ea2ee4496fe0d00ff69830ba0564d3d1e5a7', + title: 'Testing a change.', + }, + author: { + webUrl: 'http://0.0.0.0:3000/root', + avatarUrl: '/uploads/-/system/user/avatar/1/avatar.png', + username: 'root', + }, + milestones: { + nodes: [ + { + id: 'gid://gitlab/Milestone/60', + title: '12.4', + description: '', + webPath: '/root/release-test/-/milestones/2', + stats: { + totalIssuesCount: 0, + closedIssuesCount: 0, + }, + }, + { + id: 'gid://gitlab/Milestone/59', + title: '12.3', + description: 'Milestone 12.3', + webPath: '/root/release-test/-/milestones/1', + stats: { + totalIssuesCount: 2, + closedIssuesCount: 1, + }, + }, + ], + }, + }, + ], + }, + }, + }, +}; diff --git a/spec/frontend/releases/stores/modules/list/actions_spec.js b/spec/frontend/releases/stores/modules/list/actions_spec.js index 4c3af157684..88653d3502f 100644 --- a/spec/frontend/releases/stores/modules/list/actions_spec.js +++ b/spec/frontend/releases/stores/modules/list/actions_spec.js @@ -1,3 +1,4 @@ +import { cloneDeep } from 'lodash'; import testAction from 'helpers/vuex_action_helper'; import { requestReleases, @@ -8,18 +9,36 @@ import { import state from '~/releases/stores/modules/list/state'; import * as types from '~/releases/stores/modules/list/mutation_types'; import api from '~/api'; +import { gqClient, convertGraphQLResponse } from '~/releases/util'; import { parseIntPagination, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { pageInfoHeadersWithoutPagination, releases as originalReleases } from '../../../mock_data'; +import { + pageInfoHeadersWithoutPagination, + releases as originalReleases, + graphqlReleasesResponse as originalGraphqlReleasesResponse, +} from '../../../mock_data'; +import allReleasesQuery from '~/releases/queries/all_releases.query.graphql'; describe('Releases State actions', () => { let mockedState; let pageInfo; let releases; + let graphqlReleasesResponse; + let projectPath; beforeEach(() => { - mockedState = state(); + mockedState = { + ...state(), + featureFlags: { + graphqlReleaseData: true, + graphqlReleasesPage: true, + graphqlMilestoneStats: true, + }, + }; + pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination); releases = convertObjectPropsToCamelCase(originalReleases, { deep: true }); + graphqlReleasesResponse = cloneDeep(originalGraphqlReleasesResponse); + projectPath = 'root/test-project'; }); describe('requestReleases', () => { @@ -31,39 +50,17 @@ describe('Releases State actions', () => { describe('fetchReleases', () => { describe('success', () => { it('dispatches requestReleases and receiveReleasesSuccess', done => { - jest.spyOn(api, 'releases').mockImplementation((id, options) => { - expect(id).toEqual(1); - expect(options.page).toEqual('1'); - return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination }); - }); - - testAction( - fetchReleases, - { projectId: 1 }, - mockedState, - [], - [ - { - type: 'requestReleases', - }, - { - payload: { data: releases, headers: pageInfoHeadersWithoutPagination }, - type: 'receiveReleasesSuccess', - }, - ], - done, - ); - }); - - it('dispatches requestReleases and receiveReleasesSuccess on page two', done => { - jest.spyOn(api, 'releases').mockImplementation((_, options) => { - expect(options.page).toEqual('2'); - return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination }); + jest.spyOn(gqClient, 'query').mockImplementation(({ query, variables }) => { + expect(query).toEqual(allReleasesQuery); + expect(variables).toEqual({ + fullPath: projectPath, + }); + return Promise.resolve(graphqlReleasesResponse); }); testAction( fetchReleases, - { page: '2', projectId: 1 }, + { projectPath }, mockedState, [], [ @@ -71,7 +68,7 @@ describe('Releases State actions', () => { type: 'requestReleases', }, { - payload: { data: releases, headers: pageInfoHeadersWithoutPagination }, + payload: convertGraphQLResponse(graphqlReleasesResponse), type: 'receiveReleasesSuccess', }, ], @@ -82,11 +79,11 @@ describe('Releases State actions', () => { describe('error', () => { it('dispatches requestReleases and receiveReleasesError', done => { - jest.spyOn(api, 'releases').mockReturnValue(Promise.reject()); + jest.spyOn(gqClient, 'query').mockRejectedValue(); testAction( fetchReleases, - { projectId: null }, + { projectPath }, mockedState, [], [ @@ -101,6 +98,85 @@ describe('Releases State actions', () => { ); }); }); + + describe('when the graphqlReleaseData feature flag is disabled', () => { + beforeEach(() => { + mockedState.featureFlags.graphqlReleasesPage = false; + }); + + describe('success', () => { + it('dispatches requestReleases and receiveReleasesSuccess', done => { + jest.spyOn(api, 'releases').mockImplementation((id, options) => { + expect(id).toEqual(1); + expect(options.page).toEqual('1'); + return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination }); + }); + + testAction( + fetchReleases, + { projectId: 1 }, + mockedState, + [], + [ + { + type: 'requestReleases', + }, + { + payload: { data: releases, headers: pageInfoHeadersWithoutPagination }, + type: 'receiveReleasesSuccess', + }, + ], + done, + ); + }); + + it('dispatches requestReleases and receiveReleasesSuccess on page two', done => { + jest.spyOn(api, 'releases').mockImplementation((_, options) => { + expect(options.page).toEqual('2'); + return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination }); + }); + + testAction( + fetchReleases, + { page: '2', projectId: 1 }, + mockedState, + [], + [ + { + type: 'requestReleases', + }, + { + payload: { data: releases, headers: pageInfoHeadersWithoutPagination }, + type: 'receiveReleasesSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + it('dispatches requestReleases and receiveReleasesError', done => { + jest.spyOn(api, 'releases').mockReturnValue(Promise.reject()); + + testAction( + fetchReleases, + { projectId: null }, + mockedState, + [], + [ + { + type: 'requestReleases', + }, + { + type: 'receiveReleasesError', + }, + ], + done, + ); + }); + }); + }); }); describe('receiveReleasesSuccess', () => { diff --git a/spec/frontend/releases/util_spec.js b/spec/frontend/releases/util_spec.js index 90aa9c4c7d8..f40e5729188 100644 --- a/spec/frontend/releases/util_spec.js +++ b/spec/frontend/releases/util_spec.js @@ -1,4 +1,6 @@ -import { releaseToApiJson, apiJsonToRelease } from '~/releases/util'; +import { cloneDeep } from 'lodash'; +import { releaseToApiJson, apiJsonToRelease, convertGraphQLResponse } from '~/releases/util'; +import { graphqlReleasesResponse as originalGraphqlReleasesResponse } from './mock_data'; describe('releases/util.js', () => { describe('releaseToApiJson', () => { @@ -100,4 +102,55 @@ describe('releases/util.js', () => { expect(apiJsonToRelease(json)).toEqual(expectedRelease); }); }); + + describe('convertGraphQLResponse', () => { + let graphqlReleasesResponse; + let converted; + + beforeEach(() => { + graphqlReleasesResponse = cloneDeep(originalGraphqlReleasesResponse); + converted = convertGraphQLResponse(graphqlReleasesResponse); + }); + + it('matches snapshot', () => { + expect(converted).toMatchSnapshot(); + }); + + describe('assets', () => { + it("handles asset links that don't have a linkType", () => { + expect(converted.data[0].assets.links[0].linkType).not.toBeUndefined(); + + delete graphqlReleasesResponse.data.project.releases.nodes[0].assets.links.nodes[0] + .linkType; + + converted = convertGraphQLResponse(graphqlReleasesResponse); + + expect(converted.data[0].assets.links[0].linkType).toBeUndefined(); + }); + }); + + describe('_links', () => { + it("handles releases that don't have any links", () => { + expect(converted.data[0]._links.selfUrl).not.toBeUndefined(); + + delete graphqlReleasesResponse.data.project.releases.nodes[0].links; + + converted = convertGraphQLResponse(graphqlReleasesResponse); + + expect(converted.data[0]._links.selfUrl).toBeUndefined(); + }); + }); + + describe('commit', () => { + it("handles releases that don't have any commit info", () => { + expect(converted.data[0].commit).not.toBeUndefined(); + + delete graphqlReleasesResponse.data.project.releases.nodes[0].commit; + + converted = convertGraphQLResponse(graphqlReleasesResponse); + + expect(converted.data[0].commit).toBeUndefined(); + }); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js index 0eb90f5529d..e0a3208cac9 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js @@ -1,6 +1,7 @@ import { mockLabels } from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data'; import Api from '~/api'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; +import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; @@ -33,6 +34,8 @@ export const mockAuthor3 = { export const mockAuthors = [mockAuthor1, mockAuthor2, mockAuthor3]; +export const mockBranches = [{ name: 'Master' }, { name: 'v1.x' }, { name: 'my-Branch' }]; + export const mockRegularMilestone = { id: 1, name: '4.0', @@ -55,6 +58,16 @@ export const mockMilestones = [ mockEscapedMilestone, ]; +export const mockBranchToken = { + type: 'source_branch', + icon: 'branch', + title: 'Source Branch', + unique: true, + token: BranchToken, + operators: [{ value: '=', description: 'is', default: 'true' }], + fetchBranches: Api.branches.bind(Api), +}; + export const mockAuthorToken = { type: 'author_username', icon: 'user', diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js new file mode 100644 index 00000000000..12b7fd58670 --- /dev/null +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js @@ -0,0 +1,207 @@ +import { mount } from '@vue/test-utils'; +import { + GlFilteredSearchToken, + GlFilteredSearchSuggestion, + GlFilteredSearchTokenSegment, + GlDropdownDivider, +} from '@gitlab/ui'; +import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; + +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import { + DEFAULT_LABEL_NONE, + DEFAULT_LABEL_ANY, +} from '~/vue_shared/components/filtered_search_bar/constants'; +import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue'; + +import { mockBranches, mockBranchToken } from '../mock_data'; + +jest.mock('~/flash'); +const defaultStubs = { + Portal: true, + GlFilteredSearchSuggestionList: { + template: '<div></div>', + methods: { + getValue: () => '=', + }, + }, +}; + +function createComponent(options = {}) { + const { + config = mockBranchToken, + value = { data: '' }, + active = false, + stubs = defaultStubs, + } = options; + return mount(BranchToken, { + propsData: { + config, + value, + active, + }, + provide: { + portalName: 'fake target', + alignSuggestions: function fakeAlignSuggestions() {}, + }, + stubs, + }); +} + +describe('BranchToken', () => { + let mock; + let wrapper; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + wrapper.destroy(); + }); + + describe('computed', () => { + beforeEach(async () => { + wrapper = createComponent({ value: { data: mockBranches[0].name } }); + + wrapper.setData({ + branches: mockBranches, + }); + + await wrapper.vm.$nextTick(); + }); + + describe('currentValue', () => { + it('returns lowercase string for `value.data`', () => { + expect(wrapper.vm.currentValue).toBe('master'); + }); + }); + + describe('activeBranch', () => { + it('returns object for currently present `value.data`', () => { + expect(wrapper.vm.activeBranch).toEqual(mockBranches[0]); + }); + }); + }); + + describe('methods', () => { + beforeEach(() => { + wrapper = createComponent(); + }); + + describe('fetchBranchBySearchTerm', () => { + it('calls `config.fetchBranches` with provided searchTerm param', () => { + jest.spyOn(wrapper.vm.config, 'fetchBranches'); + + wrapper.vm.fetchBranchBySearchTerm('foo'); + + expect(wrapper.vm.config.fetchBranches).toHaveBeenCalledWith('foo'); + }); + + it('sets response to `branches` when request is succesful', () => { + jest.spyOn(wrapper.vm.config, 'fetchBranches').mockResolvedValue({ data: mockBranches }); + + wrapper.vm.fetchBranchBySearchTerm('foo'); + + return waitForPromises().then(() => { + expect(wrapper.vm.branches).toEqual(mockBranches); + }); + }); + + it('calls `createFlash` with flash error message when request fails', () => { + jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({}); + + wrapper.vm.fetchBranchBySearchTerm('foo'); + + return waitForPromises().then(() => { + expect(createFlash).toHaveBeenCalledWith({ + message: 'There was a problem fetching branches.', + }); + }); + }); + + it('sets `loading` to false when request completes', () => { + jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({}); + + wrapper.vm.fetchBranchBySearchTerm('foo'); + + return waitForPromises().then(() => { + expect(wrapper.vm.loading).toBe(false); + }); + }); + }); + }); + + describe('template', () => { + const defaultBranches = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY]; + async function showSuggestions() { + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const suggestionsSegment = tokenSegments.at(2); + suggestionsSegment.vm.$emit('activate'); + await wrapper.vm.$nextTick(); + } + + beforeEach(async () => { + wrapper = createComponent({ value: { data: mockBranches[0].name } }); + + wrapper.setData({ + branches: mockBranches, + }); + + await wrapper.vm.$nextTick(); + }); + + it('renders gl-filtered-search-token component', () => { + expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true); + }); + + it('renders token item when value is selected', () => { + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + + expect(tokenSegments).toHaveLength(3); + expect(tokenSegments.at(2).text()).toBe(mockBranches[0].name); + }); + + it('renders provided defaultBranches as suggestions', async () => { + wrapper = createComponent({ + active: true, + config: { ...mockBranchToken, defaultBranches }, + stubs: { Portal: true }, + }); + await showSuggestions(); + const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + + expect(suggestions).toHaveLength(defaultBranches.length); + defaultBranches.forEach((branch, index) => { + expect(suggestions.at(index).text()).toBe(branch.text); + }); + }); + + it('does not render divider when no defaultBranches', async () => { + wrapper = createComponent({ + active: true, + config: { ...mockBranchToken, defaultBranches: [] }, + stubs: { Portal: true }, + }); + await showSuggestions(); + + expect(wrapper.contains(GlFilteredSearchSuggestion)).toBe(false); + expect(wrapper.contains(GlDropdownDivider)).toBe(false); + }); + + it('renders no suggestions as default', async () => { + wrapper = createComponent({ + active: true, + config: { ...mockBranchToken }, + stubs: { Portal: true }, + }); + await showSuggestions(); + const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + + expect(suggestions).toHaveLength(0); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js index 57d4ff93eea..b67f4cf12bf 100644 --- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js +++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js @@ -34,12 +34,25 @@ describe('SuggestionDiffRow', () => { const findOldLineWrapper = () => wrapper.find('.old_line'); const findNewLineWrapper = () => wrapper.find('.new_line'); + const findSuggestionContent = () => wrapper.find('[data-testid="suggestion-diff-content"]'); afterEach(() => { wrapper.destroy(); }); describe('renders correctly', () => { + it('renders the correct base suggestion markup', () => { + factory({ + propsData: { + line: oldLine, + }, + }); + + expect(findSuggestionContent().html()).toBe( + '<td data-testid="suggestion-diff-content" class="line_content old"><span class="line">oldrichtext</span></td>', + ); + }); + it('has the right classes on the wrapper', () => { factory({ propsData: { @@ -48,6 +61,11 @@ describe('SuggestionDiffRow', () => { }); expect(wrapper.classes()).toContain('line_holder'); + expect( + findSuggestionContent() + .find('span') + .classes(), + ).toContain('line'); }); it('renders the rich text when it is available', () => { diff --git a/spec/helpers/releases_helper_spec.rb b/spec/helpers/releases_helper_spec.rb index 6ae99648ff3..f10a2ed8e60 100644 --- a/spec/helpers/releases_helper_spec.rb +++ b/spec/helpers/releases_helper_spec.rb @@ -20,7 +20,7 @@ RSpec.describe ReleasesHelper do let(:release) { create(:release, project: project) } let(:user) { create(:user) } let(:can_user_create_release) { false } - let(:common_keys) { [:project_id, :illustration_path, :documentation_path] } + let(:common_keys) { [:project_id, :project_path, :illustration_path, :documentation_path] } # rubocop: disable CodeReuse/ActiveRecord before do diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/none_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/none_spec.rb new file mode 100644 index 00000000000..3250c7cfa31 --- /dev/null +++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/none_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::Strategies::None do + let(:fake_duplicate_job) do + instance_double(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob) + end + + subject(:strategy) { described_class.new(fake_duplicate_job) } + + describe '#schedule' do + it 'yields without checking for duplicates', :aggregate_failures do + expect(fake_duplicate_job).not_to receive(:scheduled?) + expect(fake_duplicate_job).not_to receive(:duplicate?) + expect(fake_duplicate_job).not_to receive(:check!) + + expect { |b| strategy.schedule({}, &b) }.to yield_control + end + end + + describe '#perform' do + it 'does not delete any locks before executing', :aggregate_failures do + expect(fake_duplicate_job).not_to receive(:delete!) + + expect { |b| strategy.perform({}, &b) }.to yield_control + end + end +end diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies_spec.rb index 5d37e3cb1ae..84856238aab 100644 --- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies_spec.rb @@ -8,6 +8,10 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::Strategies do expect(described_class.for(:until_executing)).to eq(described_class::UntilExecuting) end + it 'returns the right class for `none`' do + expect(described_class.for(:none)).to eq(described_class::None) + end + it 'raises an UnknownStrategyError when passing an unknown key' do expect { described_class.for(:unknown) }.to raise_error(described_class::UnknownStrategyError) end diff --git a/spec/services/admin/propagate_integration_service_spec.rb b/spec/services/admin/propagate_integration_service_spec.rb index 8b895cfb449..49d974b7154 100644 --- a/spec/services/admin/propagate_integration_service_spec.rb +++ b/spec/services/admin/propagate_integration_service_spec.rb @@ -10,8 +10,9 @@ RSpec.describe Admin::PropagateIntegrationService do stub_jira_service_test end - let(:excluded_attributes) { %w[id project_id inherit_from_id instance created_at updated_at default] } + let(:excluded_attributes) { %w[id project_id group_id inherit_from_id instance created_at updated_at default] } let!(:project) { create(:project) } + let!(:group) { create(:group) } let!(:instance_integration) do JiraService.create!( instance: true, @@ -109,6 +110,10 @@ RSpec.describe Admin::PropagateIntegrationService do it_behaves_like 'inherits settings from integration' do let(:integration) { project.jira_service } end + + it_behaves_like 'inherits settings from integration' do + let(:integration) { Service.find_by(group_id: group.id) } + end end it 'updates project#has_external_issue_tracker for issue tracker services' do |