summaryrefslogtreecommitdiff
path: root/spec/frontend/packages_and_registries
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/packages_and_registries')
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/__snapshots__/settings_titles_spec.js.snap18
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js146
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/generic_settings_spec.js54
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js84
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js150
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/settings_titles_spec.js25
-rw-r--r--spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/group/mock_data.js14
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap101
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap64
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js86
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js169
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js65
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js77
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js164
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js460
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/graphql/cache_updated_spec.js56
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/mock_data.js40
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/utils_spec.js37
19 files changed, 1663 insertions, 149 deletions
diff --git a/spec/frontend/packages_and_registries/settings/group/components/__snapshots__/settings_titles_spec.js.snap b/spec/frontend/packages_and_registries/settings/group/components/__snapshots__/settings_titles_spec.js.snap
new file mode 100644
index 00000000000..f2087733d2b
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/__snapshots__/settings_titles_spec.js.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`settings_titles renders properly 1`] = `
+<div>
+ <h5
+ class="gl-border-b-solid gl-border-b-1 gl-border-gray-200"
+ >
+
+ foo
+
+ </h5>
+
+ <p>
+ bar
+ </p>
+
+</div>
+`;
diff --git a/spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js
new file mode 100644
index 00000000000..0bbb1ce3436
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js
@@ -0,0 +1,146 @@
+import { GlSprintf, GlToggle, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import component from '~/packages_and_registries/settings/group/components/duplicates_settings.vue';
+
+import {
+ DUPLICATES_TOGGLE_LABEL,
+ DUPLICATES_ALLOWED_ENABLED,
+ DUPLICATES_ALLOWED_DISABLED,
+ DUPLICATES_SETTING_EXCEPTION_TITLE,
+ DUPLICATES_SETTINGS_EXCEPTION_LEGEND,
+} from '~/packages_and_registries/settings/group/constants';
+
+describe('Duplicates Settings', () => {
+ let wrapper;
+
+ const defaultProps = {
+ duplicatesAllowed: false,
+ duplicateExceptionRegex: 'foo',
+ modelNames: {
+ allowed: 'allowedModel',
+ exception: 'exceptionModel',
+ },
+ };
+
+ const mountComponent = (propsData = defaultProps) => {
+ wrapper = shallowMount(component, {
+ propsData,
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findToggle = () => wrapper.findComponent(GlToggle);
+ const findToggleLabel = () => wrapper.find('[data-testid="toggle-label"');
+
+ const findInputGroup = () => wrapper.findComponent(GlFormGroup);
+ const findInput = () => wrapper.findComponent(GlFormInput);
+
+ it('has a toggle', () => {
+ mountComponent();
+
+ expect(findToggle().exists()).toBe(true);
+ expect(findToggle().props()).toMatchObject({
+ label: DUPLICATES_TOGGLE_LABEL,
+ value: defaultProps.duplicatesAllowed,
+ });
+ });
+
+ it('toggle emits an update event', () => {
+ mountComponent();
+
+ findToggle().vm.$emit('change', false);
+
+ expect(wrapper.emitted('update')).toStrictEqual([
+ [{ [defaultProps.modelNames.allowed]: false }],
+ ]);
+ });
+
+ describe('when the duplicates are disabled', () => {
+ it('the toggle has the disabled message', () => {
+ mountComponent();
+
+ expect(findToggleLabel().exists()).toBe(true);
+ expect(findToggleLabel().text()).toMatchInterpolatedText(DUPLICATES_ALLOWED_DISABLED);
+ });
+
+ it('shows a form group with an input field', () => {
+ mountComponent();
+
+ expect(findInputGroup().exists()).toBe(true);
+
+ expect(findInputGroup().attributes()).toMatchObject({
+ 'label-for': 'maven-duplicated-settings-regex-input',
+ label: DUPLICATES_SETTING_EXCEPTION_TITLE,
+ description: DUPLICATES_SETTINGS_EXCEPTION_LEGEND,
+ });
+ });
+
+ it('shows an input field', () => {
+ mountComponent();
+
+ expect(findInput().exists()).toBe(true);
+
+ expect(findInput().attributes()).toMatchObject({
+ id: 'maven-duplicated-settings-regex-input',
+ value: defaultProps.duplicateExceptionRegex,
+ });
+ });
+
+ it('input change event emits an update event', () => {
+ mountComponent();
+
+ findInput().vm.$emit('change', 'bar');
+
+ expect(wrapper.emitted('update')).toStrictEqual([
+ [{ [defaultProps.modelNames.exception]: 'bar' }],
+ ]);
+ });
+
+ describe('valid state', () => {
+ it('form group has correct props', () => {
+ mountComponent();
+
+ expect(findInputGroup().attributes()).toMatchObject({
+ state: 'true',
+ 'invalid-feedback': '',
+ });
+ });
+ });
+
+ describe('invalid state', () => {
+ it('form group has correct props', () => {
+ const propsWithError = {
+ ...defaultProps,
+ duplicateExceptionRegexError: 'some error string',
+ };
+
+ mountComponent(propsWithError);
+
+ expect(findInputGroup().attributes()).toMatchObject({
+ 'invalid-feedback': propsWithError.duplicateExceptionRegexError,
+ });
+ });
+ });
+ });
+
+ describe('when the duplicates are enabled', () => {
+ it('has the correct toggle label', () => {
+ mountComponent({ ...defaultProps, duplicatesAllowed: true });
+
+ expect(findToggleLabel().exists()).toBe(true);
+ expect(findToggleLabel().text()).toMatchInterpolatedText(DUPLICATES_ALLOWED_ENABLED);
+ });
+
+ it('hides the form input group', () => {
+ mountComponent({ ...defaultProps, duplicatesAllowed: true });
+
+ expect(findInputGroup().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/generic_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/generic_settings_spec.js
new file mode 100644
index 00000000000..4eafeedd55e
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/generic_settings_spec.js
@@ -0,0 +1,54 @@
+import { shallowMount } from '@vue/test-utils';
+import GenericSettings from '~/packages_and_registries/settings/group/components/generic_settings.vue';
+import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue';
+
+describe('generic_settings', () => {
+ let wrapper;
+
+ const mountComponent = () => {
+ wrapper = shallowMount(GenericSettings, {
+ scopedSlots: {
+ default: '<div data-testid="default-slot">{{props.modelNames}}</div>',
+ },
+ });
+ };
+
+ const findSettingsTitle = () => wrapper.findComponent(SettingsTitles);
+ const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('title component', () => {
+ it('has a title component', () => {
+ mountComponent();
+
+ expect(findSettingsTitle().exists()).toBe(true);
+ });
+
+ it('passes the correct props', () => {
+ mountComponent();
+
+ expect(findSettingsTitle().props()).toMatchObject({
+ title: 'Generic',
+ subTitle: 'Settings for Generic packages',
+ });
+ });
+ });
+
+ describe('default slot', () => {
+ it('accept a default slots', () => {
+ mountComponent();
+
+ expect(findDefaultSlot().exists()).toBe(true);
+ });
+
+ it('binds model names', () => {
+ mountComponent();
+
+ expect(findDefaultSlot().text()).toContain('genericDuplicatesAllowed');
+ expect(findDefaultSlot().text()).toContain('genericDuplicateExceptionRegex');
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
index be0d7114e6e..14ee3f3e3b8 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
@@ -3,6 +3,8 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import DuplicatesSettings from '~/packages_and_registries/settings/group/components/duplicates_settings.vue';
+import GenericSettings from '~/packages_and_registries/settings/group/components/generic_settings.vue';
import component from '~/packages_and_registries/settings/group/components/group_settings_app.vue';
import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue';
import {
@@ -63,6 +65,8 @@ describe('Group Settings App', () => {
stubs: {
GlSprintf,
SettingsBlock,
+ MavenSettings,
+ GenericSettings,
},
mocks: {
$toast: {
@@ -78,14 +82,17 @@ describe('Group Settings App', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
- const findSettingsBlock = () => wrapper.find(SettingsBlock);
+ const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
const findDescription = () => wrapper.find('[data-testid="description"');
- const findLink = () => wrapper.find(GlLink);
- const findMavenSettings = () => wrapper.find(MavenSettings);
- const findAlert = () => wrapper.find(GlAlert);
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findMavenSettings = () => wrapper.findComponent(MavenSettings);
+ const findMavenDuplicatedSettings = () => findMavenSettings().findComponent(DuplicatesSettings);
+ const findGenericSettings = () => wrapper.findComponent(GenericSettings);
+ const findGenericDuplicatedSettings = () =>
+ findGenericSettings().findComponent(DuplicatesSettings);
const waitForApolloQueryAndRender = async () => {
await waitForPromises();
@@ -93,7 +100,7 @@ describe('Group Settings App', () => {
};
const emitSettingsUpdate = (override) => {
- findMavenSettings().vm.$emit('update', {
+ findMavenDuplicatedSettings().vm.$emit('update', {
mavenDuplicateExceptionRegex: ')',
...override,
});
@@ -152,7 +159,7 @@ describe('Group Settings App', () => {
it('assigns duplication allowness and exception props', async () => {
mountComponent();
- expect(findMavenSettings().props('loading')).toBe(true);
+ expect(findMavenDuplicatedSettings().props('loading')).toBe(true);
await waitForApolloQueryAndRender();
@@ -161,10 +168,10 @@ describe('Group Settings App', () => {
mavenDuplicateExceptionRegex,
} = groupPackageSettingsMock.data.group.packageSettings;
- expect(findMavenSettings().props()).toMatchObject({
- mavenDuplicatesAllowed,
- mavenDuplicateExceptionRegex,
- mavenDuplicateExceptionRegexError: '',
+ expect(findMavenDuplicatedSettings().props()).toMatchObject({
+ duplicatesAllowed: mavenDuplicatesAllowed,
+ duplicateExceptionRegex: mavenDuplicateExceptionRegex,
+ duplicateExceptionRegexError: '',
loading: false,
});
});
@@ -183,6 +190,49 @@ describe('Group Settings App', () => {
});
});
+ describe('generic settings', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findGenericSettings().exists()).toBe(true);
+ });
+
+ it('assigns duplication allowness and exception props', async () => {
+ mountComponent();
+
+ expect(findGenericDuplicatedSettings().props('loading')).toBe(true);
+
+ await waitForApolloQueryAndRender();
+
+ const {
+ genericDuplicatesAllowed,
+ genericDuplicateExceptionRegex,
+ } = groupPackageSettingsMock.data.group.packageSettings;
+
+ expect(findGenericDuplicatedSettings().props()).toMatchObject({
+ duplicatesAllowed: genericDuplicatesAllowed,
+ duplicateExceptionRegex: genericDuplicateExceptionRegex,
+ duplicateExceptionRegexError: '',
+ loading: false,
+ });
+ });
+
+ it('on update event calls the mutation', async () => {
+ const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
+ mountComponent({ mutationResolver });
+
+ await waitForApolloQueryAndRender();
+
+ findMavenDuplicatedSettings().vm.$emit('update', {
+ genericDuplicateExceptionRegex: ')',
+ });
+
+ expect(mutationResolver).toHaveBeenCalledWith({
+ input: { genericDuplicateExceptionRegex: ')', namespacePath: 'foo_group_path' },
+ });
+ });
+ });
+
describe('settings update', () => {
describe('success state', () => {
it('shows a success alert', async () => {
@@ -200,26 +250,26 @@ describe('Group Settings App', () => {
});
it('has an optimistic response', async () => {
- const mavenDuplicateExceptionRegex = 'latest[master]something';
+ const mavenDuplicateExceptionRegex = 'latest[main]something';
mountComponent();
await waitForApolloQueryAndRender();
- expect(findMavenSettings().props('mavenDuplicateExceptionRegex')).toBe('');
+ expect(findMavenDuplicatedSettings().props('duplicateExceptionRegex')).toBe('');
emitSettingsUpdate({ mavenDuplicateExceptionRegex });
// wait for apollo to update the model with the optimistic response
await wrapper.vm.$nextTick();
- expect(findMavenSettings().props('mavenDuplicateExceptionRegex')).toBe(
+ expect(findMavenDuplicatedSettings().props('duplicateExceptionRegex')).toBe(
mavenDuplicateExceptionRegex,
);
// wait for the call to resolve
await waitForPromises();
- expect(findMavenSettings().props('mavenDuplicateExceptionRegex')).toBe(
+ expect(findMavenDuplicatedSettings().props('duplicateExceptionRegex')).toBe(
mavenDuplicateExceptionRegex,
);
});
@@ -245,7 +295,7 @@ describe('Group Settings App', () => {
await waitForApolloQueryAndRender();
// errors are bound to the component
- expect(findMavenSettings().props('mavenDuplicateExceptionRegexError')).toBe(
+ expect(findMavenDuplicatedSettings().props('duplicateExceptionRegexError')).toBe(
groupPackageSettingsMutationErrorMock.errors[0].extensions.problems[0].message,
);
@@ -258,7 +308,7 @@ describe('Group Settings App', () => {
await wrapper.vm.$nextTick();
// errors are reset on mutation call
- expect(findMavenSettings().props('mavenDuplicateExceptionRegexError')).toBe('');
+ expect(findMavenDuplicatedSettings().props('duplicateExceptionRegexError')).toBe('');
});
it.each`
diff --git a/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js
index 859d3587223..22644b97b43 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js
@@ -1,156 +1,54 @@
-import { GlSprintf, GlToggle, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import component from '~/packages_and_registries/settings/group/components/maven_settings.vue';
+import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue';
+import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue';
-import {
- MAVEN_TITLE,
- MAVEN_SETTINGS_SUBTITLE,
- MAVEN_DUPLICATES_ALLOWED_DISABLED,
- MAVEN_DUPLICATES_ALLOWED_ENABLED,
- MAVEN_SETTING_EXCEPTION_TITLE,
- MAVEN_SETTINGS_EXCEPTION_LEGEND,
-} from '~/packages_and_registries/settings/group/constants';
-
-describe('Maven Settings', () => {
+describe('maven_settings', () => {
let wrapper;
- const defaultProps = {
- mavenDuplicatesAllowed: false,
- mavenDuplicateExceptionRegex: 'foo',
- };
-
- const mountComponent = (propsData = defaultProps) => {
- wrapper = shallowMount(component, {
- propsData,
- stubs: {
- GlSprintf,
+ const mountComponent = () => {
+ wrapper = shallowMount(MavenSettings, {
+ scopedSlots: {
+ default: '<div data-testid="default-slot">{{props.modelNames}}</div>',
},
});
};
+ const findSettingsTitle = () => wrapper.findComponent(SettingsTitles);
+ const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
+
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
- const findTitle = () => wrapper.find('h5');
- const findSubTitle = () => wrapper.find('p');
- const findToggle = () => wrapper.find(GlToggle);
- const findToggleLabel = () => wrapper.find('[data-testid="toggle-label"');
-
- const findInputGroup = () => wrapper.find(GlFormGroup);
- const findInput = () => wrapper.find(GlFormInput);
-
- it('has a title', () => {
- mountComponent();
-
- expect(findTitle().exists()).toBe(true);
- expect(findTitle().text()).toBe(MAVEN_TITLE);
- });
-
- it('has a subtitle', () => {
- mountComponent();
-
- expect(findSubTitle().exists()).toBe(true);
- expect(findSubTitle().text()).toBe(MAVEN_SETTINGS_SUBTITLE);
- });
-
- it('has a toggle', () => {
- mountComponent();
-
- expect(findToggle().exists()).toBe(true);
- expect(findToggle().props()).toMatchObject({
- label: component.i18n.MAVEN_TOGGLE_LABEL,
- value: defaultProps.mavenDuplicatesAllowed,
- });
- });
-
- it('toggle emits an update event', () => {
- mountComponent();
-
- findToggle().vm.$emit('change', false);
-
- expect(wrapper.emitted('update')).toEqual([[{ mavenDuplicatesAllowed: false }]]);
- });
-
- describe('when the duplicates are disabled', () => {
- it('the toggle has the disabled message', () => {
+ describe('title component', () => {
+ it('has a title component', () => {
mountComponent();
- expect(findToggleLabel().exists()).toBe(true);
- expect(findToggleLabel().text()).toMatchInterpolatedText(MAVEN_DUPLICATES_ALLOWED_DISABLED);
+ expect(findSettingsTitle().exists()).toBe(true);
});
- it('shows a form group with an input field', () => {
+ it('passes the correct props', () => {
mountComponent();
- expect(findInputGroup().exists()).toBe(true);
-
- expect(findInputGroup().attributes()).toMatchObject({
- 'label-for': 'maven-duplicated-settings-regex-input',
- label: MAVEN_SETTING_EXCEPTION_TITLE,
- description: MAVEN_SETTINGS_EXCEPTION_LEGEND,
+ expect(findSettingsTitle().props()).toMatchObject({
+ title: 'Maven',
+ subTitle: 'Settings for Maven packages',
});
});
+ });
- it('shows an input field', () => {
+ describe('default slot', () => {
+ it('accept a default slots', () => {
mountComponent();
- expect(findInput().exists()).toBe(true);
-
- expect(findInput().attributes()).toMatchObject({
- id: 'maven-duplicated-settings-regex-input',
- value: defaultProps.mavenDuplicateExceptionRegex,
- });
+ expect(findDefaultSlot().exists()).toBe(true);
});
- it('input change event emits an update event', () => {
+ it('binds model names', () => {
mountComponent();
- findInput().vm.$emit('change', 'bar');
-
- expect(wrapper.emitted('update')).toEqual([[{ mavenDuplicateExceptionRegex: 'bar' }]]);
- });
-
- describe('valid state', () => {
- it('form group has correct props', () => {
- mountComponent();
-
- expect(findInputGroup().attributes()).toMatchObject({
- state: 'true',
- 'invalid-feedback': '',
- });
- });
- });
-
- describe('invalid state', () => {
- it('form group has correct props', () => {
- const propsWithError = {
- ...defaultProps,
- mavenDuplicateExceptionRegexError: 'some error string',
- };
-
- mountComponent(propsWithError);
-
- expect(findInputGroup().attributes()).toMatchObject({
- 'invalid-feedback': propsWithError.mavenDuplicateExceptionRegexError,
- });
- });
- });
- });
-
- describe('when the duplicates are enabled', () => {
- it('has the correct toggle label', () => {
- mountComponent({ ...defaultProps, mavenDuplicatesAllowed: true });
-
- expect(findToggleLabel().exists()).toBe(true);
- expect(findToggleLabel().text()).toMatchInterpolatedText(MAVEN_DUPLICATES_ALLOWED_ENABLED);
- });
-
- it('hides the form input group', () => {
- mountComponent({ ...defaultProps, mavenDuplicatesAllowed: true });
-
- expect(findInputGroup().exists()).toBe(false);
+ expect(findDefaultSlot().text()).toContain('mavenDuplicatesAllowed');
+ expect(findDefaultSlot().text()).toContain('mavenDuplicateExceptionRegex');
});
});
});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/settings_titles_spec.js b/spec/frontend/packages_and_registries/settings/group/components/settings_titles_spec.js
new file mode 100644
index 00000000000..a61edad8685
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/settings_titles_spec.js
@@ -0,0 +1,25 @@
+import { shallowMount } from '@vue/test-utils';
+import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue';
+
+describe('settings_titles', () => {
+ let wrapper;
+
+ const mountComponent = () => {
+ wrapper = shallowMount(SettingsTitles, {
+ propsData: {
+ title: 'foo',
+ subTitle: 'bar',
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders properly', () => {
+ mountComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js b/spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js
index e1a46f97318..03133bf1158 100644
--- a/spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js
@@ -9,7 +9,7 @@ describe('Package and Registries settings group cache updates', () => {
updateNamespacePackageSettings: {
packageSettings: {
mavenDuplicatesAllowed: false,
- mavenDuplicateExceptionRegex: 'latest[master]something',
+ mavenDuplicateExceptionRegex: 'latest[main]something',
},
},
},
diff --git a/spec/frontend/packages_and_registries/settings/group/mock_data.js b/spec/frontend/packages_and_registries/settings/group/mock_data.js
index 777c0898de0..65119e288a1 100644
--- a/spec/frontend/packages_and_registries/settings/group/mock_data.js
+++ b/spec/frontend/packages_and_registries/settings/group/mock_data.js
@@ -4,6 +4,8 @@ export const groupPackageSettingsMock = {
packageSettings: {
mavenDuplicatesAllowed: true,
mavenDuplicateExceptionRegex: '',
+ genericDuplicatesAllowed: true,
+ genericDuplicateExceptionRegex: '',
},
},
},
@@ -14,7 +16,9 @@ export const groupPackageSettingsMutationMock = (override) => ({
updateNamespacePackageSettings: {
packageSettings: {
mavenDuplicatesAllowed: true,
- mavenDuplicateExceptionRegex: 'latest[master]something',
+ mavenDuplicateExceptionRegex: 'latest[main]something',
+ genericDuplicatesAllowed: true,
+ genericDuplicateExceptionRegex: 'latest[main]somethingGeneric',
},
errors: [],
...override,
@@ -26,20 +30,20 @@ export const groupPackageSettingsMutationErrorMock = {
errors: [
{
message:
- 'Variable $input of type UpdateNamespacePackageSettingsInput! was provided invalid value for mavenDuplicateExceptionRegex (latest[master]somethingj)) is an invalid regexp: unexpected ): latest[master]somethingj)))',
+ 'Variable $input of type UpdateNamespacePackageSettingsInput! was provided invalid value for mavenDuplicateExceptionRegex (latest[main]somethingj)) is an invalid regexp: unexpected ): latest[main]somethingj)))',
locations: [{ line: 1, column: 41 }],
extensions: {
value: {
namespacePath: 'gitlab-org',
- mavenDuplicateExceptionRegex: 'latest[master]something))',
+ mavenDuplicateExceptionRegex: 'latest[main]something))',
},
problems: [
{
path: ['mavenDuplicateExceptionRegex'],
explanation:
- 'latest[master]somethingj)) is an invalid regexp: unexpected ): latest[master]something))',
+ 'latest[main]somethingj)) is an invalid regexp: unexpected ): latest[main]something))',
message:
- 'latest[master]somethingj)) is an invalid regexp: unexpected ): latest[master]something))',
+ 'latest[main]somethingj)) is an invalid regexp: unexpected ): latest[main]something))',
},
],
},
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap b/spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap
new file mode 100644
index 00000000000..7062773b46b
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap
@@ -0,0 +1,101 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Utils formOptionsGenerator returns an object containing cadence 1`] = `
+Array [
+ Object {
+ "default": true,
+ "key": "EVERY_DAY",
+ "label": "Every day",
+ },
+ Object {
+ "default": false,
+ "key": "EVERY_WEEK",
+ "label": "Every week",
+ },
+ Object {
+ "default": false,
+ "key": "EVERY_TWO_WEEKS",
+ "label": "Every two weeks",
+ },
+ Object {
+ "default": false,
+ "key": "EVERY_MONTH",
+ "label": "Every month",
+ },
+ Object {
+ "default": false,
+ "key": "EVERY_THREE_MONTHS",
+ "label": "Every three months",
+ },
+]
+`;
+
+exports[`Utils formOptionsGenerator returns an object containing keepN 1`] = `
+Array [
+ Object {
+ "default": false,
+ "key": "ONE_TAG",
+ "label": "1 tag per image name",
+ "variable": 1,
+ },
+ Object {
+ "default": false,
+ "key": "FIVE_TAGS",
+ "label": "5 tags per image name",
+ "variable": 5,
+ },
+ Object {
+ "default": true,
+ "key": "TEN_TAGS",
+ "label": "10 tags per image name",
+ "variable": 10,
+ },
+ Object {
+ "default": false,
+ "key": "TWENTY_FIVE_TAGS",
+ "label": "25 tags per image name",
+ "variable": 25,
+ },
+ Object {
+ "default": false,
+ "key": "FIFTY_TAGS",
+ "label": "50 tags per image name",
+ "variable": 50,
+ },
+ Object {
+ "default": false,
+ "key": "ONE_HUNDRED_TAGS",
+ "label": "100 tags per image name",
+ "variable": 100,
+ },
+]
+`;
+
+exports[`Utils formOptionsGenerator returns an object containing olderThan 1`] = `
+Array [
+ Object {
+ "default": false,
+ "key": "SEVEN_DAYS",
+ "label": "7 days",
+ "variable": 7,
+ },
+ Object {
+ "default": false,
+ "key": "FOURTEEN_DAYS",
+ "label": "14 days",
+ "variable": 14,
+ },
+ Object {
+ "default": false,
+ "key": "THIRTY_DAYS",
+ "label": "30 days",
+ "variable": 30,
+ },
+ Object {
+ "default": true,
+ "key": "NINETY_DAYS",
+ "label": "90 days",
+ "variable": 90,
+ },
+]
+`;
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap
new file mode 100644
index 00000000000..7a52b4a5d0f
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/project/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-input-stub
+ data-testid="keep-regex-input"
+ description="Tags with names that match this regex pattern are kept. %{linkStart}View regex examples.%{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-input-stub
+ data-testid="remove-regex-input"
+ description="Tags with names that match this regex pattern are removed. %{linkStart}View regex examples.%{linkEnd}"
+ error=""
+ label="Remove tags matching:"
+ name="remove-regex"
+ placeholder=".*"
+ value="asdasdssssdfdf"
+/>
+`;
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
new file mode 100644
index 00000000000..c56244a9138
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
@@ -0,0 +1,86 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlFormGroup, GlFormSelect } from 'jest/registry/shared/stubs';
+import component from '~/packages_and_registries/settings/project/components/expiration_dropdown.vue';
+
+describe('ExpirationDropdown', () => {
+ let wrapper;
+
+ const defaultProps = {
+ name: 'foo',
+ label: 'label-bar',
+ formOptions: [
+ { key: 'foo', label: 'bar' },
+ { key: 'baz', label: 'zab' },
+ ],
+ };
+
+ const findFormSelect = () => wrapper.find(GlFormSelect);
+ const findFormGroup = () => wrapper.find(GlFormGroup);
+ const findOptions = () => wrapper.findAll('[data-testid="option"]');
+
+ const mountComponent = (props) => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlFormGroup,
+ GlFormSelect,
+ },
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('structure', () => {
+ it('has a form-select component', () => {
+ mountComponent();
+ expect(findFormSelect().exists()).toBe(true);
+ });
+
+ it('has the correct options', () => {
+ mountComponent();
+
+ expect(findOptions()).toHaveLength(defaultProps.formOptions.length);
+ });
+ });
+
+ describe('model', () => {
+ it('assign the right props to the form-select component', () => {
+ const value = 'foobar';
+ const disabled = true;
+
+ mountComponent({ value, disabled });
+
+ expect(findFormSelect().props()).toMatchObject({
+ value,
+ disabled,
+ });
+ expect(findFormSelect().attributes('id')).toBe(defaultProps.name);
+ });
+
+ it('assign the right props to the form-group component', () => {
+ mountComponent();
+
+ expect(findFormGroup().attributes()).toMatchObject({
+ id: `${defaultProps.name}-form-group`,
+ 'label-for': defaultProps.name,
+ label: defaultProps.label,
+ });
+ });
+
+ it('emits input event when form-select emits input', () => {
+ const emittedValue = 'barfoo';
+
+ mountComponent();
+
+ findFormSelect().vm.$emit('input', emittedValue);
+
+ expect(wrapper.emitted('input')).toEqual([[emittedValue]]);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
new file mode 100644
index 00000000000..dd876d1d295
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
@@ -0,0 +1,169 @@
+import { GlSprintf, GlFormInput, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { GlFormGroup } from 'jest/registry/shared/stubs';
+import component from '~/packages_and_registries/settings/project/components/expiration_input.vue';
+import { NAME_REGEX_LENGTH } from '~/packages_and_registries/settings/project/constants';
+
+describe('ExpirationInput', () => {
+ let wrapper;
+
+ const defaultProps = {
+ name: 'foo',
+ label: 'label-bar',
+ placeholder: 'placeholder-baz',
+ description: '%{linkStart}description-foo%{linkEnd}',
+ };
+
+ const tagsRegexHelpPagePath = 'fooPath';
+
+ const findInput = () => wrapper.find(GlFormInput);
+ const findFormGroup = () => wrapper.find(GlFormGroup);
+ const findLabel = () => wrapper.find('[data-testid="label"]');
+ const findDescription = () => wrapper.find('[data-testid="description"]');
+ const findDescriptionLink = () => wrapper.find(GlLink);
+
+ const mountComponent = (props) => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlSprintf,
+ GlFormGroup,
+ },
+ provide: {
+ tagsRegexHelpPagePath,
+ },
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('structure', () => {
+ it('has a label', () => {
+ mountComponent();
+
+ expect(findLabel().text()).toBe(defaultProps.label);
+ });
+
+ it('has a textarea component', () => {
+ mountComponent();
+
+ expect(findInput().exists()).toBe(true);
+ });
+
+ it('has a description', () => {
+ mountComponent();
+
+ expect(findDescription().text()).toMatchInterpolatedText(defaultProps.description);
+ });
+
+ it('has a description link', () => {
+ mountComponent();
+
+ const link = findDescriptionLink();
+ expect(link.exists()).toBe(true);
+ expect(link.attributes('href')).toBe(tagsRegexHelpPagePath);
+ });
+ });
+
+ describe('model', () => {
+ it('assigns the right props to the textarea component', () => {
+ const value = 'foobar';
+ const disabled = true;
+
+ mountComponent({ value, disabled });
+
+ expect(findInput().attributes()).toMatchObject({
+ id: defaultProps.name,
+ value,
+ placeholder: defaultProps.placeholder,
+ disabled: `${disabled}`,
+ trim: '',
+ });
+ });
+
+ it('emits input event when textarea emits input', () => {
+ const emittedValue = 'barfoo';
+
+ mountComponent();
+
+ findInput().vm.$emit('input', emittedValue);
+ expect(wrapper.emitted('input')).toEqual([[emittedValue]]);
+ });
+ });
+
+ describe('regex textarea validation', () => {
+ const invalidString = new Array(NAME_REGEX_LENGTH + 2).join(',');
+
+ describe('when error contains an error message', () => {
+ const errorMessage = 'something went wrong';
+
+ it('shows the error message on the relevant field', () => {
+ mountComponent({ error: errorMessage });
+
+ expect(findFormGroup().attributes('invalid-feedback')).toBe(errorMessage);
+ });
+
+ it('gives precedence to API errors compared to local ones', () => {
+ mountComponent({
+ error: errorMessage,
+ value: invalidString,
+ });
+
+ expect(findFormGroup().attributes('invalid-feedback')).toBe(errorMessage);
+ });
+ });
+
+ describe('when error is empty', () => {
+ describe('if the user did not type', () => {
+ it('validation is not emitted', () => {
+ mountComponent();
+
+ expect(wrapper.emitted('validation')).toBeUndefined();
+ });
+
+ it('no error message is shown', () => {
+ mountComponent();
+
+ expect(findFormGroup().props('state')).toBe(true);
+ expect(findFormGroup().attributes('invalid-feedback')).toBe('');
+ });
+ });
+
+ describe('when the user typed something', () => {
+ describe(`when name regex is longer than ${NAME_REGEX_LENGTH}`, () => {
+ beforeEach(() => {
+ // since the component has no state we both emit the event and set the prop
+ mountComponent({ value: invalidString });
+
+ findInput().vm.$emit('input', invalidString);
+ });
+
+ it('textAreaValidation state is false', () => {
+ expect(findFormGroup().props('state')).toBe(false);
+ expect(findInput().attributes('state')).toBeUndefined();
+ });
+
+ it('emits the @validation event with false payload', () => {
+ expect(wrapper.emitted('validation')).toEqual([[false]]);
+ });
+ });
+
+ it(`when user input is less than ${NAME_REGEX_LENGTH} state is "true"`, () => {
+ mountComponent();
+
+ findInput().vm.$emit('input', 'foo');
+
+ expect(findFormGroup().props('state')).toBe(true);
+ expect(findInput().attributes('state')).toBe('true');
+ expect(wrapper.emitted('validation')).toEqual([[true]]);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
new file mode 100644
index 00000000000..854830391c5
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
@@ -0,0 +1,65 @@
+import { GlFormInput } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { GlFormGroup } from 'jest/registry/shared/stubs';
+import component from '~/packages_and_registries/settings/project/components/expiration_run_text.vue';
+import {
+ NEXT_CLEANUP_LABEL,
+ NOT_SCHEDULED_POLICY_TEXT,
+} from '~/packages_and_registries/settings/project/constants';
+
+describe('ExpirationToggle', () => {
+ let wrapper;
+ const value = 'foo';
+
+ const findInput = () => wrapper.find(GlFormInput);
+ const findFormGroup = () => wrapper.find(GlFormGroup);
+
+ const mountComponent = (propsData) => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlFormGroup,
+ },
+ propsData,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('structure', () => {
+ it('has an input component', () => {
+ mountComponent();
+
+ expect(findInput().exists()).toBe(true);
+ });
+ });
+
+ describe('model', () => {
+ it('assigns the right props to the form-group component', () => {
+ mountComponent();
+
+ expect(findFormGroup().attributes()).toMatchObject({
+ label: NEXT_CLEANUP_LABEL,
+ });
+ });
+ });
+
+ describe('formattedValue', () => {
+ it.each`
+ valueProp | enabled | expected
+ ${value} | ${true} | ${value}
+ ${value} | ${false} | ${NOT_SCHEDULED_POLICY_TEXT}
+ ${undefined} | ${false} | ${NOT_SCHEDULED_POLICY_TEXT}
+ ${undefined} | ${true} | ${NOT_SCHEDULED_POLICY_TEXT}
+ `(
+ 'when value is $valueProp and enabled is $enabled the input value is $expected',
+ ({ valueProp, enabled, expected }) => {
+ mountComponent({ value: valueProp, enabled });
+
+ expect(findInput().attributes('value')).toBe(expected);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
new file mode 100644
index 00000000000..3a3eb089b43
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
@@ -0,0 +1,77 @@
+import { GlToggle, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { GlFormGroup } from 'jest/registry/shared/stubs';
+import component from '~/packages_and_registries/settings/project/components/expiration_toggle.vue';
+import {
+ ENABLED_TOGGLE_DESCRIPTION,
+ DISABLED_TOGGLE_DESCRIPTION,
+} from '~/packages_and_registries/settings/project/constants';
+
+describe('ExpirationToggle', () => {
+ let wrapper;
+
+ const findToggle = () => wrapper.find(GlToggle);
+ const findDescription = () => wrapper.find('[data-testid="description"]');
+
+ const mountComponent = (propsData) => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlFormGroup,
+ GlSprintf,
+ },
+ propsData,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('structure', () => {
+ it('has a toggle component', () => {
+ mountComponent();
+
+ expect(findToggle().props('label')).toBe(component.i18n.toggleLabel);
+ });
+
+ it('has a description', () => {
+ mountComponent();
+
+ expect(findDescription().exists()).toBe(true);
+ });
+ });
+
+ describe('model', () => {
+ it('assigns the right props to the toggle component', () => {
+ mountComponent({ value: true, disabled: true });
+
+ expect(findToggle().props()).toMatchObject({
+ value: true,
+ disabled: true,
+ });
+ });
+
+ it('emits input event when toggle is updated', () => {
+ mountComponent();
+
+ findToggle().vm.$emit('change', false);
+
+ expect(wrapper.emitted('input')).toEqual([[false]]);
+ });
+ });
+
+ describe('toggle description', () => {
+ it('says enabled when the toggle is on', () => {
+ mountComponent({ value: true });
+
+ expect(findDescription().text()).toMatchInterpolatedText(ENABLED_TOGGLE_DESCRIPTION);
+ });
+
+ it('says disabled when the toggle is off', () => {
+ mountComponent({ value: false });
+
+ expect(findDescription().text()).toMatchInterpolatedText(DISABLED_TOGGLE_DESCRIPTION);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
new file mode 100644
index 00000000000..a725941f7f6
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
@@ -0,0 +1,164 @@
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import component from '~/packages_and_registries/settings/project/components/registry_settings_app.vue';
+import SettingsForm from '~/packages_and_registries/settings/project/components/settings_form.vue';
+import {
+ FETCH_SETTINGS_ERROR_MESSAGE,
+ UNAVAILABLE_FEATURE_INTRO_TEXT,
+ UNAVAILABLE_USER_FEATURE_TEXT,
+} from '~/packages_and_registries/settings/project/constants';
+import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
+
+import {
+ expirationPolicyPayload,
+ emptyExpirationPolicyPayload,
+ containerExpirationPolicyData,
+} from '../mock_data';
+
+const localVue = createLocalVue();
+
+describe('Registry Settings App', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const defaultProvidedValues = {
+ projectPath: 'path',
+ isAdmin: false,
+ adminSettingsPath: 'settingsPath',
+ enableHistoricEntries: false,
+ };
+
+ const findSettingsComponent = () => wrapper.find(SettingsForm);
+ const findAlert = () => wrapper.find(GlAlert);
+
+ const mountComponent = (provide = defaultProvidedValues, config) => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlSprintf,
+ },
+ mocks: {
+ $toast: {
+ show: jest.fn(),
+ },
+ },
+ provide,
+ ...config,
+ });
+ };
+
+ const mountComponentWithApollo = ({ provide = defaultProvidedValues, resolver } = {}) => {
+ localVue.use(VueApollo);
+
+ const requestHandlers = [[expirationPolicyQuery, resolver]];
+
+ fakeApollo = createMockApollo(requestHandlers);
+ mountComponent(provide, {
+ localVue,
+ apolloProvider: fakeApollo,
+ });
+
+ return requestHandlers.map((request) => request[1]);
+ };
+
+ afterEach(() => {
+ 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()),
+ });
+ await Promise.all(requests);
+
+ expect(findSettingsComponent().exists()).toBe(true);
+ });
+
+ describe('the form is disabled', () => {
+ it('the form is hidden', () => {
+ mountComponent();
+
+ expect(findSettingsComponent().exists()).toBe(false);
+ });
+
+ it('shows an alert', () => {
+ mountComponent();
+
+ const text = findAlert().text();
+ expect(text).toContain(UNAVAILABLE_FEATURE_INTRO_TEXT);
+ expect(text).toContain(UNAVAILABLE_USER_FEATURE_TEXT);
+ });
+
+ describe('an admin is visiting the page', () => {
+ it('shows the admin part of the alert message', () => {
+ mountComponent({ ...defaultProvidedValues, isAdmin: true });
+
+ const sprintf = findAlert().find(GlSprintf);
+ expect(sprintf.text()).toBe('administration settings');
+ expect(sprintf.find(GlLink).attributes('href')).toBe(
+ defaultProvidedValues.adminSettingsPath,
+ );
+ });
+ });
+ });
+
+ describe('fetchSettingsError', () => {
+ beforeEach(() => {
+ const requests = mountComponentWithApollo({
+ resolver: jest.fn().mockRejectedValue(new Error('GraphQL error')),
+ });
+ return Promise.all(requests);
+ });
+
+ it('the form is hidden', () => {
+ expect(findSettingsComponent().exists()).toBe(false);
+ });
+
+ it('shows an alert', () => {
+ expect(findAlert().html()).toContain(FETCH_SETTINGS_ERROR_MESSAGE);
+ });
+ });
+
+ describe('empty API response', () => {
+ it.each`
+ enableHistoricEntries | isShown
+ ${true} | ${true}
+ ${false} | ${false}
+ `('is $isShown that the form is shown', async ({ enableHistoricEntries, isShown }) => {
+ const requests = mountComponentWithApollo({
+ provide: {
+ ...defaultProvidedValues,
+ enableHistoricEntries,
+ },
+ resolver: jest.fn().mockResolvedValue(emptyExpirationPolicyPayload()),
+ });
+ await Promise.all(requests);
+
+ expect(findSettingsComponent().exists()).toBe(isShown);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
new file mode 100644
index 00000000000..7e5383d7ff1
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
@@ -0,0 +1,460 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { GlCard, GlLoadingIcon } from 'jest/registry/shared/stubs';
+import component from '~/packages_and_registries/settings/project/components/settings_form.vue';
+import {
+ UPDATE_SETTINGS_ERROR_MESSAGE,
+ UPDATE_SETTINGS_SUCCESS_MESSAGE,
+} from '~/packages_and_registries/settings/project/constants';
+import updateContainerExpirationPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_container_expiration_policy.mutation.graphql';
+import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
+import Tracking from '~/tracking';
+import { expirationPolicyPayload, expirationPolicyMutationPayload } from '../mock_data';
+
+const localVue = createLocalVue();
+
+describe('Settings Form', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const defaultProvidedValues = {
+ projectPath: 'path',
+ };
+
+ const {
+ data: {
+ project: { containerExpirationPolicy },
+ },
+ } = expirationPolicyPayload();
+
+ const defaultProps = {
+ value: { ...containerExpirationPolicy },
+ };
+
+ const trackingPayload = {
+ label: 'docker_container_retention_and_expiration_policies',
+ };
+
+ const findForm = () => wrapper.find({ ref: 'form-element' });
+
+ 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 findKeepRegexInput = () => wrapper.find('[data-testid="keep-regex-input"]');
+ const findOlderThanDropdown = () => wrapper.find('[data-testid="older-than-dropdown"]');
+ const findRemoveRegexInput = () => wrapper.find('[data-testid="remove-regex-input"]');
+
+ const mountComponent = ({
+ props = defaultProps,
+ data,
+ config,
+ provide = defaultProvidedValues,
+ mocks,
+ } = {}) => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlCard,
+ GlLoadingIcon,
+ },
+ propsData: { ...props },
+ provide,
+ data() {
+ return {
+ ...data,
+ };
+ },
+ mocks: {
+ $toast: {
+ show: jest.fn(),
+ },
+ ...mocks,
+ },
+ ...config,
+ });
+ };
+
+ const mountComponentWithApollo = ({
+ provide = defaultProvidedValues,
+ mutationResolver,
+ queryPayload = expirationPolicyPayload(),
+ } = {}) => {
+ localVue.use(VueApollo);
+
+ const requestHandlers = [
+ [updateContainerExpirationPolicyMutation, mutationResolver],
+ [expirationPolicyQuery, jest.fn().mockResolvedValue(queryPayload)],
+ ];
+
+ fakeApollo = createMockApollo(requestHandlers);
+
+ // This component does not do the query directly, but we need a proper cache to update
+ fakeApollo.defaultClient.cache.writeQuery({
+ query: expirationPolicyQuery,
+ variables: {
+ projectPath: provide.projectPath,
+ },
+ ...queryPayload,
+ });
+
+ // we keep in sync what prop we pass to the component with the cache
+ const {
+ data: {
+ project: { containerExpirationPolicy: value },
+ },
+ } = queryPayload;
+
+ mountComponent({
+ provide,
+ props: {
+ ...defaultProps,
+ value,
+ },
+ config: {
+ localVue,
+ apolloProvider: fakeApollo,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ jest.spyOn(Tracking, 'event');
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ 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'} | ${findKeepRegexInput} | ${'Keep Regex'} | ${'textarea'} | ${''}
+ ${'olderThan'} | ${findOlderThanDropdown} | ${'OlderThan'} | ${'dropdown'} | ${'NINETY_DAYS'}
+ ${'nameRegex'} | ${findRemoveRegexInput} | ${'Remove regex'} | ${'textarea'} | ${''}
+ `('$fieldName', ({ model, finder, type, defaultValue }) => {
+ it('matches snapshot', () => {
+ mountComponent();
+
+ expect(finder().element).toMatchSnapshot();
+ });
+
+ 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(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', () => {
+ it('calls the appropriate function', () => {
+ mountComponent();
+
+ findForm().trigger('reset');
+
+ 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(findKeepRegexInput().props('error')).toBe('');
+ expect(findRemoveRegexInput().props('error')).toBe('');
+ expect(findSaveButton().props('disabled')).toBe(false);
+ });
+ });
+
+ describe('form submit event ', () => {
+ it('save has type submit', () => {
+ mountComponent();
+
+ expect(findSaveButton().attributes('type')).toBe('submit');
+ });
+
+ it('dispatches the correct apollo mutation', () => {
+ const mutationResolver = jest.fn().mockResolvedValue(expirationPolicyMutationPayload());
+ mountComponentWithApollo({
+ mutationResolver,
+ });
+
+ findForm().trigger('submit');
+
+ expect(mutationResolver).toHaveBeenCalled();
+ });
+
+ it('saves the default values when a value is missing did not change the default options', async () => {
+ const mutationResolver = jest.fn().mockResolvedValue(expirationPolicyMutationPayload());
+ mountComponentWithApollo({
+ mutationResolver,
+ queryPayload: expirationPolicyPayload({ keepN: null, cadence: null, olderThan: null }),
+ });
+
+ await waitForPromises();
+
+ findForm().trigger('submit');
+
+ expect(mutationResolver).toHaveBeenCalledWith({
+ input: {
+ cadence: 'EVERY_DAY',
+ enabled: true,
+ keepN: 'TEN_TAGS',
+ nameRegex: 'asdasdssssdfdf',
+ nameRegexKeep: 'sss',
+ olderThan: 'NINETY_DAYS',
+ projectPath: 'path',
+ },
+ });
+ });
+
+ it('tracks the submit event', () => {
+ mountComponentWithApollo({
+ mutationResolver: jest.fn().mockResolvedValue(expirationPolicyMutationPayload()),
+ });
+
+ findForm().trigger('submit');
+
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'submit_form', trackingPayload);
+ });
+
+ it('show a success toast when submit succeed', async () => {
+ mountComponentWithApollo({
+ mutationResolver: jest.fn().mockResolvedValue(expirationPolicyMutationPayload()),
+ });
+
+ findForm().trigger('submit');
+ await waitForPromises();
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE, {
+ type: 'success',
+ });
+ });
+
+ describe('when submit fails', () => {
+ describe('user recoverable errors', () => {
+ it('when there is an error is shown in a toast', async () => {
+ mountComponentWithApollo({
+ mutationResolver: jest
+ .fn()
+ .mockResolvedValue(expirationPolicyMutationPayload({ errors: ['foo'] })),
+ });
+
+ findForm().trigger('submit');
+ await waitForPromises();
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('foo', {
+ type: 'error',
+ });
+ });
+ });
+
+ describe('global errors', () => {
+ it('shows an error', async () => {
+ mountComponentWithApollo({
+ mutationResolver: jest.fn().mockRejectedValue(expirationPolicyMutationPayload()),
+ });
+
+ findForm().trigger('submit');
+ await waitForPromises();
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE, {
+ type: 'error',
+ });
+ });
+
+ it('parses the error messages', async () => {
+ const mutate = jest.fn().mockRejectedValue({
+ graphQLErrors: [
+ {
+ extensions: {
+ problems: [{ path: ['nameRegexKeep'], message: 'baz' }],
+ },
+ },
+ ],
+ });
+ mountComponent({ mocks: { $apollo: { mutate } } });
+
+ findForm().trigger('submit');
+ await waitForPromises();
+ await wrapper.vm.$nextTick();
+
+ expect(findKeepRegexInput().props('error')).toEqual('baz');
+ });
+ });
+ });
+ });
+ });
+
+ describe('form actions', () => {
+ describe('cancel button', () => {
+ it('has type reset', () => {
+ mountComponent();
+
+ expect(findCancelButton().attributes('type')).toBe('reset');
+ });
+
+ it.each`
+ isLoading | isEdited | mutationLoading
+ ${true} | ${true} | ${true}
+ ${false} | ${true} | ${true}
+ ${false} | ${false} | ${true}
+ ${true} | ${false} | ${false}
+ ${false} | ${false} | ${false}
+ `(
+ 'when isLoading is $isLoading, isEdited is $isEdited and mutationLoading is $mutationLoading is disabled',
+ ({ isEdited, isLoading, mutationLoading }) => {
+ mountComponent({
+ props: { ...defaultProps, isEdited, isLoading },
+ data: { mutationLoading },
+ });
+
+ expect(findCancelButton().props('disabled')).toBe(true);
+ },
+ );
+ });
+
+ describe('submit button', () => {
+ it('has type submit', () => {
+ mountComponent();
+
+ expect(findSaveButton().attributes('type')).toBe('submit');
+ });
+
+ it.each`
+ 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, localErrors is $localErrors and mutationLoading is $mutationLoading is disabled',
+ ({ localErrors, isLoading, mutationLoading }) => {
+ mountComponent({
+ props: { ...defaultProps, isLoading },
+ data: { mutationLoading, localErrors },
+ });
+
+ expect(findSaveButton().props('disabled')).toBe(true);
+ },
+ );
+
+ it.each`
+ isLoading | mutationLoading | showLoading
+ ${true} | ${true} | ${true}
+ ${true} | ${false} | ${true}
+ ${false} | ${true} | ${true}
+ ${false} | ${false} | ${false}
+ `(
+ 'when isLoading is $isLoading and mutationLoading is $mutationLoading is $showLoading that the loading icon is shown',
+ ({ isLoading, mutationLoading, showLoading }) => {
+ mountComponent({
+ props: { ...defaultProps, isLoading },
+ data: { mutationLoading },
+ });
+
+ expect(findSaveButton().props('loading')).toBe(showLoading);
+ },
+ );
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/graphql/cache_updated_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/graphql/cache_updated_spec.js
new file mode 100644
index 00000000000..4d6bd65bd93
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/project/settings/graphql/cache_updated_spec.js
@@ -0,0 +1,56 @@
+import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
+import { updateContainerExpirationPolicy } from '~/packages_and_registries/settings/project/graphql/utils/cache_update';
+
+describe('Registry settings cache update', () => {
+ let client;
+
+ const payload = {
+ data: {
+ updateContainerExpirationPolicy: {
+ containerExpirationPolicy: {
+ enabled: true,
+ },
+ },
+ },
+ };
+
+ const cacheMock = {
+ project: {
+ containerExpirationPolicy: {
+ enabled: false,
+ },
+ },
+ };
+
+ const queryAndVariables = {
+ query: expirationPolicyQuery,
+ variables: { projectPath: 'foo' },
+ };
+
+ beforeEach(() => {
+ client = {
+ readQuery: jest.fn().mockReturnValue(cacheMock),
+ writeQuery: jest.fn(),
+ };
+ });
+ describe('Registry settings cache update', () => {
+ it('calls readQuery', () => {
+ updateContainerExpirationPolicy('foo')(client, payload);
+ expect(client.readQuery).toHaveBeenCalledWith(queryAndVariables);
+ });
+
+ it('writes the correct result in the cache', () => {
+ updateContainerExpirationPolicy('foo')(client, payload);
+ expect(client.writeQuery).toHaveBeenCalledWith({
+ ...queryAndVariables,
+ data: {
+ project: {
+ containerExpirationPolicy: {
+ enabled: true,
+ },
+ },
+ },
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
new file mode 100644
index 00000000000..9778f409010
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
@@ -0,0 +1,40 @@
+export const containerExpirationPolicyData = () => ({
+ cadence: 'EVERY_DAY',
+ enabled: true,
+ keepN: 'TEN_TAGS',
+ nameRegex: 'asdasdssssdfdf',
+ nameRegexKeep: 'sss',
+ olderThan: 'FOURTEEN_DAYS',
+ nextRunAt: '2020-11-19T07:37:03.941Z',
+});
+
+export const expirationPolicyPayload = (override) => ({
+ data: {
+ project: {
+ containerExpirationPolicy: {
+ ...containerExpirationPolicyData(),
+ ...override,
+ },
+ },
+ },
+});
+
+export const emptyExpirationPolicyPayload = () => ({
+ data: {
+ project: {
+ containerExpirationPolicy: {},
+ },
+ },
+});
+
+export const expirationPolicyMutationPayload = ({ override, errors = [] } = {}) => ({
+ data: {
+ updateContainerExpirationPolicy: {
+ containerExpirationPolicy: {
+ ...containerExpirationPolicyData(),
+ ...override,
+ },
+ errors,
+ },
+ },
+});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/utils_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/utils_spec.js
new file mode 100644
index 00000000000..4c81671cd46
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/project/settings/utils_spec.js
@@ -0,0 +1,37 @@
+import {
+ formOptionsGenerator,
+ optionLabelGenerator,
+ olderThanTranslationGenerator,
+} from '~/packages_and_registries/settings/project/utils';
+
+describe('Utils', () => {
+ describe('optionLabelGenerator', () => {
+ it('returns an array with a set label', () => {
+ const result = optionLabelGenerator(
+ [{ variable: 1 }, { variable: 2 }],
+ olderThanTranslationGenerator,
+ );
+ expect(result).toEqual([
+ { variable: 1, label: '1 day' },
+ { variable: 2, label: '2 days' },
+ ]);
+ });
+ });
+
+ describe('formOptionsGenerator', () => {
+ it('returns an object containing olderThan', () => {
+ expect(formOptionsGenerator().olderThan).toBeDefined();
+ expect(formOptionsGenerator().olderThan).toMatchSnapshot();
+ });
+
+ it('returns an object containing cadence', () => {
+ expect(formOptionsGenerator().cadence).toBeDefined();
+ expect(formOptionsGenerator().cadence).toMatchSnapshot();
+ });
+
+ it('returns an object containing keepN', () => {
+ expect(formOptionsGenerator().keepN).toBeDefined();
+ expect(formOptionsGenerator().keepN).toMatchSnapshot();
+ });
+ });
+});