diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-07 18:08:12 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-07 18:08:12 +0300 |
commit | 7bbc731c75d0b8bf7c74ba77d521266d2ed0a1fc (patch) | |
tree | 4cab2383639b839613ffc4ef457e2a594f61aaa3 /spec | |
parent | edb317e9fe43c62229805fae529c550467ee5dc5 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
20 files changed, 651 insertions, 193 deletions
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index 01e2571ff3e..247a5d3cd6c 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -11,7 +11,7 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do end def created_personal_access_token - find("[data-testid='new-access-token'] input").value + find_field('new-access-token').value end def feed_token_description diff --git a/spec/frontend/access_tokens/components/new_access_token_app_spec.js b/spec/frontend/access_tokens/components/new_access_token_app_spec.js index b750a955fb2..25b3eba6587 100644 --- a/spec/frontend/access_tokens/components/new_access_token_app_spec.js +++ b/spec/frontend/access_tokens/components/new_access_token_app_spec.js @@ -1,7 +1,7 @@ import { GlAlert } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue'; import { createAlert, VARIANT_INFO } from '~/flash'; import { __, sprintf } from '~/locale'; @@ -16,7 +16,7 @@ describe('~/access_tokens/components/new_access_token_app', () => { const accessTokenType = 'personal access token'; const createComponent = (provide = { accessTokenType }) => { - wrapper = shallowMount(NewAccessTokenApp, { + wrapper = mountExtended(NewAccessTokenApp, { provide, }); }; @@ -64,17 +64,26 @@ describe('~/access_tokens/components/new_access_token_app', () => { sprintf(__('Copy %{accessTokenType}'), { accessTokenType }), ); expect(InputCopyToggleVisibilityComponent.props('initialVisibility')).toBe(true); - expect(InputCopyToggleVisibilityComponent.props('inputClass')).toBe( - 'qa-created-access-token', - ); - expect(InputCopyToggleVisibilityComponent.props('qaSelector')).toBe( - 'created_access_token_field', - ); expect(InputCopyToggleVisibilityComponent.attributes('label')).toBe( sprintf(__('Your new %{accessTokenType}'), { accessTokenType }), ); }); + it('input field should contain QA-related selectors', async () => { + const newToken = '12345'; + await triggerSuccess(newToken); + + expect(wrapper.findComponent(GlAlert).exists()).toBe(false); + + const inputAttributes = wrapper + .findByLabelText(sprintf(__('Your new %{accessTokenType}'), { accessTokenType })) + .attributes(); + expect(inputAttributes).toMatchObject({ + class: expect.stringContaining('qa-created-access-token'), + 'data-qa-selector': 'created_access_token_field', + }); + }); + it('should render an info alert', async () => { await triggerSuccess(); diff --git a/spec/frontend/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/runner/components/registration/registration_dropdown_spec.js index 81c2788f084..e27963161d8 100644 --- a/spec/frontend/runner/components/registration/registration_dropdown_spec.js +++ b/spec/frontend/runner/components/registration/registration_dropdown_spec.js @@ -1,4 +1,4 @@ -import { GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui'; +import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui'; import { mount, shallowMount, createWrapper } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; @@ -24,6 +24,8 @@ import { const mockToken = '0123456789'; const maskToken = '**********'; +Vue.use(VueApollo); + describe('RegistrationDropdown', () => { let wrapper; @@ -32,9 +34,10 @@ describe('RegistrationDropdown', () => { const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem); const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm); const findRegistrationToken = () => wrapper.findComponent(RegistrationToken); - const findRegistrationTokenInput = () => wrapper.findByTestId('token-value').find('input'); + const findRegistrationTokenInput = () => wrapper.find('[name=token-value]'); const findTokenResetDropdownItem = () => wrapper.findComponent(RegistrationTokenResetDropdownItem); + const findModal = () => wrapper.findComponent(GlModal); const findModalContent = () => createWrapper(document.body) .find('[data-testid="runner-instructions-modal"]') @@ -43,6 +46,8 @@ describe('RegistrationDropdown', () => { const openModal = async () => { await findRegistrationInstructionsDropdownItem().trigger('click'); + findModal().vm.$emit('shown'); + await waitForPromises(); }; @@ -60,8 +65,6 @@ describe('RegistrationDropdown', () => { }; const createComponentWithModal = () => { - Vue.use(VueApollo); - const requestHandlers = [ [getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)], [getRunnerSetupInstructionsQuery, jest.fn().mockResolvedValue(mockGraphqlInstructions)], diff --git a/spec/frontend/terraform/components/states_table_spec.js b/spec/frontend/terraform/components/states_table_spec.js index fa9c8320b4f..3821b7beb5f 100644 --- a/spec/frontend/terraform/components/states_table_spec.js +++ b/spec/frontend/terraform/components/states_table_spec.js @@ -2,6 +2,8 @@ import { GlBadge, GlLoadingIcon, GlTooltip } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { useFakeDate } from 'helpers/fake_date'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import StatesTable from '~/terraform/components/states_table.vue'; import StateActions from '~/terraform/components/states_table_actions.vue'; @@ -104,11 +106,30 @@ describe('StatesTable', () => { updatedAt: '2020-10-10T00:00:00Z', latestVersion: null, }, + { + _showDetails: false, + errorMessages: [], + name: 'state-6', + loadingLock: false, + loadingRemove: false, + lockedAt: null, + lockedByUser: null, + updatedAt: '2020-10-10T00:00:00Z', + deletedAt: '2022-02-02T00:00:00Z', + latestVersion: null, + }, ], }; const createComponent = async (propsData = defaultProps) => { - wrapper = mount(StatesTable, { propsData }); + wrapper = extendedWrapper( + mount(StatesTable, { + propsData, + directives: { + GlTooltip: createMockDirective(), + }, + }), + ); await nextTick(); }; @@ -124,27 +145,28 @@ describe('StatesTable', () => { }); it.each` - name | toolTipText | locked | loading | lineNumber + name | toolTipText | hasBadge | loading | lineNumber ${'state-1'} | ${'Locked by user-1 2 days ago'} | ${true} | ${false} | ${0} ${'state-2'} | ${'Locking state'} | ${false} | ${true} | ${1} ${'state-3'} | ${'Unlocking state'} | ${false} | ${true} | ${2} ${'state-4'} | ${'Locked by Unknown User 5 days ago'} | ${true} | ${false} | ${3} ${'state-5'} | ${'Removing'} | ${false} | ${true} | ${4} + ${'state-6'} | ${'Deletion in progress'} | ${true} | ${false} | ${5} `( 'displays the name and locked information "$name" for line "$lineNumber"', - ({ name, toolTipText, locked, loading, lineNumber }) => { + ({ name, toolTipText, hasBadge, loading, lineNumber }) => { const states = wrapper.findAll('[data-testid="terraform-states-table-name"]'); - const state = states.at(lineNumber); - const toolTip = state.find(GlTooltip); expect(state.text()).toContain(name); - expect(state.find(GlBadge).exists()).toBe(locked); + expect(state.find(GlBadge).exists()).toBe(hasBadge); expect(state.find(GlLoadingIcon).exists()).toBe(loading); - expect(toolTip.exists()).toBe(locked); - if (locked) { - expect(toolTip.text()).toMatchInterpolatedText(toolTipText); + if (hasBadge) { + const badge = wrapper.findByTestId(`state-badge-${name}`); + + expect(getBinding(badge.element, 'gl-tooltip')).toBeDefined(); + expect(badge.attributes('title')).toMatchInterpolatedText(toolTipText); } }, ); diff --git a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js index f42061d5982..e1da8b690af 100644 --- a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js +++ b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js @@ -66,7 +66,7 @@ describe('InputCopyToggleVisibility', () => { }); it('displays value as hidden', () => { - expect(findFormInputGroup().props('value')).toBe('********************'); + expect(findFormInput().element.value).toBe('********************'); }); it('saves actual value to clipboard when manually copied', () => { @@ -107,7 +107,7 @@ describe('InputCopyToggleVisibility', () => { }); it('displays value', () => { - expect(findFormInputGroup().props('value')).toBe(valueProp); + expect(findFormInput().element.value).toBe(valueProp); }); it('renders a hide button', () => { @@ -159,25 +159,52 @@ describe('InputCopyToggleVisibility', () => { }); it('displays value as hidden with 20 asterisks', () => { - expect(findFormInputGroup().props('value')).toBe('********************'); + expect(findFormInput().element.value).toBe('********************'); }); }); describe('when `initialVisibility` prop is `true`', () => { + const label = 'My label'; + beforeEach(() => { createComponent({ propsData: { value: valueProp, initialVisibility: true, + label, + 'label-for': 'my-input', + formInputGroupProps: { + id: 'my-input', + }, }, }); }); it('displays value', () => { - expect(findFormInputGroup().props('value')).toBe(valueProp); + expect(findFormInput().element.value).toBe(valueProp); }); itDoesNotModifyCopyEvent(); + + describe('when input is clicked', () => { + it('selects input value', async () => { + const mockSelect = jest.fn(); + wrapper.vm.$refs.input.$el.select = mockSelect; + await wrapper.findByLabelText(label).trigger('click'); + + expect(mockSelect).toHaveBeenCalled(); + }); + }); + + describe('when label is clicked', () => { + it('selects input value', async () => { + const mockSelect = jest.fn(); + wrapper.vm.$refs.input.$el.select = mockSelect; + await wrapper.find('label').trigger('click'); + + expect(mockSelect).toHaveBeenCalled(); + }); + }); }); describe('when `showToggleVisibilityButton` is `false`', () => { @@ -196,7 +223,7 @@ describe('InputCopyToggleVisibility', () => { }); it('displays value', () => { - expect(findFormInputGroup().props('value')).toBe(valueProp); + expect(findFormInput().element.value).toBe(valueProp); }); itDoesNotModifyCopyEvent(); @@ -216,16 +243,30 @@ describe('InputCopyToggleVisibility', () => { }); }); - it('passes `formInputGroupProps` prop to `GlFormInputGroup`', () => { + it('passes `formInputGroupProps` prop only to the input', () => { createComponent({ propsData: { formInputGroupProps: { - label: 'Foo bar', + name: 'Foo bar', + 'data-qa-selector': 'Foo bar', + class: 'Foo bar', + id: 'Foo bar', }, }, }); - expect(findFormInputGroup().props('label')).toBe('Foo bar'); + expect(findFormInput().attributes()).toMatchObject({ + name: 'Foo bar', + 'data-qa-selector': 'Foo bar', + class: expect.stringContaining('Foo bar'), + id: 'Foo bar', + }); + + const attributesInputGroup = findFormInputGroup().attributes(); + expect(attributesInputGroup.name).toBeUndefined(); + expect(attributesInputGroup['data-qa-selector']).toBeUndefined(); + expect(attributesInputGroup.class).not.toContain('Foo bar'); + expect(attributesInputGroup.id).toBeUndefined(); }); it('passes `copyButtonTitle` prop to `ClipboardButton`', () => { @@ -248,32 +289,4 @@ describe('InputCopyToggleVisibility', () => { expect(wrapper.findByText(description).exists()).toBe(true); }); - - it('passes `inputClass` prop to `GlFormInputGroup`', () => { - createComponent(); - expect(findFormInputGroup().props('inputClass')).toBe('gl-font-monospace! gl-cursor-default!'); - wrapper.destroy(); - - createComponent({ - propsData: { - inputClass: 'Foo bar', - }, - }); - expect(findFormInputGroup().props('inputClass')).toBe( - 'gl-font-monospace! gl-cursor-default! Foo bar', - ); - }); - - it('passes `qaSelector` prop as an `data-qa-selector` attribute to `GlFormInputGroup`', () => { - createComponent(); - expect(findFormInputGroup().attributes('data-qa-selector')).toBeUndefined(); - wrapper.destroy(); - - createComponent({ - propsData: { - qaSelector: 'Foo bar', - }, - }); - expect(findFormInputGroup().attributes('data-qa-selector')).toBe('Foo bar'); - }); }); diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js index 639ca9e7559..7173abe1316 100644 --- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js +++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js @@ -48,13 +48,12 @@ describe('RunnerInstructionsModal component', () => { const findModal = () => wrapper.findComponent(GlModal); const findPlatformButtonGroup = () => wrapper.findByTestId('platform-buttons'); const findPlatformButtons = () => findPlatformButtonGroup().findAllComponents(GlButton); - const findOsxPlatformButton = () => wrapper.find({ ref: 'osx' }); const findArchitectureDropdownItems = () => wrapper.findAllByTestId('architecture-dropdown-item'); const findBinaryDownloadButton = () => wrapper.findByTestId('binary-download-button'); const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions'); const findRegisterCommand = () => wrapper.findByTestId('register-command'); - const createComponent = ({ props, ...options } = {}) => { + const createComponent = ({ props, shown = true, ...options } = {}) => { const requestHandlers = [ [getRunnerPlatformsQuery, runnerPlatformsHandler], [getRunnerSetupInstructionsQuery, runnerSetupInstructionsHandler], @@ -73,184 +72,202 @@ describe('RunnerInstructionsModal component', () => { ...options, }), ); + + // trigger open modal + if (shown) { + findModal().vm.$emit('shown'); + } }; beforeEach(async () => { runnerPlatformsHandler = jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms); runnerSetupInstructionsHandler = jest.fn().mockResolvedValue(mockGraphqlInstructions); - - createComponent(); - await waitForPromises(); }); afterEach(() => { wrapper.destroy(); }); - it('should not show alert', () => { - expect(findAlert().exists()).toBe(false); - }); - - it('should contain a number of platforms buttons', () => { - expect(runnerPlatformsHandler).toHaveBeenCalledWith({}); + describe('when the modal is shown', () => { + beforeEach(async () => { + createComponent(); + await waitForPromises(); + }); - const buttons = findPlatformButtons(); + it('should not show alert', async () => { + expect(findAlert().exists()).toBe(false); + }); - expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length); - }); + it('should contain a number of platforms buttons', () => { + expect(runnerPlatformsHandler).toHaveBeenCalledWith({}); - it('should contain a number of dropdown items for the architecture options', () => { - expect(findArchitectureDropdownItems()).toHaveLength( - mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length, - ); - }); + const buttons = findPlatformButtons(); - describe('should display default instructions', () => { - const { installInstructions, registerInstructions } = mockGraphqlInstructions.data.runnerSetup; + expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length); + }); - it('runner instructions are requested', () => { - expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({ - platform: 'linux', - architecture: 'amd64', - }); + it('should contain a number of dropdown items for the architecture options', () => { + expect(findArchitectureDropdownItems()).toHaveLength( + mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length, + ); }); - it('binary instructions are shown', async () => { - await waitForPromises(); - const instructions = findBinaryInstructions().text(); + describe('should display default instructions', () => { + const { + installInstructions, + registerInstructions, + } = mockGraphqlInstructions.data.runnerSetup; - expect(instructions).toBe(installInstructions); - }); + it('runner instructions are requested', () => { + expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({ + platform: 'linux', + architecture: 'amd64', + }); + }); - it('register command is shown with a replaced token', async () => { - await waitForPromises(); - const instructions = findRegisterCommand().text(); + it('binary instructions are shown', async () => { + const instructions = findBinaryInstructions().text(); - expect(instructions).toBe( - 'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN', - ); - }); + expect(instructions).toBe(installInstructions); + }); - describe('when a register token is not shown', () => { - beforeEach(async () => { - createComponent({ props: { registrationToken: undefined } }); - await waitForPromises(); + it('register command is shown with a replaced token', async () => { + const command = findRegisterCommand().text(); + + expect(command).toBe( + 'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN', + ); }); - it('register command is shown without a defined registration token', () => { - const instructions = findRegisterCommand().text(); + describe('when a register token is not shown', () => { + beforeEach(async () => { + createComponent({ props: { registrationToken: undefined } }); + await waitForPromises(); + }); + + it('register command is shown without a defined registration token', () => { + const instructions = findRegisterCommand().text(); - expect(instructions).toBe(registerInstructions); + expect(instructions).toBe(registerInstructions); + }); }); - }); - describe('when the modal is shown', () => { - it('sets the focus on the selected platform', () => { - findPlatformButtons().at(0).element.focus = jest.fn(); + describe('when providing a defaultPlatformName', () => { + beforeEach(async () => { + createComponent({ props: { defaultPlatformName: 'osx' } }); + await waitForPromises(); + }); + + it('runner instructions for the default selected platform are requested', () => { + expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({ + platform: 'osx', + architecture: 'amd64', + }); + }); + + it('sets the focus on the default selected platform', () => { + const findOsxPlatformButton = () => wrapper.find({ ref: 'osx' }); - findModal().vm.$emit('shown'); + findOsxPlatformButton().element.focus = jest.fn(); - expect(findPlatformButtons().at(0).element.focus).toHaveBeenCalled(); + findModal().vm.$emit('shown'); + + expect(findOsxPlatformButton().element.focus).toHaveBeenCalled(); + }); }); }); - describe('when providing a defaultPlatformName', () => { + describe('after a platform and architecture are selected', () => { + const windowsIndex = 2; + const { installInstructions } = mockGraphqlInstructionsWindows.data.runnerSetup; + beforeEach(async () => { - createComponent({ props: { defaultPlatformName: 'osx' } }); + runnerSetupInstructionsHandler.mockResolvedValue(mockGraphqlInstructionsWindows); + + findPlatformButtons().at(windowsIndex).vm.$emit('click'); await waitForPromises(); }); - it('runner instructions for the default selected platform are requested', () => { - expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({ - platform: 'osx', + it('runner instructions are requested', () => { + expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({ + platform: 'windows', architecture: 'amd64', }); }); - it('sets the focus on the default selected platform', () => { - findOsxPlatformButton().element.focus = jest.fn(); + it('architecture download link is updated', () => { + const architectures = + mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[windowsIndex].architectures.nodes; - findModal().vm.$emit('shown'); - - expect(findOsxPlatformButton().element.focus).toHaveBeenCalled(); + expect(findBinaryDownloadButton().attributes('href')).toBe( + architectures[0].downloadLocation, + ); }); - }); - }); - - describe('after a platform and architecture are selected', () => { - const windowsIndex = 2; - const { installInstructions } = mockGraphqlInstructionsWindows.data.runnerSetup; - beforeEach(async () => { - runnerSetupInstructionsHandler.mockResolvedValue(mockGraphqlInstructionsWindows); + it('other binary instructions are shown', () => { + const instructions = findBinaryInstructions().text(); - findPlatformButtons().at(windowsIndex).vm.$emit('click'); - await waitForPromises(); - }); - - it('runner instructions are requested', () => { - expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({ - platform: 'windows', - architecture: 'amd64', + expect(instructions).toBe(installInstructions); }); - }); - it('architecture download link is updated', () => { - const architectures = - mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[windowsIndex].architectures.nodes; + it('register command is shown', () => { + const command = findRegisterCommand().text(); - expect(findBinaryDownloadButton().attributes('href')).toBe(architectures[0].downloadLocation); - }); + expect(command).toBe( + './gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token MY_TOKEN', + ); + }); - it('other binary instructions are shown', () => { - const instructions = findBinaryInstructions().text(); + it('runner instructions are requested with another architecture', async () => { + findArchitectureDropdownItems().at(1).vm.$emit('click'); + await waitForPromises(); - expect(instructions).toBe(installInstructions); + expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({ + platform: 'windows', + architecture: '386', + }); + }); }); - it('register command is shown', () => { - const command = findRegisterCommand().text(); + describe('when the modal resizes', () => { + it('to an xs viewport', async () => { + MockResizeObserver.mockResize('xs'); + await nextTick(); - expect(command).toBe( - './gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token MY_TOKEN', - ); - }); + expect(findPlatformButtonGroup().attributes('vertical')).toBeTruthy(); + }); - it('runner instructions are requested with another architecture', async () => { - findArchitectureDropdownItems().at(1).vm.$emit('click'); - await waitForPromises(); + it('to a non-xs viewport', async () => { + MockResizeObserver.mockResize('sm'); + await nextTick(); - expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({ - platform: 'windows', - architecture: '386', + expect(findPlatformButtonGroup().props('vertical')).toBeFalsy(); }); }); }); - describe('when the modal resizes', () => { - it('to an xs viewport', async () => { - MockResizeObserver.mockResize('xs'); - await nextTick(); - - expect(findPlatformButtonGroup().attributes('vertical')).toBeTruthy(); + describe('when the modal is not shown', () => { + beforeEach(async () => { + createComponent({ shown: false }); + await waitForPromises(); }); - it('to a non-xs viewport', async () => { - MockResizeObserver.mockResize('sm'); - await nextTick(); - - expect(findPlatformButtonGroup().props('vertical')).toBeFalsy(); + it('does not fetch instructions', () => { + expect(runnerPlatformsHandler).not.toHaveBeenCalled(); + expect(runnerSetupInstructionsHandler).not.toHaveBeenCalled(); }); }); describe('when apollo is loading', () => { - it('should show a skeleton loader', async () => { + beforeEach(() => { createComponent(); + }); + + it('should show a skeleton loader', async () => { expect(findSkeletonLoader().exists()).toBe(true); expect(findGlLoadingIcon().exists()).toBe(false); - await nextTick(); - jest.runOnlyPendingTimers(); + // wait on fetch of both `platforms` and `instructions` await nextTick(); await nextTick(); @@ -258,7 +275,6 @@ describe('RunnerInstructionsModal component', () => { }); it('once loaded, should not show a loading state', async () => { - createComponent(); await waitForPromises(); expect(findSkeletonLoader().exists()).toBe(false); @@ -271,7 +287,6 @@ describe('RunnerInstructionsModal component', () => { runnerSetupInstructionsHandler.mockRejectedValue(); createComponent(); - await waitForPromises(); }); @@ -303,6 +318,7 @@ describe('RunnerInstructionsModal component', () => { mockShow = jest.fn(); createComponent({ + shown: false, stubs: { GlModal: getGlModalStub({ show: mockShow }), }, diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js index 9a95a838291..986d76d2b95 100644 --- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js +++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js @@ -1,6 +1,5 @@ -import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue'; import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; @@ -11,7 +10,11 @@ describe('RunnerInstructions component', () => { const findModal = () => wrapper.findComponent(RunnerInstructionsModal); const createComponent = () => { - wrapper = extendedWrapper(shallowMount(RunnerInstructions)); + wrapper = shallowMountExtended(RunnerInstructions, { + directives: { + GlModal: createMockDirective(), + }, + }); }; beforeEach(() => { @@ -23,19 +26,12 @@ describe('RunnerInstructions component', () => { }); it('should show the "Show runner installation instructions" button', () => { - expect(findModalButton().exists()).toBe(true); expect(findModalButton().text()).toBe('Show runner installation instructions'); }); - it('should not render the modal once mounted', () => { - expect(findModal().exists()).toBe(false); - }); - - it('should render the modal once clicked', async () => { - findModalButton().vm.$emit('click'); - - await nextTick(); + it('should render the modal', () => { + const modalId = getBinding(findModal().element, 'gl-modal'); - expect(findModal().exists()).toBe(true); + expect(findModalButton().attributes('modal-id')).toBe(modalId); }); }); diff --git a/spec/graphql/types/packages/cleanup/keep_duplicated_package_files_enum_spec.rb b/spec/graphql/types/packages/cleanup/keep_duplicated_package_files_enum_spec.rb new file mode 100644 index 00000000000..d7f24a9edfd --- /dev/null +++ b/spec/graphql/types/packages/cleanup/keep_duplicated_package_files_enum_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['PackagesCleanupKeepDuplicatedPackageFilesEnum'] do + it 'exposes all options' do + expect(described_class.values.keys) + .to contain_exactly(*Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum::OPTIONS_MAPPING.values) + end + + it 'uses all possible options from model' do + all_options = Packages::Cleanup::Policy::KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES + expect(described_class::OPTIONS_MAPPING.keys).to contain_exactly(*all_options) + end +end diff --git a/spec/graphql/types/packages/cleanup/policy_type_spec.rb b/spec/graphql/types/packages/cleanup/policy_type_spec.rb new file mode 100644 index 00000000000..f48651ed832 --- /dev/null +++ b/spec/graphql/types/packages/cleanup/policy_type_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['PackagesCleanupPolicy'] do + specify { expect(described_class.graphql_name).to eq('PackagesCleanupPolicy') } + + specify do + expect(described_class.description) + .to eq('A packages cleanup policy designed to keep only packages and packages assets that matter most') + end + + specify { expect(described_class).to require_graphql_authorizations(:admin_package) } + + describe 'keep_n_duplicated_package_files' do + subject { described_class.fields['keepNDuplicatedPackageFiles'] } + + it { is_expected.to have_non_null_graphql_type(Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum) } + end + + describe 'next_run_at' do + subject { described_class.fields['nextRunAt'] } + + it { is_expected.to have_nullable_graphql_type(Types::TimeType) } + end +end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 23deef73734..2e994bf7820 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -36,7 +36,7 @@ RSpec.describe GitlabSchema.types['Project'] do pipeline_analytics squash_read_only sast_ci_configuration cluster_agent cluster_agents agent_configurations ci_template timelogs merge_commit_template squash_commit_template work_item_types - recent_issue_boards ci_config_path_or_default + recent_issue_boards ci_config_path_or_default packages_cleanup_policy ] expect(described_class).to include_graphql_fields(*expected_fields) @@ -421,6 +421,12 @@ RSpec.describe GitlabSchema.types['Project'] do it { is_expected.to have_graphql_type(Types::ContainerExpirationPolicyType) } end + describe 'packages cleanup policy field' do + subject { described_class.fields['packagesCleanupPolicy'] } + + it { is_expected.to have_graphql_type(Types::Packages::Cleanup::PolicyType) } + end + describe 'terraform state field' do subject { described_class.fields['terraformState'] } diff --git a/spec/graphql/types/terraform/state_type_spec.rb b/spec/graphql/types/terraform/state_type_spec.rb index 9f65bb926d7..5098adbf45c 100644 --- a/spec/graphql/types/terraform/state_type_spec.rb +++ b/spec/graphql/types/terraform/state_type_spec.rb @@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['TerraformState'] do it { expect(described_class).to require_graphql_authorizations(:read_terraform_state) } describe 'fields' do - let(:fields) { %i[id name locked_by_user locked_at latest_version created_at updated_at] } + let(:fields) { %i[id name locked_by_user locked_at latest_version created_at updated_at deleted_at] } it { expect(described_class).to have_graphql_fields(fields) } @@ -17,6 +17,7 @@ RSpec.describe GitlabSchema.types['TerraformState'] do it { expect(described_class.fields['lockedAt'].type).not_to be_non_null } it { expect(described_class.fields['createdAt'].type).to be_non_null } it { expect(described_class.fields['updatedAt'].type).to be_non_null } + it { expect(described_class.fields['deletedAt'].type).not_to be_non_null } it { expect(described_class.fields['latestVersion'].type).not_to be_non_null } it { expect(described_class.fields['latestVersion'].complexity).to eq(3) } diff --git a/spec/lib/gitlab/tracking/standard_context_spec.rb b/spec/lib/gitlab/tracking/standard_context_spec.rb index c88b0af30f6..508b33949a8 100644 --- a/spec/lib/gitlab/tracking/standard_context_spec.rb +++ b/spec/lib/gitlab/tracking/standard_context_spec.rb @@ -92,6 +92,34 @@ RSpec.describe Gitlab::Tracking::StandardContext do end end + context 'with incorrect argument type' do + context 'when standard_context_type_check FF is disabled' do + before do + stub_feature_flags(standard_context_type_check: false) + end + + subject { described_class.new(project: create(:group)) } + + it 'does not call `track_and_raise_for_dev_exception`' do + expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception) + snowplow_context + end + end + + context 'when standard_context_type_check FF is enabled' do + before do + stub_feature_flags(standard_context_type_check: true) + end + + subject { described_class.new(project: create(:group)) } + + it 'does call `track_and_raise_for_dev_exception`' do + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) + snowplow_context + end + end + end + it 'contains user id' do expect(snowplow_context.to_json[:data].keys).to include(:user_id) end diff --git a/spec/models/packages/cleanup/policy_spec.rb b/spec/models/packages/cleanup/policy_spec.rb index 972071aa0ad..c08ae4aa7e7 100644 --- a/spec/models/packages/cleanup/policy_spec.rb +++ b/spec/models/packages/cleanup/policy_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Packages::Cleanup::Policy, type: :model do is_expected .to validate_inclusion_of(:keep_n_duplicated_package_files) .in_array(described_class::KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES) - .with_message('keep_n_duplicated_package_files is invalid') + .with_message('is invalid') end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index d9316344474..23e4641e0d5 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -1356,6 +1356,36 @@ RSpec.describe ProjectPolicy do end end + describe 'admin_package' do + context 'with admin' do + let(:current_user) { admin } + + context 'when admin mode enabled', :enable_admin_mode do + it { is_expected.to be_allowed(:admin_package) } + end + + context 'when admin mode disabled' do + it { is_expected.to be_disallowed(:admin_package) } + end + end + + %i[owner maintainer].each do |role| + context "with #{role}" do + let(:current_user) { public_send(role) } + + it { is_expected.to be_allowed(:admin_package) } + end + end + + %i[developer reporter guest non_member anonymous].each do |role| + context "with #{role}" do + let(:current_user) { public_send(role) } + + it { is_expected.to be_disallowed(:admin_package) } + end + end + end + describe 'read_feature_flag' do subject { described_class.new(current_user, project) } diff --git a/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb b/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb new file mode 100644 index 00000000000..7e00f3ca53a --- /dev/null +++ b/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Updating the packages cleanup policy' do + include GraphqlHelpers + using RSpec::Parameterized::TableSyntax + + let_it_be(:project, reload: true) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:params) do + { + project_path: project.full_path, + keep_n_duplicated_package_files: 'TWENTY_PACKAGE_FILES' + } + end + + let(:mutation) do + graphql_mutation(:update_packages_cleanup_policy, params, + <<~QUERY + packagesCleanupPolicy { + keepNDuplicatedPackageFiles + nextRunAt + } + errors + QUERY + ) + end + + let(:mutation_response) { graphql_mutation_response(:update_packages_cleanup_policy) } + let(:packages_cleanup_policy_response) { mutation_response['packagesCleanupPolicy'] } + + shared_examples 'accepting the mutation request and updates the existing policy' do + it 'returns the updated packages cleanup policy' do + expect { subject }.not_to change { ::Packages::Cleanup::Policy.count } + + expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('20') + expect_graphql_errors_to_be_empty + expect(packages_cleanup_policy_response['keepNDuplicatedPackageFiles']) + .to eq(params[:keep_n_duplicated_package_files]) + expect(packages_cleanup_policy_response['nextRunAt']).not_to eq(nil) + end + end + + shared_examples 'accepting the mutation request and creates a policy' do + it 'returns the created packages cleanup policy' do + expect { subject }.to change { ::Packages::Cleanup::Policy.count }.by(1) + + expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('20') + expect_graphql_errors_to_be_empty + expect(packages_cleanup_policy_response['keepNDuplicatedPackageFiles']) + .to eq(params[:keep_n_duplicated_package_files]) + expect(packages_cleanup_policy_response['nextRunAt']).not_to eq(nil) + end + end + + shared_examples 'denying the mutation request' do + it 'returns an error' do + expect { subject }.not_to change { ::Packages::Cleanup::Policy.count } + + expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).not_to eq('20') + expect(mutation_response).to be_nil + expect_graphql_errors_to_include(/you don't have permission to perform this action/) + end + end + + describe 'post graphql mutation' do + subject { post_graphql_mutation(mutation, current_user: user) } + + context 'with existing packages cleanup policy' do + let_it_be(:project_packages_cleanup_policy) { create(:packages_cleanup_policy, project: project) } + + where(:user_role, :shared_examples_name) do + :maintainer | 'accepting the mutation request and updates the existing policy' + :developer | 'denying the mutation request' + :reporter | 'denying the mutation request' + :guest | 'denying the mutation request' + :anonymous | 'denying the mutation request' + end + + with_them do + before do + project.send("add_#{user_role}", user) unless user_role == :anonymous + end + + it_behaves_like params[:shared_examples_name] + end + end + + context 'without existing packages cleanup policy' do + where(:user_role, :shared_examples_name) do + :maintainer | 'accepting the mutation request and creates a policy' + :developer | 'denying the mutation request' + :reporter | 'denying the mutation request' + :guest | 'denying the mutation request' + :anonymous | 'denying the mutation request' + end + + with_them do + before do + project.send("add_#{user_role}", user) unless user_role == :anonymous + end + + it_behaves_like params[:shared_examples_name] + end + end + end +end diff --git a/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb b/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb new file mode 100644 index 00000000000..a025c57d4b8 --- /dev/null +++ b/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'getting the packages cleanup policy linked to a project' do + using RSpec::Parameterized::TableSyntax + include GraphqlHelpers + + let_it_be_with_reload(:project) { create(:project) } + let_it_be(:current_user) { project.first_owner } + + let(:fields) do + <<~QUERY + #{all_graphql_fields_for('packages_cleanup_policy'.classify)} + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('packagesCleanupPolicy', {}, fields) + ) + end + + subject { post_graphql(query, current_user: current_user) } + + it_behaves_like 'a working graphql query' do + before do + subject + end + end + + context 'with an existing policy' do + let_it_be(:policy) { create(:packages_cleanup_policy, project: project) } + + it_behaves_like 'a working graphql query' do + before do + subject + end + end + end + + context 'with different permissions' do + let_it_be(:current_user) { create(:user) } + + let(:packages_cleanup_policy_response) { graphql_data_at('project', 'packagesCleanupPolicy') } + + where(:visibility, :role, :policy_visible) do + :private | :maintainer | true + :private | :developer | false + :private | :reporter | false + :private | :guest | false + :private | :anonymous | false + :public | :maintainer | true + :public | :developer | false + :public | :reporter | false + :public | :guest | false + :public | :anonymous | false + end + + with_them do + before do + project.update!(visibility: visibility.to_s) + project.add_user(current_user, role) unless role == :anonymous + end + + it 'return the proper response' do + subject + + if policy_visible + expect(packages_cleanup_policy_response) + .to eq('keepNDuplicatedPackageFiles' => 'ALL_PACKAGE_FILES', 'nextRunAt' => nil) + else + expect(packages_cleanup_policy_response).to be_blank + end + end + end + end +end diff --git a/spec/services/packages/cleanup/update_policy_service_spec.rb b/spec/services/packages/cleanup/update_policy_service_spec.rb new file mode 100644 index 00000000000..a11fbb766f5 --- /dev/null +++ b/spec/services/packages/cleanup/update_policy_service_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Cleanup::UpdatePolicyService do + using RSpec::Parameterized::TableSyntax + + let_it_be_with_reload(:project) { create(:project) } + let_it_be(:current_user) { create(:user) } + + let(:params) { { keep_n_duplicated_package_files: 50 } } + + describe '#execute' do + subject { described_class.new(project: project, current_user: current_user, params: params).execute } + + shared_examples 'creating the policy' do + it 'creates a new one' do + expect { subject }.to change { ::Packages::Cleanup::Policy.count }.from(0).to(1) + + expect(subject.payload[:packages_cleanup_policy]).to be_present + expect(subject.success?).to be_truthy + expect(project.packages_cleanup_policy).to be_persisted + expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('50') + end + + context 'with invalid parameters' do + let(:params) { { keep_n_duplicated_package_files: 100 } } + + it 'does not create one' do + expect { subject }.not_to change { ::Packages::Cleanup::Policy.count } + + expect(subject.status).to eq(:error) + expect(subject.message).to eq('Keep n duplicated package files is invalid') + end + end + end + + shared_examples 'updating the policy' do + it 'updates the existing one' do + expect { subject }.not_to change { ::Packages::Cleanup::Policy.count } + + expect(subject.payload[:packages_cleanup_policy]).to be_present + expect(subject.success?).to be_truthy + expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('50') + end + + context 'with invalid parameters' do + let(:params) { { keep_n_duplicated_package_files: 100 } } + + it 'does not update one' do + expect { subject }.not_to change { policy.keep_n_duplicated_package_files } + + expect(subject.status).to eq(:error) + expect(subject.message).to eq('Keep n duplicated package files is invalid') + end + end + end + + shared_examples 'denying access' do + it 'returns an error' do + subject + + expect(subject.message).to eq('Access denied') + expect(subject.status).to eq(:error) + end + end + + context 'with existing container expiration policy' do + let_it_be(:policy) { create(:packages_cleanup_policy, project: project) } + + where(:user_role, :shared_examples_name) do + :maintainer | 'updating the policy' + :developer | 'denying access' + :reporter | 'denying access' + :guest | 'denying access' + :anonymous | 'denying access' + end + + with_them do + before do + project.send("add_#{user_role}", current_user) unless user_role == :anonymous + end + + it_behaves_like params[:shared_examples_name] + end + end + + context 'without existing container expiration policy' do + where(:user_role, :shared_examples_name) do + :maintainer | 'creating the policy' + :developer | 'denying access' + :reporter | 'denying access' + :guest | 'denying access' + :anonymous | 'denying access' + end + + with_them do + before do + project.send("add_#{user_role}", current_user) unless user_role == :anonymous + end + + it_behaves_like params[:shared_examples_name] + end + end + end +end diff --git a/spec/support/helpers/next_instance_of.rb b/spec/support/helpers/next_instance_of.rb index 461d411a5ce..3c88715615d 100644 --- a/spec/support/helpers/next_instance_of.rb +++ b/spec/support/helpers/next_instance_of.rb @@ -22,7 +22,7 @@ module NextInstanceOf def stub_new(target, number, ordered = false, *new_args, &blk) receive_new = receive(:new) receive_new.ordered if ordered - receive_new.with(*new_args) if new_args.any? + receive_new.with(*new_args) if new_args.present? if number.is_a?(Range) receive_new.at_least(number.begin).times if number.begin diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb index d9460c7b8f1..1f5d6ed5586 100644 --- a/spec/support/shared_examples/features/runners_shared_examples.rb +++ b/spec/support/shared_examples/features/runners_shared_examples.rb @@ -35,11 +35,11 @@ RSpec.shared_examples 'shows and resets runner registration token' do it 'has a registration token' do click_on 'Click to reveal' - expect(page.find('[data-testid="token-value"] input').value).to have_content(registration_token) + expect(page.find_field('token-value').value).to have_content(registration_token) end describe 'reset registration token' do - let!(:old_registration_token) { find('[data-testid="token-value"] input').value } + let!(:old_registration_token) { find_field('token-value').value } before do click_on 'Reset registration token' diff --git a/spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb b/spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb index be7f7ef5c8c..785cba24f9d 100644 --- a/spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb +++ b/spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Ci::ResourceGroups::AssignResourceFromResourceGroupWorker do context 'when resource group exists' do it 'executes AssignResourceFromResourceGroupService' do - expect_next_instances_of(Ci::ResourceGroups::AssignResourceFromResourceGroupService, 2, resource_group.project, nil) do |service| + expect_next_instances_of(Ci::ResourceGroups::AssignResourceFromResourceGroupService, 2, false, resource_group.project, nil) do |service| expect(service).to receive(:execute).with(resource_group) end |