diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-23 00:08:01 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-23 00:08:01 +0300 |
commit | c50e042a392687730db9b8c2607883485b258ae4 (patch) | |
tree | 519b069aa0a400241a2f8dc0f900f09625e3d8ed /spec | |
parent | 7e2f555a6dc37839727dee130d8ed4421b680d42 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
23 files changed, 308 insertions, 185 deletions
diff --git a/spec/features/nav/pinned_nav_items_spec.rb b/spec/features/nav/pinned_nav_items_spec.rb index 308350d5166..cf53e0a322a 100644 --- a/spec/features/nav/pinned_nav_items_spec.rb +++ b/spec/features/nav/pinned_nav_items_spec.rb @@ -89,7 +89,7 @@ RSpec.describe 'Navigation menu item pinning', :js, feature_category: :navigatio before do within '#super-sidebar' do click_on 'Operate' - add_pin('Package Registry') + add_pin('Terraform states') add_pin('Terraform modules') wait_for_requests end @@ -97,8 +97,8 @@ RSpec.describe 'Navigation menu item pinning', :js, feature_category: :navigatio it 'can be unpinned from within the pinned section' do within '[data-testid="pinned-nav-items"]' do - remove_pin('Package Registry') - expect(page).not_to have_content 'Package Registry' + remove_pin('Terraform states') + expect(page).not_to have_content 'Terraform states' end end @@ -117,7 +117,7 @@ RSpec.describe 'Navigation menu item pinning', :js, feature_category: :navigatio it 'can be reordered' do within '[data-testid="pinned-nav-items"]' do pinned_items = page.find_all('a').map(&:text) - item2 = page.find('a', text: 'Package Registry') + item2 = page.find('a', text: 'Terraform states') item3 = page.find('a', text: 'Terraform modules') expect(pinned_items[1..2]).to eq [item2.text, item3.text] drag_item(item3, to: item2) diff --git a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js index c435dd57de2..88d4398aa70 100644 --- a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js +++ b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js @@ -24,7 +24,7 @@ describe('RunnerStatusCell', () => { propsData: { runner: { runnerType: INSTANCE_TYPE, - active: true, + paused: false, status: STATUS_ONLINE, jobExecutionStatus: JOB_STATUS_IDLE, ...runner, @@ -59,7 +59,7 @@ describe('RunnerStatusCell', () => { it('Displays paused status', () => { createComponent({ runner: { - active: false, + paused: true, status: STATUS_ONLINE, }, }); diff --git a/spec/frontend/ci/runner/components/runner_delete_button_spec.js b/spec/frontend/ci/runner/components/runner_delete_button_spec.js index 3123f2894fb..3b3f3b1770d 100644 --- a/spec/frontend/ci/runner/components/runner_delete_button_spec.js +++ b/spec/frontend/ci/runner/components/runner_delete_button_spec.js @@ -236,7 +236,7 @@ describe('RunnerDeleteButton', () => { createComponent({ props: { runner: { - active: true, + paused: false, }, compact: true, }, diff --git a/spec/frontend/ci/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js index 0f4ec717c3e..9da640afeb7 100644 --- a/spec/frontend/ci/runner/components/runner_list_spec.js +++ b/spec/frontend/ci/runner/components/runner_list_spec.js @@ -18,7 +18,6 @@ import { I18N_PROJECT_TYPE, I18N_STATUS_NEVER_CONTACTED } from '~/ci/runner/cons import { allRunnersData, onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data'; const mockRunners = allRunnersData.data.runners.nodes; -const mockActiveRunnersCount = mockRunners.length; describe('RunnerList', () => { let wrapper; @@ -44,7 +43,6 @@ describe('RunnerList', () => { apolloProvider: createMockApollo([], {}, cacheConfig), propsData: { runners: mockRunners, - activeRunnersCount: mockActiveRunnersCount, ...props, }, provide: { diff --git a/spec/frontend/ci/runner/components/runner_pause_button_spec.js b/spec/frontend/ci/runner/components/runner_pause_button_spec.js index 350d029f3fc..1ea870e004a 100644 --- a/spec/frontend/ci/runner/components/runner_pause_button_spec.js +++ b/spec/frontend/ci/runner/components/runner_pause_button_spec.js @@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; -import runnerToggleActiveMutation from '~/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql'; +import runnerTogglePausedMutation from '~/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql'; import waitForPromises from 'helpers/wait_for_promises'; import { captureException } from '~/ci/runner/sentry_utils'; import { createAlert } from '~/alert'; @@ -27,7 +27,7 @@ jest.mock('~/ci/runner/sentry_utils'); describe('RunnerPauseButton', () => { let wrapper; - let runnerToggleActiveHandler; + let runnerTogglePausedHandler; const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value; const findBtn = () => wrapper.findComponent(GlButton); @@ -39,12 +39,12 @@ describe('RunnerPauseButton', () => { propsData: { runner: { id: mockRunner.id, - active: mockRunner.active, + paused: mockRunner.paused, ...runner, }, ...propsData, }, - apolloProvider: createMockApollo([[runnerToggleActiveMutation, runnerToggleActiveHandler]]), + apolloProvider: createMockApollo([[runnerTogglePausedMutation, runnerTogglePausedHandler]]), directives: { GlTooltip: createMockDirective('gl-tooltip'), }, @@ -57,13 +57,13 @@ describe('RunnerPauseButton', () => { }; beforeEach(() => { - runnerToggleActiveHandler = jest.fn().mockImplementation(({ input }) => { + runnerTogglePausedHandler = jest.fn().mockImplementation(({ input }) => { return Promise.resolve({ data: { runnerUpdate: { runner: { id: input.id, - active: input.active, + paused: !input.paused, }, errors: [], }, @@ -76,15 +76,15 @@ describe('RunnerPauseButton', () => { describe('Pause/Resume action', () => { describe.each` - runnerState | icon | content | tooltip | isActive | newActiveValue - ${'paused'} | ${'play'} | ${I18N_RESUME} | ${I18N_RESUME_TOOLTIP} | ${false} | ${true} - ${'active'} | ${'pause'} | ${I18N_PAUSE} | ${I18N_PAUSE_TOOLTIP} | ${true} | ${false} - `('When the runner is $runnerState', ({ icon, content, tooltip, isActive, newActiveValue }) => { + runnerState | icon | content | tooltip | isPaused | newPausedValue + ${'paused'} | ${'play'} | ${I18N_RESUME} | ${I18N_RESUME_TOOLTIP} | ${true} | ${false} + ${'active'} | ${'pause'} | ${I18N_PAUSE} | ${I18N_PAUSE_TOOLTIP} | ${false} | ${true} + `('When the runner is $runnerState', ({ icon, content, tooltip, isPaused, newPausedValue }) => { beforeEach(() => { createComponent({ props: { runner: { - active: isActive, + paused: isPaused, }, }, }); @@ -106,7 +106,7 @@ describe('RunnerPauseButton', () => { describe(`Before the ${icon} button is clicked`, () => { it('The mutation has not been called', () => { - expect(runnerToggleActiveHandler).toHaveBeenCalledTimes(0); + expect(runnerTogglePausedHandler).not.toHaveBeenCalled(); }); }); @@ -134,12 +134,12 @@ describe('RunnerPauseButton', () => { await clickAndWait(); }); - it(`The mutation to that sets active to ${newActiveValue} is called`, () => { - expect(runnerToggleActiveHandler).toHaveBeenCalledTimes(1); - expect(runnerToggleActiveHandler).toHaveBeenCalledWith({ + it(`The mutation to that sets "paused" to ${newPausedValue} is called`, () => { + expect(runnerTogglePausedHandler).toHaveBeenCalledTimes(1); + expect(runnerTogglePausedHandler).toHaveBeenCalledWith({ input: { id: mockRunner.id, - active: newActiveValue, + paused: newPausedValue, }, }); }); @@ -158,7 +158,7 @@ describe('RunnerPauseButton', () => { const mockErrorMsg = 'Update error!'; beforeEach(async () => { - runnerToggleActiveHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); + runnerTogglePausedHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); await clickAndWait(); }); @@ -180,12 +180,12 @@ describe('RunnerPauseButton', () => { const mockErrorMsg2 = 'User not allowed!'; beforeEach(async () => { - runnerToggleActiveHandler.mockResolvedValueOnce({ + runnerTogglePausedHandler.mockResolvedValueOnce({ data: { runnerUpdate: { runner: { id: mockRunner.id, - active: isActive, + paused: isPaused, }, errors: [mockErrorMsg, mockErrorMsg2], }, @@ -215,7 +215,7 @@ describe('RunnerPauseButton', () => { createComponent({ props: { runner: { - active: true, + paused: false, }, compact: true, }, diff --git a/spec/frontend/ci/runner/components/runner_update_form_spec.js b/spec/frontend/ci/runner/components/runner_update_form_spec.js index ee37d6241b5..d1d4e38f47c 100644 --- a/spec/frontend/ci/runner/components/runner_update_form_spec.js +++ b/spec/frontend/ci/runner/components/runner_update_form_spec.js @@ -56,7 +56,7 @@ describe('RunnerUpdateForm', () => { const submitFormAndWait = () => submitForm().then(waitForPromises); const getFieldsModel = () => ({ - active: !findPausedCheckbox().element.checked, + paused: findPausedCheckbox().element.checked, accessLevel: findProtectedCheckbox().element.checked ? ACCESS_LEVEL_REF_PROTECTED : ACCESS_LEVEL_NOT_PROTECTED, @@ -179,8 +179,8 @@ describe('RunnerUpdateForm', () => { describe('On submit, runner gets updated', () => { it.each` test | initialValue | findCheckbox | checked | submitted - ${'pauses'} | ${{ active: true }} | ${findPausedCheckbox} | ${true} | ${{ active: false }} - ${'activates'} | ${{ active: false }} | ${findPausedCheckbox} | ${false} | ${{ active: true }} + ${'pauses'} | ${{ paused: false }} | ${findPausedCheckbox} | ${true} | ${{ paused: true }} + ${'activates'} | ${{ paused: true }} | ${findPausedCheckbox} | ${false} | ${{ paused: false }} ${'unprotects'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED }} | ${findProtectedCheckbox} | ${true} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED }} ${'protects'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED }} | ${findProtectedCheckbox} | ${false} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED }} ${'"runs untagged jobs"'} | ${{ runUntagged: true }} | ${findRunUntaggedCheckbox} | ${false} | ${{ runUntagged: false }} diff --git a/spec/frontend/ci/runner/runner_update_form_utils_spec.js b/spec/frontend/ci/runner/runner_update_form_utils_spec.js index b2f7bbc49a9..80c492bb431 100644 --- a/spec/frontend/ci/runner/runner_update_form_utils_spec.js +++ b/spec/frontend/ci/runner/runner_update_form_utils_spec.js @@ -12,7 +12,7 @@ const mockRunner = { description: mockDescription, maximumTimeout: 100, accessLevel: ACCESS_LEVEL_NOT_PROTECTED, - active: true, + paused: false, locked: true, runUntagged: true, tagList: ['tag-1', 'tag-2'], @@ -79,7 +79,7 @@ describe('~/ci/runner/runner_update_form_utils', () => { ${',,,,, commas'} | ${['commas']} ${'more ,,,,, commas'} | ${['more', 'commas']} ${' trimmed , trimmed2 '} | ${['trimmed', 'trimmed2']} - `('collect tags separated by commas for "$value"', ({ tagList, tagListInput }) => { + `('collect comma-separated tags "$tagList" as $tagListInput', ({ tagList, tagListInput }) => { const variables = modelToUpdateMutationVariables({ ...mockModel, tagList, diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js index 1dcac017ccf..5c36dbf9c9c 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js @@ -1,22 +1,34 @@ -import { GlDropdown, GlButton, GlFormCheckbox } from '@gitlab/ui'; -import { nextTick } from 'vue'; +import { GlAlert, GlDropdown, GlButton, GlFormCheckbox, GlLoadingIcon } from '@gitlab/ui'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; import { stubComponent } from 'helpers/stub_component'; import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; -import { packageFiles as packageFilesMock } from 'jest/packages_and_registries/package_registry/mock_data'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { s__ } from '~/locale'; +import { + packageFiles as packageFilesMock, + packageFilesQuery, +} from 'jest/packages_and_registries/package_registry/mock_data'; import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import getPackageFiles from '~/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql'; + +Vue.use(VueApollo); + describe('Package Files', () => { let wrapper; + let apolloProvider; const findAllRows = () => wrapper.findAllByTestId('file-row'); const findDeleteSelectedButton = () => wrapper.findByTestId('delete-selected'); const findFirstRow = () => extendedWrapper(findAllRows().at(0)); const findSecondRow = () => extendedWrapper(findAllRows().at(1)); + const findPackageFilesAlert = () => wrapper.findComponent(GlAlert); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findFirstRowDownloadLink = () => findFirstRow().findByTestId('download-link'); - const findFirstRowCommitLink = () => findFirstRow().findByTestId('commit-link'); - const findSecondRowCommitLink = () => findSecondRow().findByTestId('commit-link'); const findFirstRowFileIcon = () => findFirstRow().findComponent(FileIcon); const findFirstRowCreatedAt = () => findFirstRow().findComponent(TimeAgoTooltip); const findFirstActionMenu = () => extendedWrapper(findFirstRow().findComponent(GlDropdown)); @@ -30,16 +42,23 @@ describe('Package Files', () => { const [file] = files; const createComponent = ({ - packageFiles = [file], + packageId = '1', + packageType = 'NPM', isLoading = false, canDelete = true, stubs, + resolver = jest.fn().mockResolvedValue(packageFilesQuery([file])), } = {}) => { + const requestHandlers = [[getPackageFiles, resolver]]; + apolloProvider = createMockApollo(requestHandlers); + wrapper = mountExtended(PackageFiles, { + apolloProvider, propsData: { canDelete, isLoading, - packageFiles, + packageId, + packageType, }, stubs: { GlTable: false, @@ -49,35 +68,61 @@ describe('Package Files', () => { }; describe('rows', () => { - it('renders a single file for an npm package', () => { + it('do not get rendered when query is loading', () => { createComponent(); + expect(findLoadingIcon().exists()).toBe(true); + expect(findDeleteSelectedButton().props('disabled')).toBe(true); + }); + + it('renders a single file for an npm package', async () => { + createComponent(); + await waitForPromises(); + expect(findAllRows()).toHaveLength(1); + expect(findLoadingIcon().exists()).toBe(false); }); - it('renders multiple files for a package that contains more than one file', () => { - createComponent({ packageFiles: files }); + it('renders multiple files for a package that contains more than one file', async () => { + createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()) }); + await waitForPromises(); expect(findAllRows()).toHaveLength(2); }); + + it('does not render gl-alert', async () => { + createComponent(); + await waitForPromises(); + + expect(findPackageFilesAlert().exists()).toBe(false); + }); + + it('renders gl-alert if load fails', async () => { + createComponent({ resolver: jest.fn().mockRejectedValue() }); + await waitForPromises(); + + expect(findPackageFilesAlert().exists()).toBe(true); + expect(findPackageFilesAlert().text()).toBe( + s__('PackageRegistry|Something went wrong while fetching package assets.'), + ); + }); }); describe('link', () => { - it('exists', () => { + beforeEach(async () => { createComponent(); + await waitForPromises(); + }); + it('exists', () => { expect(findFirstRowDownloadLink().exists()).toBe(true); }); it('has the correct attrs bound', () => { - createComponent(); - expect(findFirstRowDownloadLink().attributes('href')).toBe(file.downloadPath); }); it('emits "download-file" event on click', () => { - createComponent(); - findFirstRowDownloadLink().vm.$emit('click'); expect(wrapper.emitted('download-file')).toEqual([[]]); @@ -85,90 +130,43 @@ describe('Package Files', () => { }); describe('file-icon', () => { - it('exists', () => { + beforeEach(async () => { createComponent(); + await waitForPromises(); + }); + it('exists', () => { expect(findFirstRowFileIcon().exists()).toBe(true); }); it('has the correct props bound', () => { - createComponent(); - expect(findFirstRowFileIcon().props('fileName')).toBe(file.fileName); }); }); describe('time-ago tooltip', () => { - it('exists', () => { + beforeEach(async () => { createComponent(); + await waitForPromises(); + }); + it('exists', () => { expect(findFirstRowCreatedAt().exists()).toBe(true); }); it('has the correct props bound', () => { - createComponent(); - expect(findFirstRowCreatedAt().props('time')).toBe(file.createdAt); }); }); - describe('commit', () => { - const withPipeline = { - ...file, - pipelines: [ - { - sha: 'sha', - id: 1, - commitPath: 'commitPath', - }, - ], - }; - - describe('when package file has a pipeline associated', () => { - it('exists', () => { - createComponent({ packageFiles: [withPipeline] }); - - expect(findFirstRowCommitLink().exists()).toBe(true); - }); - - it('the link points to the commit path', () => { - createComponent({ packageFiles: [withPipeline] }); - - expect(findFirstRowCommitLink().attributes('href')).toBe( - withPipeline.pipelines[0].commitPath, - ); - }); - - it('the text is the pipeline sha', () => { - createComponent({ packageFiles: [withPipeline] }); - - expect(findFirstRowCommitLink().text()).toBe(withPipeline.pipelines[0].sha); - }); - }); - - describe('when package file has no pipeline associated', () => { - it('does not exist', () => { - createComponent(); - - expect(findFirstRowCommitLink().exists()).toBe(false); - }); - }); - - describe('when only one file lacks an associated pipeline', () => { - it('renders the commit when it exists and not otherwise', () => { - createComponent({ packageFiles: [withPipeline, file] }); - - expect(findFirstRowCommitLink().exists()).toBe(true); - expect(findSecondRowCommitLink().exists()).toBe(false); - }); - }); - }); - describe('action menu', () => { describe('when the user can delete', () => { - it('exists', () => { + beforeEach(async () => { createComponent(); + await waitForPromises(); + }); + it('exists', () => { expect(findFirstActionMenu().exists()).toBe(true); expect(findFirstActionMenu().props('icon')).toBe('ellipsis_v'); expect(findFirstActionMenu().props('textSrOnly')).toBe(true); @@ -178,14 +176,10 @@ describe('Package Files', () => { describe('menu items', () => { describe('delete file', () => { it('exists', () => { - createComponent(); - expect(findActionMenuDelete().exists()).toBe(true); }); it('emits a delete event when clicked', async () => { - createComponent(); - await findActionMenuDelete().trigger('click'); const [[items]] = wrapper.emitted('delete-files'); @@ -199,8 +193,9 @@ describe('Package Files', () => { describe('when the user can not delete', () => { const canDelete = false; - it('does not exist', () => { + it('does not exist', async () => { createComponent({ canDelete }); + await waitForPromises(); expect(findFirstActionMenu().exists()).toBe(false); }); @@ -209,22 +204,33 @@ describe('Package Files', () => { describe('multi select', () => { describe('when user can delete', () => { - it('delete selected button exists & is disabled', () => { + it('delete selected button exists & is disabled', async () => { createComponent(); + await waitForPromises(); expect(findDeleteSelectedButton().exists()).toBe(true); expect(findDeleteSelectedButton().text()).toMatchInterpolatedText('Delete selected'); expect(findDeleteSelectedButton().props('disabled')).toBe(true); }); - it('delete selected button exists & is disabled when isLoading prop is true', () => { - createComponent({ isLoading: true }); + it('delete selected button exists & is disabled when isLoading prop is true', async () => { + createComponent(); + await waitForPromises(); + const first = findAllRowCheckboxes().at(0); + + await first.setChecked(true); + + expect(findDeleteSelectedButton().props('disabled')).toBe(false); + + await wrapper.setProps({ isLoading: true }); expect(findDeleteSelectedButton().props('disabled')).toBe(true); + expect(findLoadingIcon().exists()).toBe(true); }); - it('checkboxes to select file are visible', () => { - createComponent({ packageFiles: files }); + it('checkboxes to select file are visible', async () => { + createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()) }); + await waitForPromises(); expect(findCheckAllCheckbox().exists()).toBe(true); expect(findAllRowCheckboxes()).toHaveLength(2); @@ -232,6 +238,7 @@ describe('Package Files', () => { it('selecting a checkbox enables delete selected button', async () => { createComponent(); + await waitForPromises(); const first = findAllRowCheckboxes().at(0); @@ -244,7 +251,8 @@ describe('Package Files', () => { it('will toggle between selecting all and deselecting all files', async () => { const getChecked = () => findAllRowCheckboxes().filter((x) => x.element.checked === true); - createComponent({ packageFiles: files }); + createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()) }); + await waitForPromises(); expect(getChecked()).toHaveLength(0); @@ -262,9 +270,10 @@ describe('Package Files', () => { expect(findCheckAllCheckbox().props('indeterminate')).toBe(state); createComponent({ - packageFiles: files, + resolver: jest.fn().mockResolvedValue(packageFilesQuery()), stubs: { GlFormCheckbox: stubComponent(GlFormCheckbox, { props: ['indeterminate'] }) }, }); + await waitForPromises(); expectIndeterminateState(false); @@ -288,6 +297,7 @@ describe('Package Files', () => { it('emits a delete event when selected', async () => { createComponent(); + await waitForPromises(); const first = findAllRowCheckboxes().at(0); @@ -301,7 +311,8 @@ describe('Package Files', () => { }); it('emits delete event with both items when all are selected', async () => { - createComponent({ packageFiles: files }); + createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()) }); + await waitForPromises(); await findCheckAllCheckbox().setChecked(true); @@ -315,14 +326,16 @@ describe('Package Files', () => { describe('when user cannot delete', () => { const canDelete = false; - it('delete selected button does not exist', () => { + it('delete selected button does not exist', async () => { createComponent({ canDelete }); + await waitForPromises(); expect(findDeleteSelectedButton().exists()).toBe(false); }); - it('checkboxes to select file are not visible', () => { - createComponent({ packageFiles: files, canDelete }); + it('checkboxes to select file are not visible', async () => { + createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()), canDelete }); + await waitForPromises(); expect(findCheckAllCheckbox().exists()).toBe(false); expect(findAllRowCheckboxes()).toHaveLength(0); @@ -332,24 +345,27 @@ describe('Package Files', () => { describe('additional details', () => { describe('details toggle button', () => { - it('exists', () => { + it('exists', async () => { createComponent(); + await waitForPromises(); expect(findFirstToggleDetailsButton().exists()).toBe(true); }); - it('is hidden when no details is present', () => { + it('is hidden when no details is present', async () => { const { ...noShaFile } = file; noShaFile.fileSha256 = null; noShaFile.fileMd5 = null; noShaFile.fileSha1 = null; - createComponent({ packageFiles: [noShaFile] }); + createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery([noShaFile])) }); + await waitForPromises(); expect(findFirstToggleDetailsButton().exists()).toBe(false); }); it('toggles the details row', async () => { createComponent(); + await waitForPromises(); expect(findFirstToggleDetailsButton().props('icon')).toBe('chevron-down'); @@ -380,6 +396,7 @@ describe('Package Files', () => { ${'sha-1'} | ${'SHA-1'} | ${'be93151dc23ac34a82752444556fe79b32c7a1ad'} `('has a $title row', async ({ selector, title, sha }) => { createComponent(); + await waitForPromises(); await showShaFiles(); @@ -393,7 +410,8 @@ describe('Package Files', () => { const { ...missingMd5 } = file; missingMd5.fileMd5 = null; - createComponent({ packageFiles: [missingMd5] }); + createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery([missingMd5])) }); + await waitForPromises(); await showShaFiles(); diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js index 5fb53566d4e..fa6a69b1a1f 100644 --- a/spec/frontend/packages_and_registries/package_registry/mock_data.js +++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js @@ -257,7 +257,7 @@ export const packageDetailsQuery = ({ pageInfo: { hasNextPage: true, }, - nodes: packageFiles(), + nodes: packageFiles().map(({ id, size }) => ({ id, size })), __typename: 'PackageFileConnection', }, versions: { @@ -285,6 +285,19 @@ export const packagePipelinesQuery = (pipelines = packagePipelines()) => ({ }, }); +export const packageFilesQuery = (files = packageFiles()) => ({ + data: { + package: { + id: 'gid://gitlab/Packages::Package/111', + packageFiles: { + nodes: files, + __typename: 'PackageFileConnection', + }, + __typename: 'PackageDetailsType', + }, + }, +}); + export const emptyPackageDetailsQuery = () => ({ data: { package: { diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js index 0962b4fa757..8b15dfd7d4a 100644 --- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js @@ -328,18 +328,18 @@ describe('PackagesApp', () => { describe('package files', () => { it('renders the package files component and has the right props', async () => { - const expectedFile = { ...packageFiles()[0] }; - // eslint-disable-next-line no-underscore-dangle - delete expectedFile.__typename; createComponent(); await waitForPromises(); expect(findPackageFiles().exists()).toBe(true); - expect(findPackageFiles().props('packageFiles')[0]).toMatchObject(expectedFile); - expect(findPackageFiles().props('canDelete')).toBe(packageData().canDestroy); - expect(findPackageFiles().props('isLoading')).toEqual(false); + expect(findPackageFiles().props()).toMatchObject({ + canDelete: packageData().canDestroy, + isLoading: false, + packageId: packageData().id, + packageType: packageData().packageType, + }); }); it('does not render the package files table when the package is composer', async () => { diff --git a/spec/graphql/resolvers/users/participants_resolver_spec.rb b/spec/graphql/resolvers/users/participants_resolver_spec.rb index 224213d1521..63a14daabba 100644 --- a/spec/graphql/resolvers/users/participants_resolver_spec.rb +++ b/spec/graphql/resolvers/users/participants_resolver_spec.rb @@ -8,39 +8,54 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do describe '#resolve' do let_it_be(:user) { create(:user) } let_it_be(:guest) { create(:user) } - let_it_be(:project) { create(:project, :public) } + let_it_be(:project) do + create(:project, :public).tap do |r| + r.add_developer(user) + r.add_guest(guest) + end + end + + let_it_be(:private_project) { create(:project, :private).tap { |r| r.add_developer(user) } } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:private_issue) { create(:issue, project: private_project) } let_it_be(:public_note_author) { create(:user) } let_it_be(:public_reply_author) { create(:user) } let_it_be(:internal_note_author) { create(:user) } let_it_be(:internal_reply_author) { create(:user) } + let_it_be(:system_note_author) { create(:user) } + let_it_be(:internal_system_note_author) { create(:user) } let_it_be(:public_note) { create(:note, project: project, noteable: issue, author: public_note_author) } let_it_be(:internal_note) { create(:note, :confidential, project: project, noteable: issue, author: internal_note_author) } - let_it_be(:public_reply) { create(:note, noteable: issue, in_reply_to: public_note, project: project, author: public_reply_author) } - let_it_be(:internal_reply) { create(:note, :confidential, noteable: issue, in_reply_to: internal_note, project: project, author: internal_reply_author) } - - let_it_be(:note_metadata2) { create(:system_note_metadata, note: public_note) } + let_it_be(:public_reply) do + create(:note, noteable: issue, in_reply_to: public_note, project: project, author: public_reply_author) + end - let_it_be(:issue_emoji) { create(:award_emoji, name: 'thumbsup', awardable: issue) } - let_it_be(:note_emoji1) { create(:award_emoji, name: 'thumbsup', awardable: public_note) } - let_it_be(:note_emoji2) { create(:award_emoji, name: 'thumbsup', awardable: internal_note) } - let_it_be(:note_emoji3) { create(:award_emoji, name: 'thumbsup', awardable: public_reply) } - let_it_be(:note_emoji4) { create(:award_emoji, name: 'thumbsup', awardable: internal_reply) } + let_it_be(:internal_reply) do + create(:note, :confidential, noteable: issue, in_reply_to: internal_note, project: project, author: internal_reply_author) + end - let_it_be(:issue_emoji_author) { issue_emoji.user } - let_it_be(:public_note_emoji_author) { note_emoji1.user } - let_it_be(:internal_note_emoji_author) { note_emoji2.user } - let_it_be(:public_reply_emoji_author) { note_emoji3.user } - let_it_be(:internal_reply_emoji_author) { note_emoji4.user } + let_it_be(:issue_emoji_author) { create(:award_emoji, name: 'thumbsup', awardable: issue).user } + let_it_be(:public_note_emoji_author) { create(:award_emoji, name: 'thumbsup', awardable: public_note).user } + let_it_be(:internal_note_emoji_author) { create(:award_emoji, name: 'thumbsup', awardable: internal_note).user } + let_it_be(:public_reply_emoji_author) { create(:award_emoji, name: 'thumbsup', awardable: public_reply).user } + let_it_be(:internal_reply_emoji_author) { create(:award_emoji, name: 'thumbsup', awardable: internal_reply).user } - subject(:resolved_items) { resolve(described_class, args: {}, ctx: { current_user: current_user }, obj: issue)&.items } + subject(:resolved_items) do + resolve(described_class, args: {}, ctx: { current_user: current_user }, obj: issue)&.items + end - before do - project.add_guest(guest) - project.add_developer(user) + before_all do + create(:system_note, project: project, noteable: issue, author: system_note_author) + create( + :system_note, + note: "mentioned in issue #{private_issue.to_reference(full: true)}", + project: project, noteable: issue, author: internal_system_note_author + ) + create(:system_note_metadata, note: public_note) end context 'when current user is not set' do @@ -54,7 +69,8 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do public_note_author, public_note_emoji_author, public_reply_author, - public_reply_emoji_author + public_reply_emoji_author, + system_note_author ] ) end @@ -71,7 +87,8 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do public_note_author, public_note_emoji_author, public_reply_author, - public_reply_emoji_author + public_reply_emoji_author, + system_note_author ] ) end @@ -92,13 +109,17 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do internal_note_emoji_author, internal_reply_author, public_reply_emoji_author, - internal_reply_emoji_author + internal_reply_emoji_author, + system_note_author, + internal_system_note_author ] ) end context 'N+1 queries' do - let(:query) { -> { resolve(described_class, args: {}, ctx: { current_user: current_user }, obj: issue)&.items } } + let(:query) do + -> { resolve(described_class, args: {}, ctx: { current_user: current_user }, obj: issue)&.items } + end before do # warm-up diff --git a/spec/lib/sidebars/groups/super_sidebar_menus/deploy_menu_spec.rb b/spec/lib/sidebars/groups/super_sidebar_menus/deploy_menu_spec.rb new file mode 100644 index 00000000000..ec3f911d8dc --- /dev/null +++ b/spec/lib/sidebars/groups/super_sidebar_menus/deploy_menu_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Groups::SuperSidebarMenus::DeployMenu, feature_category: :navigation do + subject { described_class.new({}) } + + let(:items) { subject.instance_variable_get(:@items) } + + it 'has title and sprite_icon' do + expect(subject.title).to eq(s_("Navigation|Deploy")) + expect(subject.sprite_icon).to eq("deployments") + end + + it 'defines list of NilMenuItem placeholders' do + expect(items.map(&:class).uniq).to eq([Sidebars::NilMenuItem]) + expect(items.map(&:item_id)).to eq([ + :packages_registry + ]) + end +end diff --git a/spec/lib/sidebars/groups/super_sidebar_menus/operations_menu_spec.rb b/spec/lib/sidebars/groups/super_sidebar_menus/operations_menu_spec.rb index e9c2701021c..df37d5f1b0d 100644 --- a/spec/lib/sidebars/groups/super_sidebar_menus/operations_menu_spec.rb +++ b/spec/lib/sidebars/groups/super_sidebar_menus/operations_menu_spec.rb @@ -9,14 +9,13 @@ RSpec.describe Sidebars::Groups::SuperSidebarMenus::OperationsMenu, feature_cate it 'has title and sprite_icon' do expect(subject.title).to eq(s_("Navigation|Operate")) - expect(subject.sprite_icon).to eq("deployments") + expect(subject.sprite_icon).to eq("cloud-pod") end it 'defines list of NilMenuItem placeholders' do expect(items.map(&:class).uniq).to eq([Sidebars::NilMenuItem]) expect(items.map(&:item_id)).to eq([ :dependency_proxy, - :packages_registry, :container_registry, :group_kubernetes_clusters ]) diff --git a/spec/lib/sidebars/groups/super_sidebar_panel_spec.rb b/spec/lib/sidebars/groups/super_sidebar_panel_spec.rb index 5035da9c488..245d1eca0a4 100644 --- a/spec/lib/sidebars/groups/super_sidebar_panel_spec.rb +++ b/spec/lib/sidebars/groups/super_sidebar_panel_spec.rb @@ -36,6 +36,7 @@ RSpec.describe Sidebars::Groups::SuperSidebarPanel, feature_category: :navigatio Sidebars::Groups::SuperSidebarMenus::PlanMenu, Sidebars::Groups::SuperSidebarMenus::CodeMenu, Sidebars::Groups::SuperSidebarMenus::BuildMenu, + Sidebars::Groups::SuperSidebarMenus::DeployMenu, Sidebars::Groups::SuperSidebarMenus::SecureMenu, Sidebars::Groups::SuperSidebarMenus::OperationsMenu, Sidebars::Groups::SuperSidebarMenus::MonitorMenu, diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb index d459d47c31a..b7d05867d77 100644 --- a/spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb +++ b/spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb @@ -23,8 +23,7 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::AnalyzeMenu, feature_categ :code_review, :merge_request_analytics, :issues, - :insights, - :model_experiments + :insights ]) end end diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/build_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/build_menu_spec.rb index 3f2a40e1c7d..06b87003d83 100644 --- a/spec/lib/sidebars/projects/super_sidebar_menus/build_menu_spec.rb +++ b/spec/lib/sidebars/projects/super_sidebar_menus/build_menu_spec.rb @@ -18,10 +18,7 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::BuildMenu, feature_categor :pipelines, :jobs, :pipelines_editor, - :releases, - :environments, :pipeline_schedules, - :feature_flags, :test_cases, :artifacts ]) diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/deploy_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/deploy_menu_spec.rb new file mode 100644 index 00000000000..50eee173d31 --- /dev/null +++ b/spec/lib/sidebars/projects/super_sidebar_menus/deploy_menu_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Projects::SuperSidebarMenus::DeployMenu, feature_category: :navigation do + subject { described_class.new({}) } + + let(:items) { subject.instance_variable_get(:@items) } + + it 'has title and sprite_icon' do + expect(subject.title).to eq(s_("Navigation|Deploy")) + expect(subject.sprite_icon).to eq("deployments") + end + + it 'defines list of NilMenuItem placeholders' do + expect(items.map(&:class).uniq).to eq([Sidebars::NilMenuItem]) + expect(items.map(&:item_id)).to eq([ + :releases, + :feature_flags, + :packages_registry, + :container_registry, + :model_experiments + ]) + end +end diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/operations_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/operations_menu_spec.rb index 6ab070c40ae..68ca4fe2aa0 100644 --- a/spec/lib/sidebars/projects/super_sidebar_menus/operations_menu_spec.rb +++ b/spec/lib/sidebars/projects/super_sidebar_menus/operations_menu_spec.rb @@ -9,14 +9,13 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::OperationsMenu, feature_ca it 'has title and sprite_icon' do expect(subject.title).to eq(s_("Navigation|Operate")) - expect(subject.sprite_icon).to eq("deployments") + expect(subject.sprite_icon).to eq("cloud-pod") end it 'defines list of NilMenuItem placeholders' do expect(items.map(&:class).uniq).to eq([Sidebars::NilMenuItem]) expect(items.map(&:item_id)).to eq([ - :packages_registry, - :container_registry, + :environments, :kubernetes, :terraform_states, :infrastructure_registry, diff --git a/spec/lib/sidebars/projects/super_sidebar_panel_spec.rb b/spec/lib/sidebars/projects/super_sidebar_panel_spec.rb index 93f0072a111..9ed328f5090 100644 --- a/spec/lib/sidebars/projects/super_sidebar_panel_spec.rb +++ b/spec/lib/sidebars/projects/super_sidebar_panel_spec.rb @@ -47,6 +47,7 @@ RSpec.describe Sidebars::Projects::SuperSidebarPanel, feature_category: :navigat Sidebars::Projects::SuperSidebarMenus::PlanMenu, Sidebars::Projects::SuperSidebarMenus::CodeMenu, Sidebars::Projects::SuperSidebarMenus::BuildMenu, + Sidebars::Projects::SuperSidebarMenus::DeployMenu, Sidebars::Projects::SuperSidebarMenus::SecureMenu, Sidebars::Projects::SuperSidebarMenus::OperationsMenu, Sidebars::Projects::SuperSidebarMenus::MonitorMenu, diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb index d3cb386e8e0..71dd543b3ec 100644 --- a/spec/models/integrations/jira_spec.rb +++ b/spec/models/integrations/jira_spec.rb @@ -326,6 +326,18 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do end end end + + context 'with long running regex' do + let(:key) { "JIRAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1\nanother line\n" } + + before do + jira_integration.jira_issue_regex = '((a|b)+|c)+$' + end + + it 'handles long inputs' do + expect(jira_integration.reference_pattern.match(key).to_s).to eq('') + end + end end describe '.valid_jira_cloud_url?' do diff --git a/spec/models/integrations/pipelines_email_spec.rb b/spec/models/integrations/pipelines_email_spec.rb index 37a3849a768..7e80defcb87 100644 --- a/spec/models/integrations/pipelines_email_spec.rb +++ b/spec/models/integrations/pipelines_email_spec.rb @@ -84,7 +84,7 @@ RSpec.describe Integrations::PipelinesEmail, :mailer do end it 'sends email' do - emails = receivers.map { |r| double(notification_email_or_default: r) } + emails = receivers.map { |r| double(notification_email_or_default: r, username: r, id: r) } should_only_email(*emails) end @@ -206,10 +206,6 @@ RSpec.describe Integrations::PipelinesEmail, :mailer do end context 'with recipients' do - context 'with failed pipeline' do - it_behaves_like 'sending email' - end - context 'with succeeded pipeline' do before do data[:object_attributes][:status] = 'success' @@ -240,10 +236,7 @@ RSpec.describe Integrations::PipelinesEmail, :mailer do context 'when the pipeline failed' do context 'on default branch' do - before do - data[:object_attributes][:ref] = project.default_branch - pipeline.update!(ref: project.default_branch) - end + it_behaves_like 'sending email' context 'notifications are enabled only for default branch' do it_behaves_like 'sending email', branches_to_be_notified: "default" @@ -253,7 +246,7 @@ RSpec.describe Integrations::PipelinesEmail, :mailer do it_behaves_like 'not sending email', branches_to_be_notified: "protected" end - context 'notifications are enabled only for default and protected branches ' do + context 'notifications are enabled only for default and protected branches' do it_behaves_like 'sending email', branches_to_be_notified: "default_and_protected" end @@ -273,11 +266,13 @@ RSpec.describe Integrations::PipelinesEmail, :mailer do it_behaves_like 'not sending email', branches_to_be_notified: "default" end - context 'notifications are enabled only for protected branch' do + context 'notifications are enabled only for protected branch', + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/411331' do it_behaves_like 'sending email', branches_to_be_notified: "protected" end - context 'notifications are enabled only for default and protected branches ' do + context 'notifications are enabled only for default and protected branches', + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/411331' do it_behaves_like 'sending email', branches_to_be_notified: "default_and_protected" end diff --git a/spec/requests/api/v3/github_spec.rb b/spec/requests/api/v3/github_spec.rb index b6fccd9b7cb..fbda291e901 100644 --- a/spec/requests/api/v3/github_spec.rb +++ b/spec/requests/api/v3/github_spec.rb @@ -13,16 +13,33 @@ RSpec.describe API::V3::Github, :aggregate_failures, feature_category: :integrat end describe 'GET /orgs/:namespace/repos' do + let_it_be(:group) { create(:group) } + it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do subject do - group = create(:group) jira_get v3_api("/orgs/#{group.path}/repos", user) end end - it 'returns an empty array' do - group = create(:group) + it 'logs when the endpoint is hit and `jira_dvcs_end_of_life_amnesty` is enabled' do + expect(Gitlab::JsonLogger).to receive(:info).with( + including( + namespace: group.path, + user_id: user.id, + message: 'Deprecated Jira DVCS endpoint request' + ) + ) + + jira_get v3_api("/orgs/#{group.path}/repos", user) + + stub_feature_flags(jira_dvcs_end_of_life_amnesty: false) + expect(Gitlab::JsonLogger).not_to receive(:info) + + jira_get v3_api("/orgs/#{group.path}/repos", user) + end + + it 'returns an empty array' do jira_get v3_api("/orgs/#{group.path}/repos", user) expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/support/shared_examples/models/concerns/participable_shared_examples.rb b/spec/support/shared_examples/models/concerns/participable_shared_examples.rb index ec7a9105bb2..f772cfc6bbd 100644 --- a/spec/support/shared_examples/models/concerns/participable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/participable_shared_examples.rb @@ -10,13 +10,14 @@ RSpec.shared_examples 'visible participants for issuable with read ability' do | allow(model).to receive(:participant_attrs).and_return([:bar]) end - shared_examples 'check for participables read ability' do |ability_name| + shared_examples 'check for participables read ability' do |ability_name, ability_source: nil| it 'receives expected ability' do instance = model.new + source = ability_source == :participable_source ? participable_source : instance allow(instance).to receive(:bar).and_return(participable_source) - expect(Ability).to receive(:allowed?).with(anything, ability_name, instance) + expect(Ability).to receive(:allowed?).with(anything, ability_name, source) expect(instance.visible_participants(user1)).to be_empty end @@ -39,4 +40,10 @@ RSpec.shared_examples 'visible participants for issuable with read ability' do | it_behaves_like 'check for participables read ability', :read_internal_note end + + context 'when source is a system note' do + let(:participable_source) { build(:system_note) } + + it_behaves_like 'check for participables read ability', :read_note, ability_source: :participable_source + end end |