diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-27 15:16:04 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-27 15:16:04 +0300 |
commit | 5cb0fa35e709bcd7f9d69e050010e44092a48623 (patch) | |
tree | 9a10a2a58b3129b6dae59d2aa320f00b1e4a2953 /spec | |
parent | 996683657578757cf42ef7478a5c3b9874b312f0 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
20 files changed, 381 insertions, 157 deletions
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb index 9f6fa146972..6d0d768d356 100644 --- a/spec/features/groups/issues_spec.rb +++ b/spec/features/groups/issues_spec.rb @@ -23,6 +23,10 @@ RSpec.describe 'Group issues page', feature_category: :subgroups do context 'rss feed' do let(:access_level) { ProjectFeature::ENABLED } + before do + click_button 'Actions' + end + context 'when signed in' do let(:user) do user_in_group.ensure_feed_token diff --git a/spec/features/issues/csv_spec.rb b/spec/features/issues/csv_spec.rb index 8629201459f..21d5041b210 100644 --- a/spec/features/issues/csv_spec.rb +++ b/spec/features/issues/csv_spec.rb @@ -16,6 +16,7 @@ RSpec.describe 'Issues csv', :js, feature_category: :team_planning do def request_csv(params = {}) visit project_issues_path(project, params) + click_button 'Actions' click_button 'Export as CSV' click_on 'Export issues' end diff --git a/spec/features/issues/rss_spec.rb b/spec/features/issues/rss_spec.rb index 75e7cd03a65..eb45d3c8d8b 100644 --- a/spec/features/issues/rss_spec.rb +++ b/spec/features/issues/rss_spec.rb @@ -23,6 +23,7 @@ RSpec.describe 'Project Issues RSS', :js, feature_category: :team_planning do before do sign_in(user) visit path + click_button 'Actions' end it_behaves_like "it has an RSS link with current_user's feed token" @@ -32,6 +33,7 @@ RSpec.describe 'Project Issues RSS', :js, feature_category: :team_planning do context 'when signed out' do before do visit path + click_button 'Actions' end it_behaves_like "it has an RSS link without a feed token" diff --git a/spec/features/merge_requests/rss_spec.rb b/spec/features/merge_requests/rss_spec.rb index 9c9f46278f6..5f697a4f79d 100644 --- a/spec/features/merge_requests/rss_spec.rb +++ b/spec/features/merge_requests/rss_spec.rb @@ -25,7 +25,7 @@ RSpec.describe 'Project Merge Requests RSS', feature_category: :code_review_work visit path end - it_behaves_like "it has an RSS button with current_user's feed token" + it_behaves_like "it has an RSS link with current_user's feed token" it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" end @@ -34,7 +34,7 @@ RSpec.describe 'Project Merge Requests RSS', feature_category: :code_review_work visit path end - it_behaves_like "it has an RSS button without a feed token" + it_behaves_like "it has an RSS link without a feed token" it_behaves_like "an autodiscoverable RSS feed without a feed token" end diff --git a/spec/features/merge_requests/user_exports_as_csv_spec.rb b/spec/features/merge_requests/user_exports_as_csv_spec.rb index 23ac1b264ad..57d6ee69923 100644 --- a/spec/features/merge_requests/user_exports_as_csv_spec.rb +++ b/spec/features/merge_requests/user_exports_as_csv_spec.rb @@ -14,6 +14,7 @@ RSpec.describe 'Merge Requests > Exports as CSV', :js, feature_category: :code_r context 'button is clicked' do before do + click_button 'Actions' click_button 'Export as CSV' end diff --git a/spec/features/projects/container_registry_spec.rb b/spec/features/projects/container_registry_spec.rb index e8ac34d4355..5306a9f15c6 100644 --- a/spec/features/projects/container_registry_spec.rb +++ b/spec/features/projects/container_registry_spec.rb @@ -30,6 +30,20 @@ RSpec.describe 'Container Registry', :js, feature_category: :projects do expect(page).to have_title _('Container Registry') end + it 'does not have link to settings' do + visit_container_registry + + expect(page).not_to have_link _('Configure in settings') + end + + it 'has link to settings when user is maintainer' do + project.add_maintainer(user) + + visit_container_registry + + expect(page).to have_link _('Configure in settings') + end + context 'when there are no image repositories' do it 'list page has no container title' do visit_container_registry diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index aa167fe7aba..6d576bc8e38 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -498,16 +498,40 @@ RSpec.describe MergeRequestsFinder, feature_category: :code_review_workflow do create(:approval, merge_request: merge_request3, user: user2) end - it 'for approved' do - merge_requests = described_class.new(user, { approved: true }).execute + context 'when flag `mr_approved_filter` is disabled' do + before do + stub_feature_flags(mr_approved_filter: false) + end - expect(merge_requests).to contain_exactly(merge_request3) + it 'for approved' do + merge_requests = described_class.new(user, { approved: true }).execute + + expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3, merge_request4, merge_request5) + end + + it 'for not approved' do + merge_requests = described_class.new(user, { approved: false }).execute + + expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3, merge_request4, merge_request5) + end end - it 'for not approved' do - merge_requests = described_class.new(user, { approved: false }).execute + context 'when flag `mr_approved_filter` is enabled' do + before do + stub_feature_flags(mr_approved_filter: true) + end + + it 'for approved' do + merge_requests = described_class.new(user, { approved: true }).execute + + expect(merge_requests).to contain_exactly(merge_request3) + end + + it 'for not approved' do + merge_requests = described_class.new(user, { approved: false }).execute - expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request4, merge_request5) + expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request4, merge_request5) + end end end diff --git a/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js index d419b34df1b..6d11e5879f6 100644 --- a/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js +++ b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js @@ -6,19 +6,12 @@ import { LINUX_PLATFORM, MACOS_PLATFORM, WINDOWS_PLATFORM, - AWS_PLATFORM, DOCKER_HELP_URL, KUBERNETES_HELP_URL, } from '~/ci/runner/constants'; import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue'; -const mockProvide = { - awsImgPath: 'awsLogo.svg', - dockerImgPath: 'dockerLogo.svg', - kubernetesImgPath: 'kubernetesLogo.svg', -}; - describe('RunnerPlatformsRadioGroup', () => { let wrapper; @@ -35,7 +28,6 @@ describe('RunnerPlatformsRadioGroup', () => { value: null, ...props, }, - provide: mockProvide, ...options, }); }; @@ -51,7 +43,6 @@ describe('RunnerPlatformsRadioGroup', () => { ['Linux', null], ['macOS', null], ['Windows', null], - ['AWS', expect.any(String)], ['Docker', expect.any(String)], ['Kubernetes', expect.any(String)], ]); @@ -69,7 +60,6 @@ describe('RunnerPlatformsRadioGroup', () => { ${'Linux'} | ${LINUX_PLATFORM} ${'macOS'} | ${MACOS_PLATFORM} ${'Windows'} | ${WINDOWS_PLATFORM} - ${'AWS'} | ${AWS_PLATFORM} `('user can select "$text"', async ({ text, value }) => { const radio = findFormRadioByText(text); expect(radio.props('value')).toBe(value); diff --git a/spec/frontend/issuable/components/csv_import_export_buttons_spec.js b/spec/frontend/issuable/components/csv_import_export_buttons_spec.js index a861148abb6..0e2f71fa3ee 100644 --- a/spec/frontend/issuable/components/csv_import_export_buttons_spec.js +++ b/spec/frontend/issuable/components/csv_import_export_buttons_spec.js @@ -1,5 +1,4 @@ -import { GlButton, GlDropdown } from '@gitlab/ui'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import { createMockDirective } from 'helpers/vue_mock_directive'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import CsvExportModal from '~/issuable/components/csv_export_modal.vue'; import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; @@ -33,8 +32,7 @@ describe('CsvImportExportButtons', () => { }); } - const findExportCsvButton = () => wrapper.findComponent(GlButton); - const findImportDropdown = () => wrapper.findComponent(GlDropdown); + const findExportCsvButton = () => wrapper.findByRole('menuitem', { name: 'Export as CSV' }); const findImportCsvButton = () => wrapper.findByRole('menuitem', { name: 'Import CSV' }); const findImportFromJiraLink = () => wrapper.findByRole('menuitem', { name: 'Import from Jira' }); const findExportCsvModal = () => wrapper.findComponent(CsvExportModal); @@ -50,13 +48,6 @@ describe('CsvImportExportButtons', () => { expect(findExportCsvButton().exists()).toBe(true); }); - it('export button has a tooltip', () => { - const tooltip = getBinding(findExportCsvButton().element, 'gl-tooltip'); - - expect(tooltip).toBeDefined(); - expect(tooltip.value).toBe('Export as CSV'); - }); - it('renders the export modal', () => { expect(findExportCsvModal().props()).toMatchObject({ exportCsvPath, issuableCount }); }); @@ -64,7 +55,7 @@ describe('CsvImportExportButtons', () => { it('opens the export modal', () => { findExportCsvButton().trigger('click'); - expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.exportModalId); + expect(glModalDirective).toHaveBeenCalled(); }); }); @@ -83,79 +74,38 @@ describe('CsvImportExportButtons', () => { }); describe('when the showImportButton=true', () => { - beforeEach(() => { + it('renders the import csv menu item', () => { wrapper = createComponent({ showImportButton: true }); - }); - - it('displays the import dropdown', () => { - expect(findImportDropdown().exists()).toBe(true); - }); - it('renders the import csv menu item', () => { expect(findImportCsvButton().exists()).toBe(true); }); - describe('when showLabel=false', () => { - beforeEach(() => { - wrapper = createComponent({ showImportButton: true, showLabel: false }); - }); - - it('hides button text', () => { - expect(findImportDropdown().props()).toMatchObject({ - text: 'Import issues', - textSrOnly: true, - }); - }); - - it('import button has a tooltip', () => { - const tooltip = getBinding(findImportDropdown().element, 'gl-tooltip'); - - expect(tooltip).toBeDefined(); - expect(tooltip.value).toBe('Import issues'); - }); - }); - - describe('when showLabel=true', () => { - beforeEach(() => { - wrapper = createComponent({ showImportButton: true, showLabel: true }); - }); - - it('displays a button text', () => { - expect(findImportDropdown().props()).toMatchObject({ - text: 'Import issues', - textSrOnly: false, - }); - }); - - it('import button has no tooltip', () => { - const tooltip = getBinding(findImportDropdown().element, 'gl-tooltip'); - - expect(tooltip.value).toBe(null); - }); - }); - it('renders the import modal', () => { + wrapper = createComponent({ showImportButton: true }); + expect(findImportCsvModal().exists()).toBe(true); }); it('opens the import modal', () => { + wrapper = createComponent({ showImportButton: true }); + findImportCsvButton().trigger('click'); - expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.importModalId); + expect(glModalDirective).toHaveBeenCalled(); }); describe('import from jira link', () => { const projectImportJiraPath = 'gitlab-org/gitlab-test/-/import/jira'; - beforeEach(() => { - wrapper = createComponent({ - showImportButton: true, - canEdit: true, - projectImportJiraPath, + describe('when canEdit=true', () => { + beforeEach(() => { + wrapper = createComponent({ + showImportButton: true, + canEdit: true, + projectImportJiraPath, + }); }); - }); - describe('when canEdit=true', () => { it('renders the import dropdown item', () => { expect(findImportFromJiraLink().exists()).toBe(true); }); @@ -182,8 +132,8 @@ describe('CsvImportExportButtons', () => { wrapper = createComponent({ showImportButton: false }); }); - it('does not display the import dropdown', () => { - expect(findImportDropdown().exists()).toBe(false); + it('does not render the import csv menu item', () => { + expect(findImportCsvButton().exists()).toBe(false); }); it('does not render the import modal', () => { diff --git a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js index 0a2e4e7c671..4ea3a39f15b 100644 --- a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js +++ b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js @@ -1,4 +1,4 @@ -import { GlEmptyState, GlLink } from '@gitlab/ui'; +import { GlDropdown, GlEmptyState, GlLink } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue'; @@ -26,6 +26,7 @@ describe('EmptyStateWithoutAnyIssues component', () => { }; const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons); + const findCsvImportExportDropdown = () => wrapper.findComponent(GlDropdown); const findGlEmptyState = () => wrapper.findComponent(GlEmptyState); const findGlLink = () => wrapper.findComponent(GlLink); const findIssuesHelpPageLink = () => @@ -135,6 +136,7 @@ describe('EmptyStateWithoutAnyIssues component', () => { it('renders', () => { mountComponent({ props: { showCsvButtons: true } }); + expect(findCsvImportExportDropdown().props('text')).toBe('Import issues'); expect(findCsvImportExportButtons().props()).toMatchObject({ exportCsvPath: defaultProps.exportCsvPathWithQuery, issuableCount: 0, @@ -146,6 +148,7 @@ describe('EmptyStateWithoutAnyIssues component', () => { it('does not render', () => { mountComponent({ props: { showCsvButtons: false } }); + expect(findCsvImportExportDropdown().exists()).toBe(false); expect(findCsvImportExportButtons().exists()).toBe(false); }); }); diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js index 61dbc9afeeb..d1b796c5aa6 100644 --- a/spec/frontend/issues/list/components/issues_list_app_spec.js +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -1,4 +1,4 @@ -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlDropdown } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; import { mount, shallowMount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; @@ -11,6 +11,7 @@ import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_coun import createMockApollo from 'helpers/mock_apollo_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { getIssuesCountsQueryResponse, @@ -126,12 +127,16 @@ describe('CE IssuesListApp component', () => { const mockIssuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse); const mockIssuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse); + const findCalendarButton = () => + wrapper.findByRole('menuitem', { name: IssuesListApp.i18n.calendarLabel }); const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons); + const findDropdown = () => wrapper.findComponent(GlDropdown); const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail); + const findGlButton = () => wrapper.findComponent(GlButton); const findGlButtons = () => wrapper.findAllComponents(GlButton); - const findGlButtonAt = (index) => findGlButtons().at(index); const findIssuableList = () => wrapper.findComponent(IssuableList); const findNewResourceDropdown = () => wrapper.findComponent(NewResourceDropdown); + const findRssButton = () => wrapper.findByRole('menuitem', { name: IssuesListApp.i18n.rssLabel }); const findLabelsToken = () => findIssuableList() @@ -232,63 +237,66 @@ describe('CE IssuesListApp component', () => { }); describe('header action buttons', () => { - it('renders rss button', async () => { - wrapper = mountComponent({ mountFn: mount }); - await waitForPromises(); + describe('actions dropdown', () => { + it('renders', () => { + wrapper = mountComponent({ mountFn: mount }); - expect(findGlButtonAt(0).props('icon')).toBe('rss'); - expect(findGlButtonAt(0).attributes()).toMatchObject({ - href: defaultProvide.rssPath, - 'aria-label': IssuesListApp.i18n.rssLabel, + expect(findDropdown().props()).toMatchObject({ + category: 'tertiary', + icon: 'ellipsis_v', + text: 'Actions', + textSrOnly: true, + }); }); - }); - it('renders calendar button', async () => { - wrapper = mountComponent({ mountFn: mount }); - await waitForPromises(); + describe('csv import/export buttons', () => { + describe('when user is signed in', () => { + beforeEach(() => { + setWindowLocation('?search=refactor&state=opened'); - expect(findGlButtonAt(1).props('icon')).toBe('calendar'); - expect(findGlButtonAt(1).attributes()).toMatchObject({ - href: defaultProvide.calendarPath, - 'aria-label': IssuesListApp.i18n.calendarLabel, - }); - }); + wrapper = mountComponent({ + provide: { initialSortBy: CREATED_DESC, isSignedIn: true }, + mountFn: mount, + }); - describe('csv import/export component', () => { - describe('when user is signed in', () => { - beforeEach(() => { - setWindowLocation('?search=refactor&state=opened'); + return waitForPromises(); + }); - wrapper = mountComponent({ - provide: { initialSortBy: CREATED_DESC, isSignedIn: true }, - mountFn: mount, + it('renders', () => { + expect(findCsvImportExportButtons().props()).toMatchObject({ + exportCsvPath: `${defaultProvide.exportCsvPath}?search=refactor&state=opened`, + issuableCount: 1, + }); }); + }); - return waitForPromises(); + describe('when user is not signed in', () => { + it('does not render', () => { + wrapper = mountComponent({ provide: { isSignedIn: false }, mountFn: mount }); + + expect(findCsvImportExportButtons().exists()).toBe(false); + }); }); - it('renders', () => { - expect(findCsvImportExportButtons().props()).toMatchObject({ - exportCsvPath: `${defaultProvide.exportCsvPath}?search=refactor&state=opened`, - issuableCount: 1, + describe('when in a group context', () => { + it('does not render', () => { + wrapper = mountComponent({ provide: { isProject: false }, mountFn: mount }); + + expect(findCsvImportExportButtons().exists()).toBe(false); }); }); }); - describe('when user is not signed in', () => { - it('does not render', () => { - wrapper = mountComponent({ provide: { isSignedIn: false }, mountFn: mount }); + it('renders RSS button link', () => { + wrapper = mountComponent({ mountFn: mountExtended }); - expect(findCsvImportExportButtons().exists()).toBe(false); - }); + expect(findRssButton().attributes('href')).toBe(defaultProvide.rssPath); }); - describe('when in a group context', () => { - it('does not render', () => { - wrapper = mountComponent({ provide: { isProject: false }, mountFn: mount }); + it('renders calendar button link', () => { + wrapper = mountComponent({ mountFn: mountExtended }); - expect(findCsvImportExportButtons().exists()).toBe(false); - }); + expect(findCalendarButton().attributes('href')).toBe(defaultProvide.calendarPath); }); }); @@ -296,7 +304,7 @@ describe('CE IssuesListApp component', () => { it('renders when user has permissions', () => { wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount }); - expect(findGlButtonAt(2).text()).toBe('Edit issues'); + expect(findGlButton().text()).toBe('Edit issues'); }); it('does not render when user does not have permissions', () => { @@ -309,7 +317,7 @@ describe('CE IssuesListApp component', () => { wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount }); jest.spyOn(eventHub, '$emit'); - findGlButtonAt(2).vm.$emit('click'); + findGlButton().vm.$emit('click'); await waitForPromises(); expect(eventHub.$emit).toHaveBeenCalledWith('issuables:enableBulkEdit'); @@ -320,8 +328,8 @@ describe('CE IssuesListApp component', () => { it('renders when user has permissions', () => { wrapper = mountComponent({ provide: { showNewIssueLink: true }, mountFn: mount }); - expect(findGlButtonAt(2).text()).toBe('New issue'); - expect(findGlButtonAt(2).attributes('href')).toBe(defaultProvide.newIssuePath); + expect(findGlButton().text()).toBe('New issue'); + expect(findGlButton().attributes('href')).toBe(defaultProvide.newIssuePath); }); it('does not render when user does not have permissions', () => { diff --git a/spec/frontend/issues/show/components/task_list_item_actions_spec.js b/spec/frontend/issues/show/components/task_list_item_actions_spec.js index 8caa5236796..7dacbefaeff 100644 --- a/spec/frontend/issues/show/components/task_list_item_actions_spec.js +++ b/spec/frontend/issues/show/components/task_list_item_actions_spec.js @@ -1,4 +1,4 @@ -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue'; import eventHub from '~/issues/show/event_hub'; @@ -6,9 +6,9 @@ import eventHub from '~/issues/show/event_hub'; describe('TaskListItemActions component', () => { let wrapper; - const findGlDropdown = () => wrapper.findComponent(GlDropdown); - const findConvertToTaskItem = () => wrapper.findAllComponents(GlDropdownItem).at(0); - const findDeleteItem = () => wrapper.findAllComponents(GlDropdownItem).at(1); + const findGlDropdown = () => wrapper.findComponent(GlDisclosureDropdown); + const findConvertToTaskItem = () => wrapper.findAllComponents(GlDisclosureDropdownItem).at(0); + const findDeleteItem = () => wrapper.findAllComponents(GlDisclosureDropdownItem).at(1); const mountComponent = () => { const li = document.createElement('li'); @@ -20,6 +20,7 @@ describe('TaskListItemActions component', () => { provide: { canUpdate: true }, attachTo: document.querySelector('div'), }); + wrapper.vm.$refs.dropdown.close = jest.fn(); }; beforeEach(() => { @@ -30,8 +31,8 @@ describe('TaskListItemActions component', () => { expect(findGlDropdown().props()).toMatchObject({ category: 'tertiary', icon: 'ellipsis_v', - right: true, - text: TaskListItemActions.i18n.taskActions, + placement: 'right', + toggleText: TaskListItemActions.i18n.taskActions, textSrOnly: true, }); }); @@ -39,7 +40,7 @@ describe('TaskListItemActions component', () => { it('emits event when `Convert to task` dropdown item is clicked', () => { jest.spyOn(eventHub, '$emit'); - findConvertToTaskItem().vm.$emit('click'); + findConvertToTaskItem().vm.$emit('action'); expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10'); }); @@ -47,7 +48,7 @@ describe('TaskListItemActions component', () => { it('emits event when `Delete` dropdown item is clicked', () => { jest.spyOn(eventHub, '$emit'); - findDeleteItem().vm.$emit('click'); + findDeleteItem().vm.$emit('action'); expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10'); }); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js index 5c353deacbb..1823bbfe533 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js @@ -1,8 +1,8 @@ -import { GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui'; +import { GlSkeletonLoader, GlSprintf, GlAlert, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; - import VueApollo from 'vue-apollo'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql'; @@ -16,6 +16,7 @@ import { DELETE_IMAGE_SUCCESS_MESSAGE, DELETE_IMAGE_ERROR_MESSAGE, SORT_FIELDS, + SETTINGS_TEXT, } from '~/packages_and_registries/container_registry/explorer/constants'; import deleteContainerRepositoryMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql'; import getContainerRepositoriesDetails from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql'; @@ -48,6 +49,7 @@ describe('List Page', () => { const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findCliCommands = () => wrapper.findComponent(CliCommands); + const findSettingsLink = () => wrapper.findComponent(GlButton); const findProjectEmptyState = () => wrapper.findComponent(ProjectEmptyState); const findGroupEmptyState = () => wrapper.findComponent(GroupEmptyState); const findRegistryHeader = () => wrapper.findComponent(RegistryHeader); @@ -110,6 +112,9 @@ describe('List Page', () => { ...dockerCommands, }; }, + directives: { + GlTooltip: createMockDirective('gl-tooltip'), + }, }); }; @@ -122,6 +127,42 @@ describe('List Page', () => { expect(findRegistryHeader().props()).toMatchObject({ imagesCount: 2, metadataLoading: false, + helpPagePath: '', + hideExpirationPolicyData: false, + showCleanupPolicyLink: false, + expirationPolicy: {}, + cleanupPoliciesSettingsPath: '', + }); + }); + + describe('link to settings', () => { + beforeEach(() => { + const config = { + showContainerRegistrySettings: true, + cleanupPoliciesSettingsPath: 'bar', + }; + mountComponent({ config }); + }); + + it('is rendered', () => { + expect(findSettingsLink().exists()).toBe(true); + }); + + it('has the right icon', () => { + expect(findSettingsLink().props('icon')).toBe('settings'); + }); + + it('has the right attributes', () => { + expect(findSettingsLink().attributes()).toMatchObject({ + 'aria-label': SETTINGS_TEXT, + href: 'bar', + }); + }); + + it('sets tooltip with right label', () => { + const tooltip = getBinding(findSettingsLink().element, 'gl-tooltip'); + + expect(tooltip.value).toBe(SETTINGS_TEXT); }); }); @@ -235,6 +276,14 @@ describe('List Page', () => { expect(findCliCommands().exists()).toBe(false); }); + + it('link to settings is not visible', async () => { + mountComponent({ resolver, config }); + + await waitForApolloRequestRender(); + + expect(findSettingsLink().exists()).toBe(false); + }); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js index 8c6b3cc464c..2c8d8b11b94 100644 --- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js @@ -105,4 +105,31 @@ describe('MRWidget approvals summary', () => { expect(wrapper.findComponent(UserAvatarList).exists()).toBe(false); }); }); + + describe('user avatars list layout', () => { + beforeEach(() => { + createComponent(); + }); + + it('does not add top padding initially', () => { + const avatarsList = findAvatars(); + + expect(avatarsList.classes()).not.toContain('gl-pt-1'); + }); + + it('adds some top padding when the list is expanded', async () => { + const avatarsList = findAvatars(); + await avatarsList.vm.$emit('expanded'); + + expect(avatarsList.classes()).toContain('gl-pt-1'); + }); + + it('removes the top padding when the list collapsed', async () => { + const avatarsList = findAvatars(); + await avatarsList.vm.$emit('expanded'); + await avatarsList.vm.$emit('collapsed'); + + expect(avatarsList.classes()).not.toContain('gl-pt-1'); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js index 1754292cb63..075cb753301 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js @@ -148,6 +148,13 @@ describe('UserAvatarList', () => { expect(links.length).toEqual(TEST_BREAKPOINT); }); + it('does not emit any event on mount', async () => { + factory(); + await nextTick(); + + expect(wrapper.emitted()).toEqual({}); + }); + describe('with expand clicked', () => { beforeEach(() => { factory(); @@ -160,13 +167,25 @@ describe('UserAvatarList', () => { expect(links.length).toEqual(props.items.length); }); - it('with collapse clicked, it renders avatars up to breakpoint', async () => { - clickButton(); + it('emits the `expanded` event', () => { + expect(wrapper.emitted('expanded')).toHaveLength(1); + }); - await nextTick(); - const links = wrapper.findAllComponents(UserAvatarLink); + describe('with collapse clicked', () => { + beforeEach(() => { + clickButton(); + }); + + it('renders avatars up to breakpoint', async () => { + await nextTick(); + const links = wrapper.findAllComponents(UserAvatarLink); + + expect(links.length).toEqual(TEST_BREAKPOINT); + }); - expect(links.length).toEqual(TEST_BREAKPOINT); + it('emits the `collapsed` event', () => { + expect(wrapper.emitted('collapsed')).toHaveLength(1); + }); }); }); }); diff --git a/spec/helpers/packages_helper_spec.rb b/spec/helpers/packages_helper_spec.rb index dcc5e336253..ae8a7f0c14c 100644 --- a/spec/helpers/packages_helper_spec.rb +++ b/spec/helpers/packages_helper_spec.rb @@ -129,6 +129,62 @@ RSpec.describe PackagesHelper, feature_category: :package_registry do end end + describe '#show_container_registry_settings' do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:admin) { create(:admin) } + + before do + allow(helper).to receive(:current_user) { user } + end + + subject { helper.show_container_registry_settings(project) } + + context 'with container registry config enabled' do + before do + stub_config(registry: { enabled: true }) + end + + context 'when user has permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :admin_container_image, project).and_return(true) + end + + it { is_expected.to be(true) } + end + + context 'when user does not have permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :admin_container_image, project).and_return(false) + end + + it { is_expected.to be(false) } + end + end + + context 'with container registry config disabled' do + before do + stub_config(registry: { enabled: false }) + end + + context 'when user has permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :admin_container_image, project).and_return(true) + end + + it { is_expected.to be(false) } + end + + context 'when user does not have permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :admin_container_image, project).and_return(false) + end + + it { is_expected.to be(false) } + end + end + end + describe '#show_group_package_registry_settings' do let_it_be(:group) { create(:group) } let_it_be(:user) { create(:user) } diff --git a/spec/models/protected_tag/create_access_level_spec.rb b/spec/models/protected_tag/create_access_level_spec.rb index 566f8695388..77a7d15b597 100644 --- a/spec/models/protected_tag/create_access_level_spec.rb +++ b/spec/models/protected_tag/create_access_level_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe ProtectedTag::CreateAccessLevel, feature_category: :source_code_management do + include_examples 'protected tag access' + describe 'associations' do it { is_expected.to belong_to(:deploy_key) } end @@ -10,16 +12,6 @@ RSpec.describe ProtectedTag::CreateAccessLevel, feature_category: :source_code_m describe 'validations', :aggregate_failures do let_it_be(:protected_tag) { create(:protected_tag) } - it 'verifies access levels' do - is_expected.to validate_inclusion_of(:access_level).in_array( - [ - Gitlab::Access::MAINTAINER, - Gitlab::Access::DEVELOPER, - Gitlab::Access::NO_ACCESS - ] - ) - end - context 'when deploy key enabled for the project' do let(:deploy_key) { create(:deploy_key, projects: [protected_tag.project]) } diff --git a/spec/support/shared_examples/features/rss_shared_examples.rb b/spec/support/shared_examples/features/rss_shared_examples.rb index 29ecbd0dc0e..f6566214e32 100644 --- a/spec/support/shared_examples/features/rss_shared_examples.rb +++ b/spec/support/shared_examples/features/rss_shared_examples.rb @@ -43,6 +43,7 @@ RSpec.shared_examples "updates atom feed link" do |type| it "for #{type}" do sign_in(user) visit path + click_button 'Actions', match: :first link = find_link('Subscribe to RSS feed') params = CGI.parse(URI.parse(link[:href]).query) diff --git a/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb b/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb new file mode 100644 index 00000000000..53ebc207128 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'protected ref access' do |association| + let_it_be(:project) { create(:project) } + let_it_be(:protected_ref) { create(association, project: project) } # rubocop:disable Rails/SaveBang + + it { is_expected.to validate_inclusion_of(:access_level).in_array(described_class.allowed_access_levels) } + + it { is_expected.to validate_presence_of(:access_level) } + + context 'when not role?' do + before do + allow(subject).to receive(:role?).and_return(false) + end + + it { is_expected.not_to validate_presence_of(:access_level) } + end + + describe '#check_access' do + let_it_be(:current_user) { create(:user) } + + let(:access_level) { ::Gitlab::Access::DEVELOPER } + + before_all do + project.add_maintainer(current_user) + end + + subject do + described_class.new( + association => protected_ref, + access_level: access_level + ) + end + + context 'when current_user is nil' do + it { expect(subject.check_access(nil)).to eq(false) } + end + + context 'when access_level is NO_ACCESS' do + let(:access_level) { ::Gitlab::Access::NO_ACCESS } + + it { expect(subject.check_access(current_user)).to eq(false) } + end + + context 'when current_user can push_code to project and access_level is permitted' do + before do + allow(current_user).to receive(:can?).with(:push_code, project).and_return(true) + end + + it { expect(subject.check_access(current_user)).to eq(true) } + end + + context 'when current_user cannot push_code to project' do + before do + allow(current_user).to receive(:can?).with(:push_code, project).and_return(false) + end + + it { expect(subject.check_access(current_user)).to eq(false) } + end + end +end diff --git a/spec/support/shared_examples/models/concerns/protected_tag_access_examples.rb b/spec/support/shared_examples/models/concerns/protected_tag_access_examples.rb new file mode 100644 index 00000000000..49f616d5a59 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/protected_tag_access_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'protected tag access' do + include_examples 'protected ref access', :protected_tag + + let_it_be(:protected_tag) { create(:protected_tag) } + + it { is_expected.to belong_to(:protected_tag) } + + describe '#project' do + before do + allow(protected_tag).to receive(:project) + end + + it 'delegates project to protected_tag association' do + described_class.new(protected_tag: protected_tag).project + + expect(protected_tag).to have_received(:project) + end + end +end |