diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-03 00:09:44 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-03 00:09:44 +0300 |
commit | f96f2720d1b21b76eadedc54fdea67cb70e98d94 (patch) | |
tree | 527d27d5ceb816969e315b6223b3ddb2ca128dae /spec | |
parent | ad05e1db038a2e983d25555144fa29063e060c50 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
18 files changed, 596 insertions, 151 deletions
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb index cb333bdb428..120c5b56e03 100644 --- a/spec/features/projects/settings/registry_settings_spec.rb +++ b/spec/features/projects/settings/registry_settings_spec.rb @@ -26,20 +26,20 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p subject settings_block = find('#js-registry-policies') - expect(settings_block).to have_text 'Cleanup policy for tags' + expect(settings_block).to have_text 'Clean up image tags' end it 'saves cleanup policy submit the form' do subject within '#js-registry-policies' do - within '.gl-card-body' do - select('7 days until tags are automatically removed', from: 'Expiration interval:') - select('Every day', from: 'Expiration schedule:') - select('50 tags per image name', from: 'Number of tags to retain:') - fill_in('Tags with names matching this regex pattern will expire:', with: '.*-production') - end - submit_button = find('.gl-card-footer .btn.btn-success') + select('Every day', from: 'Run cleanup') + select('50 tags per image name', from: 'Keep the most recent:') + fill_in('Keep tags matching:', with: 'stable') + select('7 days', from: 'Remove tags older than:') + fill_in('Remove tags matching:', with: '.*-production') + + submit_button = find('.btn.btn-success') expect(submit_button).not_to be_disabled submit_button.click end @@ -51,10 +51,9 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p subject within '#js-registry-policies' do - within '.gl-card-body' do - fill_in('Tags with names matching this regex pattern will expire:', with: '*-production') - end - submit_button = find('.gl-card-footer .btn.btn-success') + fill_in('Remove tags matching:', with: '*-production') + + submit_button = find('.btn.btn-success') expect(submit_button).not_to be_disabled submit_button.click end @@ -85,7 +84,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p within '#js-registry-policies' do case result when :available_section - expect(find('.gl-card-header')).to have_content('Tag expiration policy') + expect(find('[data-testid="enable-toggle"]')).to have_content('Tags that match the rules on this page are automatically scheduled for deletion.') when :disabled_message expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled') end diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js index 01c789270da..416564b72c3 100644 --- a/spec/frontend/diffs/components/app_spec.js +++ b/spec/frontend/diffs/components/app_spec.js @@ -17,10 +17,14 @@ import axios from '~/lib/utils/axios_utils'; import * as urlUtils from '~/lib/utils/url_utility'; import diffsMockData from '../mock_data/merge_request_diffs'; +import { EVT_VIEW_FILE_BY_FILE } from '~/diffs/constants'; + +import eventHub from '~/diffs/event_hub'; + const mergeRequestDiff = { version_index: 1 }; const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`; -const COMMIT_URL = '[BASE URL]/OLD'; -const UPDATED_COMMIT_URL = '[BASE URL]/NEW'; +const COMMIT_URL = `${TEST_HOST}/COMMIT/OLD`; +const UPDATED_COMMIT_URL = `${TEST_HOST}/COMMIT/NEW`; function getCollapsedFilesWarning(wrapper) { return wrapper.find(CollapsedFilesWarning); @@ -61,7 +65,7 @@ describe('diffs/components/app', () => { changesEmptyStateIllustration: '', dismissEndpoint: '', showSuggestPopover: true, - viewDiffsFileByFile: false, + fileByFileUserPreference: false, ...props, }, provide, @@ -700,12 +704,14 @@ describe('diffs/components/app', () => { }); describe('file-by-file', () => { - it('renders a single diff', () => { - createComponent({ viewDiffsFileByFile: true }, ({ state }) => { + it('renders a single diff', async () => { + createComponent({ fileByFileUserPreference: true }, ({ state }) => { state.diffs.diffFiles.push({ file_hash: '123' }); state.diffs.diffFiles.push({ file_hash: '312' }); }); + await wrapper.vm.$nextTick(); + expect(wrapper.findAll(DiffFile).length).toBe(1); }); @@ -713,31 +719,37 @@ describe('diffs/components/app', () => { const fileByFileNav = () => wrapper.find('[data-testid="file-by-file-navigation"]'); const paginator = () => fileByFileNav().find(GlPagination); - it('sets previous button as disabled', () => { - createComponent({ viewDiffsFileByFile: true }, ({ state }) => { + it('sets previous button as disabled', async () => { + createComponent({ fileByFileUserPreference: true }, ({ state }) => { state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' }); }); + await wrapper.vm.$nextTick(); + expect(paginator().attributes('prevpage')).toBe(undefined); expect(paginator().attributes('nextpage')).toBe('2'); }); - it('sets next button as disabled', () => { - createComponent({ viewDiffsFileByFile: true }, ({ state }) => { + it('sets next button as disabled', async () => { + createComponent({ fileByFileUserPreference: true }, ({ state }) => { state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' }); state.diffs.currentDiffFileId = '312'; }); + await wrapper.vm.$nextTick(); + expect(paginator().attributes('prevpage')).toBe('1'); expect(paginator().attributes('nextpage')).toBe(undefined); }); - it("doesn't display when there's fewer than 2 files", () => { - createComponent({ viewDiffsFileByFile: true }, ({ state }) => { + it("doesn't display when there's fewer than 2 files", async () => { + createComponent({ fileByFileUserPreference: true }, ({ state }) => { state.diffs.diffFiles.push({ file_hash: '123' }); state.diffs.currentDiffFileId = '123'; }); + await wrapper.vm.$nextTick(); + expect(fileByFileNav().exists()).toBe(false); }); @@ -748,11 +760,13 @@ describe('diffs/components/app', () => { `( 'it calls navigateToDiffFileIndex with $index when $link is clicked', async ({ currentDiffFileId, targetFile }) => { - createComponent({ viewDiffsFileByFile: true }, ({ state }) => { + createComponent({ fileByFileUserPreference: true }, ({ state }) => { state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' }); state.diffs.currentDiffFileId = currentDiffFileId; }); + await wrapper.vm.$nextTick(); + jest.spyOn(wrapper.vm, 'navigateToDiffFileIndex'); paginator().vm.$emit('input', targetFile); @@ -763,5 +777,24 @@ describe('diffs/components/app', () => { }, ); }); + + describe('control via event stream', () => { + it.each` + setting + ${true} + ${false} + `( + 'triggers the action with the new fileByFile setting - $setting - when the event with that setting is received', + async ({ setting }) => { + createComponent(); + await wrapper.vm.$nextTick(); + + eventHub.$emit(EVT_VIEW_FILE_BY_FILE, { setting }); + await wrapper.vm.$nextTick(); + + expect(store.state.diffs.viewDiffsFileByFile).toBe(setting); + }, + ); + }); }); }); diff --git a/spec/frontend/diffs/components/settings_dropdown_spec.js b/spec/frontend/diffs/components/settings_dropdown_spec.js index 72330d8efba..a9b1c81d00e 100644 --- a/spec/frontend/diffs/components/settings_dropdown_spec.js +++ b/spec/frontend/diffs/components/settings_dropdown_spec.js @@ -2,12 +2,18 @@ import { mount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import diffModule from '~/diffs/store/modules'; import SettingsDropdown from '~/diffs/components/settings_dropdown.vue'; -import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; +import { + EVT_VIEW_FILE_BY_FILE, + PARALLEL_DIFF_VIEW_TYPE, + INLINE_DIFF_VIEW_TYPE, +} from '~/diffs/constants'; +import eventHub from '~/diffs/event_hub'; const localVue = createLocalVue(); localVue.use(Vuex); describe('Diff settings dropdown component', () => { + let wrapper; let vm; let actions; @@ -25,10 +31,15 @@ describe('Diff settings dropdown component', () => { extendStore(store); - vm = mount(SettingsDropdown, { + wrapper = mount(SettingsDropdown, { localVue, store, }); + vm = wrapper.vm; + } + + function getFileByFileCheckbox(vueWrapper) { + return vueWrapper.find('[data-testid="file-by-file"]'); } beforeEach(() => { @@ -41,14 +52,14 @@ describe('Diff settings dropdown component', () => { }); afterEach(() => { - vm.destroy(); + wrapper.destroy(); }); describe('tree view buttons', () => { it('list view button dispatches setRenderTreeList with false', () => { createComponent(); - vm.find('.js-list-view').trigger('click'); + wrapper.find('.js-list-view').trigger('click'); expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), false); }); @@ -56,7 +67,7 @@ describe('Diff settings dropdown component', () => { it('tree view button dispatches setRenderTreeList with true', () => { createComponent(); - vm.find('.js-tree-view').trigger('click'); + wrapper.find('.js-tree-view').trigger('click'); expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), true); }); @@ -68,8 +79,8 @@ describe('Diff settings dropdown component', () => { }); }); - expect(vm.find('.js-list-view').classes('selected')).toBe(true); - expect(vm.find('.js-tree-view').classes('selected')).toBe(false); + expect(wrapper.find('.js-list-view').classes('selected')).toBe(true); + expect(wrapper.find('.js-tree-view').classes('selected')).toBe(false); }); it('sets tree button as selected when renderTreeList is true', () => { @@ -79,8 +90,8 @@ describe('Diff settings dropdown component', () => { }); }); - expect(vm.find('.js-list-view').classes('selected')).toBe(false); - expect(vm.find('.js-tree-view').classes('selected')).toBe(true); + expect(wrapper.find('.js-list-view').classes('selected')).toBe(false); + expect(wrapper.find('.js-tree-view').classes('selected')).toBe(true); }); }); @@ -92,8 +103,8 @@ describe('Diff settings dropdown component', () => { }); }); - expect(vm.find('.js-inline-diff-button').classes('selected')).toBe(true); - expect(vm.find('.js-parallel-diff-button').classes('selected')).toBe(false); + expect(wrapper.find('.js-inline-diff-button').classes('selected')).toBe(true); + expect(wrapper.find('.js-parallel-diff-button').classes('selected')).toBe(false); }); it('sets parallel button as selected', () => { @@ -103,14 +114,14 @@ describe('Diff settings dropdown component', () => { }); }); - expect(vm.find('.js-inline-diff-button').classes('selected')).toBe(false); - expect(vm.find('.js-parallel-diff-button').classes('selected')).toBe(true); + expect(wrapper.find('.js-inline-diff-button').classes('selected')).toBe(false); + expect(wrapper.find('.js-parallel-diff-button').classes('selected')).toBe(true); }); it('calls setInlineDiffViewType when clicking inline button', () => { createComponent(); - vm.find('.js-inline-diff-button').trigger('click'); + wrapper.find('.js-inline-diff-button').trigger('click'); expect(actions.setInlineDiffViewType).toHaveBeenCalled(); }); @@ -118,7 +129,7 @@ describe('Diff settings dropdown component', () => { it('calls setParallelDiffViewType when clicking parallel button', () => { createComponent(); - vm.find('.js-parallel-diff-button').trigger('click'); + wrapper.find('.js-parallel-diff-button').trigger('click'); expect(actions.setParallelDiffViewType).toHaveBeenCalled(); }); @@ -132,7 +143,7 @@ describe('Diff settings dropdown component', () => { }); }); - expect(vm.find('#show-whitespace').element.checked).toBe(false); + expect(wrapper.find('#show-whitespace').element.checked).toBe(false); }); it('sets as checked when showWhitespace is true', () => { @@ -142,13 +153,13 @@ describe('Diff settings dropdown component', () => { }); }); - expect(vm.find('#show-whitespace').element.checked).toBe(true); + expect(wrapper.find('#show-whitespace').element.checked).toBe(true); }); it('calls setShowWhitespace on change', () => { createComponent(); - const checkbox = vm.find('#show-whitespace'); + const checkbox = wrapper.find('#show-whitespace'); checkbox.element.checked = true; checkbox.trigger('change'); @@ -159,4 +170,52 @@ describe('Diff settings dropdown component', () => { }); }); }); + + describe('file-by-file toggle', () => { + beforeEach(() => { + jest.spyOn(eventHub, '$emit'); + }); + + it.each` + fileByFile | checked + ${true} | ${true} + ${false} | ${false} + `( + 'sets { checked: $checked } if the fileByFile setting is $fileByFile', + async ({ fileByFile, checked }) => { + createComponent(store => { + Object.assign(store.state.diffs, { + viewDiffsFileByFile: fileByFile, + }); + }); + + await vm.$nextTick(); + + expect(vm.checked).toBe(checked); + }, + ); + + it.each` + start | emit + ${true} | ${false} + ${false} | ${true} + `( + 'when the file by file setting starts as $start, toggling the checkbox should emit an event set to $emit', + async ({ start, emit }) => { + createComponent(store => { + Object.assign(store.state.diffs, { + viewDiffsFileByFile: start, + }); + }); + + await vm.$nextTick(); + + getFileByFileCheckbox(wrapper).trigger('click'); + + await vm.$nextTick(); + + expect(eventHub.$emit).toHaveBeenCalledWith(EVT_VIEW_FILE_BY_FILE, { setting: emit }); + }, + ); + }); }); diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index 0032fe926d0..c1a7844d301 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -48,6 +48,7 @@ import { moveToNeighboringCommit, setCurrentDiffFileIdFromNote, navigateToDiffFileIndex, + setFileByFile, } from '~/diffs/store/actions'; import eventHub from '~/notes/event_hub'; import * as types from '~/diffs/store/mutation_types'; @@ -1455,4 +1456,20 @@ describe('DiffsStoreActions', () => { ); }); }); + + describe('setFileByFile', () => { + it.each` + value + ${true} + ${false} + `('commits SET_FILE_BY_FILE with the new value $value', ({ value }) => { + return testAction( + setFileByFile, + { fileByFile: value }, + { viewDiffsFileByFile: null }, + [{ type: types.SET_FILE_BY_FILE, payload: value }], + [], + ); + }); + }); }); diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js index 2dda9a0ad71..c061c5c2590 100644 --- a/spec/frontend/diffs/store/mutations_spec.js +++ b/spec/frontend/diffs/store/mutations_spec.js @@ -892,4 +892,18 @@ describe('DiffsStoreMutations', () => { expect(state.showSuggestPopover).toBe(false); }); }); + + describe('SET_FILE_BY_FILE', () => { + it.each` + value | opposite + ${true} | ${false} + ${false} | ${true} + `('sets viewDiffsFileByFile to $value', ({ value, opposite }) => { + const state = { viewDiffsFileByFile: opposite }; + + mutations[types.SET_FILE_BY_FILE](state, value); + + expect(state.viewDiffsFileByFile).toBe(value); + }); + }); }); diff --git a/spec/frontend/diffs/utils/preferences_spec.js b/spec/frontend/diffs/utils/preferences_spec.js new file mode 100644 index 00000000000..a48db1d7512 --- /dev/null +++ b/spec/frontend/diffs/utils/preferences_spec.js @@ -0,0 +1,40 @@ +import Cookies from 'js-cookie'; +import { getParameterValues } from '~/lib/utils/url_utility'; + +import { fileByFile } from '~/diffs/utils/preferences'; +import { + DIFF_FILE_BY_FILE_COOKIE_NAME, + DIFF_VIEW_FILE_BY_FILE, + DIFF_VIEW_ALL_FILES, +} from '~/diffs/constants'; + +jest.mock('~/lib/utils/url_utility'); + +describe('diffs preferences', () => { + describe('fileByFile', () => { + it.each` + result | preference | cookie | searchParam + ${false} | ${false} | ${undefined} | ${undefined} + ${true} | ${true} | ${undefined} | ${undefined} + ${true} | ${false} | ${DIFF_VIEW_FILE_BY_FILE} | ${undefined} + ${false} | ${true} | ${DIFF_VIEW_ALL_FILES} | ${undefined} + ${true} | ${false} | ${undefined} | ${[DIFF_VIEW_FILE_BY_FILE]} + ${false} | ${true} | ${undefined} | ${[DIFF_VIEW_ALL_FILES]} + ${true} | ${false} | ${DIFF_VIEW_FILE_BY_FILE} | ${[DIFF_VIEW_FILE_BY_FILE]} + ${true} | ${true} | ${DIFF_VIEW_ALL_FILES} | ${[DIFF_VIEW_FILE_BY_FILE]} + ${false} | ${false} | ${DIFF_VIEW_ALL_FILES} | ${[DIFF_VIEW_ALL_FILES]} + ${false} | ${true} | ${DIFF_VIEW_FILE_BY_FILE} | ${[DIFF_VIEW_ALL_FILES]} + `( + 'should return $result when { preference: $preference, cookie: $cookie, search: $searchParam }', + ({ result, preference, cookie, searchParam }) => { + if (cookie) { + Cookies.set(DIFF_FILE_BY_FILE_COOKIE_NAME, cookie); + } + + getParameterValues.mockReturnValue(searchParam); + + expect(fileByFile(preference)).toBe(result); + }, + ); + }); +}); diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js index 197f646a22e..b42339f626e 100644 --- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js +++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js @@ -5,7 +5,14 @@ import waitForPromises from 'helpers/wait_for_promises'; import httpStatusCodes from '~/lib/utils/http_status'; import axios from '~/lib/utils/axios_utils'; import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue'; -import { mockRefs, mockParams, mockPostParams, mockProjectId, mockError } from '../mock_data'; +import { + mockBranches, + mockTags, + mockParams, + mockPostParams, + mockProjectId, + mockError, +} from '../mock_data'; import { redirectTo } from '~/lib/utils/url_utility'; jest.mock('~/lib/utils/url_utility', () => ({ @@ -37,6 +44,10 @@ describe('Pipeline New Form', () => { const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]'); const findLoadingIcon = () => wrapper.find(GlLoadingIcon); const getExpectedPostParams = () => JSON.parse(mock.history.post[0].data); + const changeRef = i => + findDropdownItems() + .at(i) + .vm.$emit('click'); const createComponent = (term = '', props = {}, method = shallowMount) => { wrapper = method(PipelineNewForm, { @@ -44,7 +55,8 @@ describe('Pipeline New Form', () => { projectId: mockProjectId, pipelinesPath, configVariablesPath, - refs: mockRefs, + branches: mockBranches, + tags: mockTags, defaultBranch: 'master', settingsLink: '', maxWarnings: 25, @@ -76,8 +88,11 @@ describe('Pipeline New Form', () => { }); it('displays dropdown with all branches and tags', () => { + const refLength = mockBranches.length + mockTags.length; + createComponent(); - expect(findDropdownItems()).toHaveLength(mockRefs.length); + + expect(findDropdownItems()).toHaveLength(refLength); }); it('when user enters search term the list is filtered', () => { @@ -130,15 +145,6 @@ describe('Pipeline New Form', () => { expect(findVariableRows()).toHaveLength(2); }); - it('creates a pipeline on submit', async () => { - findForm().vm.$emit('submit', dummySubmitEvent); - - await waitForPromises(); - - expect(getExpectedPostParams()).toEqual(mockPostParams); - expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${postResponse.id}`); - }); - it('creates blank variable on input change event', async () => { const input = findKeyInputs().at(2); input.element.value = 'test_var_2'; @@ -150,45 +156,81 @@ describe('Pipeline New Form', () => { expect(findKeyInputs().at(3).element.value).toBe(''); expect(findValueInputs().at(3).element.value).toBe(''); }); + }); - describe('when the form has been modified', () => { - const selectRef = i => - findDropdownItems() - .at(i) - .vm.$emit('click'); + describe('Pipeline creation', () => { + beforeEach(async () => { + mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, postResponse); - beforeEach(async () => { - const input = findKeyInputs().at(0); - input.element.value = 'test_var_2'; - input.trigger('change'); + await waitForPromises(); + }); + it('creates pipeline with full ref and variables', async () => { + createComponent(); - findRemoveIcons() - .at(1) - .trigger('click'); + changeRef(0); - await wrapper.vm.$nextTick(); - }); + findForm().vm.$emit('submit', dummySubmitEvent); - it('form values are restored when the ref changes', async () => { - expect(findVariableRows()).toHaveLength(2); + await waitForPromises(); - selectRef(1); - await waitForPromises(); + expect(getExpectedPostParams().ref).toEqual(wrapper.vm.$data.refValue.fullName); + expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${postResponse.id}`); + }); + it('creates a pipeline with short ref and variables', async () => { + // query params are used + createComponent('', mockParams); - expect(findVariableRows()).toHaveLength(3); - expect(findKeyInputs().at(0).element.value).toBe('test_var'); - }); + await waitForPromises(); - it('form values are restored again when the ref is reverted', async () => { - selectRef(1); - await waitForPromises(); + findForm().vm.$emit('submit', dummySubmitEvent); - selectRef(2); - await waitForPromises(); + await waitForPromises(); - expect(findVariableRows()).toHaveLength(2); - expect(findKeyInputs().at(0).element.value).toBe('test_var_2'); - }); + expect(getExpectedPostParams()).toEqual(mockPostParams); + expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${postResponse.id}`); + }); + }); + + describe('When the ref has been changed', () => { + beforeEach(async () => { + createComponent('', {}, mount); + + await waitForPromises(); + }); + it('variables persist between ref changes', async () => { + changeRef(0); // change to master + + await waitForPromises(); + + const masterInput = findKeyInputs().at(0); + masterInput.element.value = 'build_var'; + masterInput.trigger('change'); + + await wrapper.vm.$nextTick(); + + changeRef(1); // change to branch-1 + + await waitForPromises(); + + const branchOneInput = findKeyInputs().at(0); + branchOneInput.element.value = 'deploy_var'; + branchOneInput.trigger('change'); + + await wrapper.vm.$nextTick(); + + changeRef(0); // change back to master + + await waitForPromises(); + + expect(findKeyInputs().at(0).element.value).toBe('build_var'); + expect(findVariableRows().length).toBe(2); + + changeRef(1); // change back to branch-1 + + await waitForPromises(); + + expect(findKeyInputs().at(0).element.value).toBe('deploy_var'); + expect(findVariableRows().length).toBe(2); }); }); @@ -321,6 +363,7 @@ describe('Pipeline New Form', () => { it('shows the correct warning title', () => { const { length } = mockError.warnings; + expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`); }); diff --git a/spec/frontend/pipeline_new/mock_data.js b/spec/frontend/pipeline_new/mock_data.js index cdbd6d4437e..feb24ec602d 100644 --- a/spec/frontend/pipeline_new/mock_data.js +++ b/spec/frontend/pipeline_new/mock_data.js @@ -1,4 +1,14 @@ -export const mockRefs = ['master', 'branch-1', 'tag-1']; +export const mockBranches = [ + { shortName: 'master', fullName: 'refs/heads/master' }, + { shortName: 'branch-1', fullName: 'refs/heads/branch-1' }, + { shortName: 'branch-2', fullName: 'refs/heads/branch-2' }, +]; + +export const mockTags = [ + { shortName: '1.0.0', fullName: 'refs/tags/1.0.0' }, + { shortName: '1.1.0', fullName: 'refs/tags/1.1.0' }, + { shortName: '1.2.0', fullName: 'refs/tags/1.2.0' }, +]; export const mockParams = { refParam: 'tag-1', @@ -31,3 +41,7 @@ export const mockError = { ], total_warnings: 7, }; + +export const mockBranchRefs = ['master', 'dev', 'release']; + +export const mockTagRefs = ['1.0.0', '1.1.0', '1.2.0']; diff --git a/spec/frontend/pipeline_new/utils/format_refs_spec.js b/spec/frontend/pipeline_new/utils/format_refs_spec.js new file mode 100644 index 00000000000..1fda6a8af83 --- /dev/null +++ b/spec/frontend/pipeline_new/utils/format_refs_spec.js @@ -0,0 +1,21 @@ +import formatRefs from '~/pipeline_new/utils/format_refs'; +import { BRANCH_REF_TYPE, TAG_REF_TYPE } from '~/pipeline_new/constants'; +import { mockBranchRefs, mockTagRefs } from '../mock_data'; + +describe('Format refs util', () => { + it('formats branch ref correctly', () => { + expect(formatRefs(mockBranchRefs, BRANCH_REF_TYPE)).toEqual([ + { fullName: 'refs/heads/master', shortName: 'master' }, + { fullName: 'refs/heads/dev', shortName: 'dev' }, + { fullName: 'refs/heads/release', shortName: 'release' }, + ]); + }); + + it('formats tag ref correctly', () => { + expect(formatRefs(mockTagRefs, TAG_REF_TYPE)).toEqual([ + { fullName: 'refs/tags/1.0.0', shortName: '1.0.0' }, + { fullName: 'refs/tags/1.1.0', shortName: '1.1.0' }, + { fullName: 'refs/tags/1.2.0', shortName: '1.2.0' }, + ]); + }); +}); diff --git a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap new file mode 100644 index 00000000000..86895341f2c --- /dev/null +++ b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Settings Form Cadence matches snapshot 1`] = ` +<expiration-dropdown-stub + class="gl-mr-7 gl-mb-0!" + data-testid="cadence-dropdown" + formoptions="[object Object],[object Object],[object Object],[object Object],[object Object]" + label="Run cleanup:" + name="cadence" + value="EVERY_DAY" +/> +`; + +exports[`Settings Form Enable matches snapshot 1`] = ` +<expiration-toggle-stub + class="gl-mb-0!" + data-testid="enable-toggle" + value="true" +/> +`; + +exports[`Settings Form Keep N matches snapshot 1`] = ` +<expiration-dropdown-stub + data-testid="keep-n-dropdown" + formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]" + label="Keep the most recent:" + name="keep-n" + value="TEN_TAGS" +/> +`; + +exports[`Settings Form Keep Regex matches snapshot 1`] = ` +<expiration-textarea-stub + data-testid="keep-regex-textarea" + description="Tags with names that match this regex pattern are kept. %{linkStart}More information%{linkEnd}" + error="" + label="Keep tags matching:" + name="keep-regex" + placeholder="" + value="sss" +/> +`; + +exports[`Settings Form OlderThan matches snapshot 1`] = ` +<expiration-dropdown-stub + data-testid="older-than-dropdown" + formoptions="[object Object],[object Object],[object Object],[object Object]" + label="Remove tags older than:" + name="older-than" + value="FOURTEEN_DAYS" +/> +`; + +exports[`Settings Form Remove regex matches snapshot 1`] = ` +<expiration-textarea-stub + data-testid="remove-regex-textarea" + description="Tags with names that match this regex pattern are removed. %{linkStart}More information%{linkEnd}" + error="" + label="Remove tags matching:" + name="remove-regex" + placeholder=".*" + value="asdasdssssdfdf" +/> +`; diff --git a/spec/frontend/registry/settings/components/registry_settings_app_spec.js b/spec/frontend/registry/settings/components/registry_settings_app_spec.js index a784396f47a..6be24b81ac1 100644 --- a/spec/frontend/registry/settings/components/registry_settings_app_spec.js +++ b/spec/frontend/registry/settings/components/registry_settings_app_spec.js @@ -11,7 +11,11 @@ import { UNAVAILABLE_USER_FEATURE_TEXT, } from '~/registry/settings/constants'; -import { expirationPolicyPayload, emptyExpirationPolicyPayload } from '../mock_data'; +import { + expirationPolicyPayload, + emptyExpirationPolicyPayload, + containerExpirationPolicyData, +} from '../mock_data'; const localVue = createLocalVue(); @@ -62,6 +66,29 @@ describe('Registry Settings App', () => { wrapper.destroy(); }); + describe('isEdited status', () => { + it.each` + description | apiResponse | workingCopy | result + ${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false} + ${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true} + ${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false} + ${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true} + ${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true} + `('$description', async ({ apiResponse, workingCopy, result }) => { + const requests = mountComponentWithApollo({ + provide: { ...defaultProvidedValues, enableHistoricEntries: true }, + resolver: jest.fn().mockResolvedValue(apiResponse), + }); + await Promise.all(requests); + + findSettingsComponent().vm.$emit('input', workingCopy); + + await wrapper.vm.$nextTick(); + + expect(findSettingsComponent().props('isEdited')).toBe(result); + }); + }); + it('renders the setting form', async () => { const requests = mountComponentWithApollo({ resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()), diff --git a/spec/frontend/registry/settings/components/settings_form_spec.js b/spec/frontend/registry/settings/components/settings_form_spec.js index 4346cfadcc8..3744faa0d80 100644 --- a/spec/frontend/registry/settings/components/settings_form_spec.js +++ b/spec/frontend/registry/settings/components/settings_form_spec.js @@ -4,7 +4,6 @@ import createMockApollo from 'jest/helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import Tracking from '~/tracking'; import component from '~/registry/settings/components/settings_form.vue'; -import expirationPolicyFields from '~/registry/shared/components/expiration_policy_fields.vue'; import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.graphql'; import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.graphql'; import { @@ -39,9 +38,15 @@ describe('Settings Form', () => { }; const findForm = () => wrapper.find({ ref: 'form-element' }); - const findFields = () => wrapper.find(expirationPolicyFields); - const findCancelButton = () => wrapper.find({ ref: 'cancel-button' }); - const findSaveButton = () => wrapper.find({ ref: 'save-button' }); + + const findCancelButton = () => wrapper.find('[data-testid="cancel-button"'); + const findSaveButton = () => wrapper.find('[data-testid="save-button"'); + const findEnableToggle = () => wrapper.find('[data-testid="enable-toggle"]'); + const findCadenceDropdown = () => wrapper.find('[data-testid="cadence-dropdown"]'); + const findKeepNDropdown = () => wrapper.find('[data-testid="keep-n-dropdown"]'); + const findKeepRegexTextarea = () => wrapper.find('[data-testid="keep-regex-textarea"]'); + const findOlderThanDropdown = () => wrapper.find('[data-testid="older-than-dropdown"]'); + const findRemoveRegexTextarea = () => wrapper.find('[data-testid="remove-regex-textarea"]'); const mountComponent = ({ props = defaultProps, @@ -109,45 +114,136 @@ describe('Settings Form', () => { wrapper.destroy(); }); - describe('data binding', () => { - it('v-model change update the settings property', () => { + describe.each` + model | finder | fieldName | type | defaultValue + ${'enabled'} | ${findEnableToggle} | ${'Enable'} | ${'toggle'} | ${false} + ${'cadence'} | ${findCadenceDropdown} | ${'Cadence'} | ${'dropdown'} | ${'EVERY_DAY'} + ${'keepN'} | ${findKeepNDropdown} | ${'Keep N'} | ${'dropdown'} | ${'TEN_TAGS'} + ${'nameRegexKeep'} | ${findKeepRegexTextarea} | ${'Keep Regex'} | ${'textarea'} | ${''} + ${'olderThan'} | ${findOlderThanDropdown} | ${'OlderThan'} | ${'dropdown'} | ${'NINETY_DAYS'} + ${'nameRegex'} | ${findRemoveRegexTextarea} | ${'Remove regex'} | ${'textarea'} | ${''} + `('$fieldName', ({ model, finder, type, defaultValue }) => { + it('matches snapshot', () => { mountComponent(); - findFields().vm.$emit('input', { newValue: 'foo' }); - expect(wrapper.emitted('input')).toEqual([['foo']]); + + expect(finder().element).toMatchSnapshot(); }); - it('v-model change update the api error property', () => { - const apiErrors = { baz: 'bar' }; - mountComponent({ data: { apiErrors } }); - expect(findFields().props('apiErrors')).toEqual(apiErrors); - findFields().vm.$emit('input', { newValue: 'foo', modified: 'baz' }); - expect(findFields().props('apiErrors')).toEqual({}); + it('input event triggers a model update', () => { + mountComponent(); + + finder().vm.$emit('input', 'foo'); + expect(wrapper.emitted('input')[0][0]).toMatchObject({ + [model]: 'foo', + }); }); it('shows the default option when none are selected', () => { mountComponent({ props: { value: {} } }); - expect(findFields().props('value')).toEqual({ - cadence: 'EVERY_DAY', - keepN: 'TEN_TAGS', - olderThan: 'NINETY_DAYS', - }); + expect(finder().props('value')).toEqual(defaultValue); }); + + if (type !== 'toggle') { + it.each` + isLoading | mutationLoading | enabledValue + ${false} | ${false} | ${false} + ${true} | ${false} | ${false} + ${true} | ${true} | ${true} + ${false} | ${true} | ${true} + ${false} | ${false} | ${false} + `( + 'is disabled when is loading is $isLoading, mutationLoading is $mutationLoading and enabled is $enabledValue', + ({ isLoading, mutationLoading, enabledValue }) => { + mountComponent({ + props: { isLoading, value: { enabled: enabledValue } }, + data: { mutationLoading }, + }); + expect(finder().props('disabled')).toEqual(true); + }, + ); + } else { + it.each` + isLoading | mutationLoading + ${true} | ${false} + ${true} | ${true} + ${false} | ${true} + `( + 'is disabled when is loading is $isLoading and mutationLoading is $mutationLoading', + ({ isLoading, mutationLoading }) => { + mountComponent({ + props: { isLoading, value: {} }, + data: { mutationLoading }, + }); + expect(finder().props('disabled')).toEqual(true); + }, + ); + } + + if (type === 'textarea') { + it('input event updates the api error property', async () => { + const apiErrors = { [model]: 'bar' }; + mountComponent({ data: { apiErrors } }); + + finder().vm.$emit('input', 'foo'); + expect(finder().props('error')).toEqual('bar'); + + await wrapper.vm.$nextTick(); + + expect(finder().props('error')).toEqual(''); + }); + + it('validation event updates buttons disabled state', async () => { + mountComponent(); + + expect(findSaveButton().props('disabled')).toBe(false); + + finder().vm.$emit('validation', false); + + await wrapper.vm.$nextTick(); + + expect(findSaveButton().props('disabled')).toBe(true); + }); + } + + if (type === 'dropdown') { + it('has the correct formOptions', () => { + mountComponent(); + expect(finder().props('formOptions')).toEqual(wrapper.vm.$options.formOptions[model]); + }); + } }); describe('form', () => { describe('form reset event', () => { - beforeEach(() => { + it('calls the appropriate function', () => { mountComponent(); findForm().trigger('reset'); - }); - it('calls the appropriate function', () => { + expect(wrapper.emitted('reset')).toEqual([[]]); }); it('tracks the reset event', () => { + mountComponent(); + + findForm().trigger('reset'); + expect(Tracking.event).toHaveBeenCalledWith(undefined, 'reset_form', trackingPayload); }); + + it('resets the errors objects', async () => { + mountComponent({ + data: { apiErrors: { nameRegex: 'bar' }, localErrors: { nameRegexKeep: false } }, + }); + + findForm().trigger('reset'); + + await wrapper.vm.$nextTick(); + + expect(findKeepRegexTextarea().props('error')).toBe(''); + expect(findRemoveRegexTextarea().props('error')).toBe(''); + expect(findSaveButton().props('disabled')).toBe(false); + }); }); describe('form submit event ', () => { @@ -209,6 +305,7 @@ describe('Settings Form', () => { }); }); }); + describe('global errors', () => { it('shows an error', async () => { const handlers = mountComponentWithApollo({ @@ -230,7 +327,7 @@ describe('Settings Form', () => { graphQLErrors: [ { extensions: { - problems: [{ path: ['name'], message: 'baz' }], + problems: [{ path: ['nameRegexKeep'], message: 'baz' }], }, }, ], @@ -241,7 +338,7 @@ describe('Settings Form', () => { await waitForPromises(); await wrapper.vm.$nextTick(); - expect(findFields().props('apiErrors')).toEqual({ name: 'baz' }); + expect(findKeepRegexTextarea().props('error')).toEqual('baz'); }); }); }); @@ -257,23 +354,21 @@ describe('Settings Form', () => { }); it.each` - isLoading | isEdited | mutationLoading | isDisabled - ${true} | ${true} | ${true} | ${true} - ${false} | ${true} | ${true} | ${true} - ${false} | ${false} | ${true} | ${true} - ${true} | ${false} | ${false} | ${true} - ${false} | ${false} | ${false} | ${true} - ${false} | ${true} | ${false} | ${false} + isLoading | isEdited | mutationLoading + ${true} | ${true} | ${true} + ${false} | ${true} | ${true} + ${false} | ${false} | ${true} + ${true} | ${false} | ${false} + ${false} | ${false} | ${false} `( - 'when isLoading is $isLoading and isEdited is $isEdited and mutationLoading is $mutationLoading is $isDisabled that the is disabled', - ({ isEdited, isLoading, mutationLoading, isDisabled }) => { + 'when isLoading is $isLoading, isEdited is $isEdited and mutationLoading is $mutationLoading is disabled', + ({ isEdited, isLoading, mutationLoading }) => { mountComponent({ props: { ...defaultProps, isEdited, isLoading }, data: { mutationLoading }, }); - const expectation = isDisabled ? 'true' : undefined; - expect(findCancelButton().attributes('disabled')).toBe(expectation); + expect(findCancelButton().props('disabled')).toBe(true); }, ); }); @@ -284,24 +379,24 @@ describe('Settings Form', () => { expect(findSaveButton().attributes('type')).toBe('submit'); }); + it.each` - isLoading | fieldsAreValid | mutationLoading | isDisabled - ${true} | ${true} | ${true} | ${true} - ${false} | ${true} | ${true} | ${true} - ${false} | ${false} | ${true} | ${true} - ${true} | ${false} | ${false} | ${true} - ${false} | ${false} | ${false} | ${true} - ${false} | ${true} | ${false} | ${false} + isLoading | localErrors | mutationLoading + ${true} | ${{}} | ${true} + ${true} | ${{}} | ${false} + ${false} | ${{}} | ${true} + ${false} | ${{ foo: false }} | ${true} + ${true} | ${{ foo: false }} | ${false} + ${false} | ${{ foo: false }} | ${false} `( - 'when isLoading is $isLoading and fieldsAreValid is $fieldsAreValid and mutationLoading is $mutationLoading is $isDisabled that the is disabled', - ({ fieldsAreValid, isLoading, mutationLoading, isDisabled }) => { + 'when isLoading is $isLoading, localErrors is $localErrors and mutationLoading is $mutationLoading is disabled', + ({ localErrors, isLoading, mutationLoading }) => { mountComponent({ props: { ...defaultProps, isLoading }, - data: { mutationLoading, fieldsAreValid }, + data: { mutationLoading, localErrors }, }); - const expectation = isDisabled ? 'true' : undefined; - expect(findSaveButton().attributes('disabled')).toBe(expectation); + expect(findSaveButton().props('disabled')).toBe(true); }, ); diff --git a/spec/frontend/registry/shared/__snapshots__/utils_spec.js.snap b/spec/frontend/registry/shared/__snapshots__/utils_spec.js.snap index 032007bba51..7062773b46b 100644 --- a/spec/frontend/registry/shared/__snapshots__/utils_spec.js.snap +++ b/spec/frontend/registry/shared/__snapshots__/utils_spec.js.snap @@ -76,25 +76,25 @@ Array [ Object { "default": false, "key": "SEVEN_DAYS", - "label": "7 days until tags are automatically removed", + "label": "7 days", "variable": 7, }, Object { "default": false, "key": "FOURTEEN_DAYS", - "label": "14 days until tags are automatically removed", + "label": "14 days", "variable": 14, }, Object { "default": false, "key": "THIRTY_DAYS", - "label": "30 days until tags are automatically removed", + "label": "30 days", "variable": 30, }, Object { "default": true, "key": "NINETY_DAYS", - "label": "90 days until tags are automatically removed", + "label": "90 days", "variable": 90, }, ] diff --git a/spec/frontend/registry/shared/utils_spec.js b/spec/frontend/registry/shared/utils_spec.js index edb0c3261be..1ba832f67ea 100644 --- a/spec/frontend/registry/shared/utils_spec.js +++ b/spec/frontend/registry/shared/utils_spec.js @@ -11,10 +11,7 @@ describe('Utils', () => { [{ variable: 1 }, { variable: 2 }], olderThanTranslationGenerator, ); - expect(result).toEqual([ - { variable: 1, label: '1 day until tags are automatically removed' }, - { variable: 2, label: '2 days until tags are automatically removed' }, - ]); + expect(result).toEqual([{ variable: 1, label: '1 day' }, { variable: 2, label: '2 days' }]); }); }); diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 412f45739b1..9117fc9006d 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -86,6 +86,7 @@ label: - issues - merge_requests - priorities +- epic_board_labels milestone: - group - project diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb index 62380299ea0..41ce480b02f 100644 --- a/spec/models/custom_emoji_spec.rb +++ b/spec/models/custom_emoji_spec.rb @@ -22,6 +22,15 @@ RSpec.describe CustomEmoji do expect(new_emoji.errors.messages).to eq(name: ["#{emoji_name} is already being used for another emoji"]) end + it 'disallows very long invalid emoji name without regular expression backtracking issues' do + new_emoji = build(:custom_emoji, name: 'a' * 10000 + '!', group: group) + + Timeout.timeout(1) do + expect(new_emoji).not_to be_valid + expect(new_emoji.errors.messages).to eq(name: ["is too long (maximum is 36 characters)", "is invalid"]) + end + end + it 'disallows duplicate custom emoji names within namespace' do old_emoji = create(:custom_emoji, group: group) new_emoji = build(:custom_emoji, name: old_emoji.name, namespace: old_emoji.namespace, group: group) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 0ca35476233..3bcb21bc828 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -5071,11 +5071,11 @@ RSpec.describe Project, factory_default: :keep do end end - describe "#default_branch" do - context "with an empty repository" do + describe '#default_branch' do + context 'with an empty repository' do let_it_be(:project) { create(:project_empty_repo) } - context "group.default_branch_name is available" do + context 'group.default_branch_name is available' do let(:project_group) { create(:group) } let(:project) { create(:project, path: 'avatar', namespace: project_group) } @@ -5088,19 +5088,19 @@ RSpec.describe Project, factory_default: :keep do .and_return('example_branch') end - it "returns the group default value" do - expect(project.default_branch).to eq("example_branch") + it 'returns the group default value' do + expect(project.default_branch).to eq('example_branch') end end - context "Gitlab::CurrentSettings.default_branch_name is available" do + context 'Gitlab::CurrentSettings.default_branch_name is available' do before do expect(Gitlab::CurrentSettings) .to receive(:default_branch_name) .and_return(example_branch_name) end - context "is missing or nil" do + context 'is missing or nil' do let(:example_branch_name) { nil } it "returns nil" do @@ -5108,10 +5108,18 @@ RSpec.describe Project, factory_default: :keep do end end - context "is present" do - let(:example_branch_name) { "example_branch_name" } + context 'is blank' do + let(:example_branch_name) { '' } - it "returns the expected branch name" do + it 'returns nil' do + expect(project.default_branch).to be_nil + end + end + + context 'is present' do + let(:example_branch_name) { 'example_branch_name' } + + it 'returns the expected branch name' do expect(project.default_branch).to eq(example_branch_name) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e8405934f62..55ee846090f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,6 +8,10 @@ if $".include?(File.expand_path('fast_spec_helper.rb', __dir__)) abort 'Aborting...' end +# Enable deprecation warnings by default and make them more visible +# to developers to ease upgrading to newer Ruby versions. +Warning[:deprecated] = true unless ENV.key?('SILENCE_DEPRECATIONS') + require './spec/deprecation_toolkit_env' require './spec/simplecov_env' |