summaryrefslogtreecommitdiff
path: root/spec/frontend/releases/components
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/releases/components')
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js (renamed from spec/frontend/releases/components/app_edit_spec.js)103
-rw-r--r--spec/frontend/releases/components/app_index_spec.js2
-rw-r--r--spec/frontend/releases/components/app_new_spec.js26
-rw-r--r--spec/frontend/releases/components/app_show_spec.js2
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js143
-rw-r--r--spec/frontend/releases/components/release_block_assets_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js2
-rw-r--r--spec/frontend/releases/components/release_block_metadata_spec.js2
-rw-r--r--spec/frontend/releases/components/tag_field_exsting_spec.js78
-rw-r--r--spec/frontend/releases/components/tag_field_new_spec.js144
-rw-r--r--spec/frontend/releases/components/tag_field_spec.js59
11 files changed, 465 insertions, 100 deletions
diff --git a/spec/frontend/releases/components/app_edit_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index 4450b047acd..e9727801c1a 100644
--- a/spec/frontend/releases/components/app_edit_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -1,15 +1,15 @@
import Vuex from 'vuex';
import { mount } from '@vue/test-utils';
-import ReleaseEditApp from '~/releases/components/app_edit.vue';
+import { merge } from 'lodash';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue';
import { release as originalRelease, milestones as originalMilestones } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
-import { merge } from 'lodash';
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
-describe('Release edit component', () => {
+describe('Release edit/new component', () => {
let wrapper;
let release;
let actions;
@@ -27,13 +27,14 @@ describe('Release edit component', () => {
};
actions = {
- fetchRelease: jest.fn(),
- updateRelease: jest.fn(),
+ initializeRelease: jest.fn(),
+ saveRelease: jest.fn(),
addEmptyAssetLink: jest.fn(),
};
getters = {
isValid: () => true,
+ isExistingRelease: () => true,
validationErrors: () => ({
assets: {
links: [],
@@ -57,12 +58,14 @@ describe('Release edit component', () => {
),
);
- wrapper = mount(ReleaseEditApp, {
+ wrapper = mount(ReleaseEditNewApp, {
store,
provide: {
glFeatures: featureFlags,
},
});
+
+ wrapper.element.querySelectorAll('input').forEach(input => jest.spyOn(input, 'focus'));
};
beforeEach(() => {
@@ -80,14 +83,23 @@ describe('Release edit component', () => {
});
const findSubmitButton = () => wrapper.find('button[type=submit]');
+ const findForm = () => wrapper.find('form');
describe(`basic functionality tests: all tests unrelated to the "${BACK_URL_PARAM}" parameter`, () => {
- beforeEach(() => {
- factory();
+ beforeEach(factory);
+
+ it('calls initializeRelease when the component is created', () => {
+ expect(actions.initializeRelease).toHaveBeenCalledTimes(1);
});
- it('calls fetchRelease when the component is created', () => {
- expect(actions.fetchRelease).toHaveBeenCalledTimes(1);
+ it('focuses the first non-disabled input element once the page is shown', () => {
+ const firstEnabledInput = wrapper.element.querySelector('input:enabled');
+ const allInputs = wrapper.element.querySelectorAll('input');
+
+ allInputs.forEach(input => {
+ const expectedFocusCalls = input === firstEnabledInput ? 1 : 0;
+ expect(input.focus).toHaveBeenCalledTimes(expectedFocusCalls);
+ });
});
it('renders the description text at the top of the page', () => {
@@ -96,28 +108,6 @@ describe('Release edit component', () => {
);
});
- it('renders the correct tag name in the "Tag name" field', () => {
- expect(wrapper.find('#git-ref').element.value).toBe(release.tagName);
- });
-
- it('renders the correct help text under the "Tag name" field', () => {
- const helperText = wrapper.find('#tag-name-help');
- const helperTextLink = helperText.find('a');
- const helperTextLinkAttrs = helperTextLink.attributes();
-
- expect(helperText.text()).toBe(
- 'Changing a Release tag is only supported via Releases API. More information',
- );
- expect(helperTextLink.text()).toBe('More information');
- expect(helperTextLinkAttrs).toEqual(
- expect.objectContaining({
- href: state.updateReleaseApiDocsPath,
- rel: 'noopener noreferrer',
- target: '_blank',
- }),
- );
- });
-
it('renders the correct release title in the "Release title" field', () => {
expect(wrapper.find('#release-title').element.value).toBe(release.name);
});
@@ -130,16 +120,15 @@ describe('Release edit component', () => {
expect(findSubmitButton().attributes('type')).toBe('submit');
});
- it('calls updateRelease when the form is submitted', () => {
- wrapper.find('form').trigger('submit');
- expect(actions.updateRelease).toHaveBeenCalledTimes(1);
+ it('calls saveRelease when the form is submitted', () => {
+ findForm().trigger('submit');
+
+ expect(actions.saveRelease).toHaveBeenCalledTimes(1);
});
});
describe(`when the URL does not contain a "${BACK_URL_PARAM}" parameter`, () => {
- beforeEach(() => {
- factory();
- });
+ beforeEach(factory);
it(`renders a "Cancel" button with an href pointing to "${BACK_URL_PARAM}"`, () => {
const cancelButton = wrapper.find('.js-cancel-button');
@@ -164,6 +153,34 @@ describe('Release edit component', () => {
});
});
+ describe('when creating a new release', () => {
+ beforeEach(() => {
+ factory({
+ store: {
+ modules: {
+ detail: {
+ getters: {
+ isExistingRelease: () => false,
+ },
+ },
+ },
+ },
+ });
+ });
+
+ it('renders the submit button with the text "Create release"', () => {
+ expect(findSubmitButton().text()).toBe('Create release');
+ });
+ });
+
+ describe('when editing an existing release', () => {
+ beforeEach(factory);
+
+ it('renders the submit button with the text "Save changes"', () => {
+ expect(findSubmitButton().text()).toBe('Save changes');
+ });
+ });
+
describe('asset links form', () => {
const findAssetLinksForm = () => wrapper.find(AssetLinksForm);
@@ -227,6 +244,12 @@ describe('Release edit component', () => {
it('renders the submit button as disabled', () => {
expect(findSubmitButton().attributes('disabled')).toBe('disabled');
});
+
+ it('does not allow the form to be submitted', () => {
+ findForm().trigger('submit');
+
+ expect(actions.saveRelease).not.toHaveBeenCalled();
+ });
});
});
});
diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js
index 91beb5b1418..8eafe07cb2f 100644
--- a/spec/frontend/releases/components/app_index_spec.js
+++ b/spec/frontend/releases/components/app_index_spec.js
@@ -1,6 +1,7 @@
import { range as rge } from 'lodash';
import Vue from 'vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import app from '~/releases/components/app_index.vue';
import createStore from '~/releases/stores';
import listModule from '~/releases/stores/modules/list';
@@ -13,7 +14,6 @@ import {
releases,
} from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import waitForPromises from 'helpers/wait_for_promises';
describe('Releases App ', () => {
const Component = Vue.extend(app);
diff --git a/spec/frontend/releases/components/app_new_spec.js b/spec/frontend/releases/components/app_new_spec.js
deleted file mode 100644
index 0d5664766e5..00000000000
--- a/spec/frontend/releases/components/app_new_spec.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { mount } from '@vue/test-utils';
-import ReleaseNewApp from '~/releases/components/app_new.vue';
-
-Vue.use(Vuex);
-
-describe('Release new component', () => {
- let wrapper;
-
- const factory = () => {
- const store = new Vuex.Store();
- wrapper = mount(ReleaseNewApp, { store });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('renders the app', () => {
- factory();
-
- expect(wrapper.exists()).toBe(true);
- });
-});
diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js
index 3dc9964c25c..e757fe98661 100644
--- a/spec/frontend/releases/components/app_show_spec.js
+++ b/spec/frontend/releases/components/app_show_spec.js
@@ -1,8 +1,8 @@
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
+import { GlSkeletonLoading } from '@gitlab/ui';
import ReleaseShowApp from '~/releases/components/app_show.vue';
import { release as originalRelease } from '../mock_data';
-import { GlSkeletonLoading } from '@gitlab/ui';
import ReleaseBlock from '~/releases/components/release_block.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js
index e1f8592270e..727d593d851 100644
--- a/spec/frontend/releases/components/asset_links_form_spec.js
+++ b/spec/frontend/releases/components/asset_links_form_spec.js
@@ -3,6 +3,7 @@ import { mount, createLocalVue } from '@vue/test-utils';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import { release as originalRelease } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
+import { ENTER_KEY } from '~/lib/utils/keys';
import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
const localVue = createLocalVue();
@@ -91,42 +92,128 @@ describe('Release edit component', () => {
expect(actions.removeAssetLink).toHaveBeenCalledTimes(1);
});
- it('calls the "updateAssetLinkUrl" store method when text is entered into the "URL" input field', () => {
- const linkIdToUpdate = release.assets.links[0].id;
- const newUrl = 'updated url';
+ describe('URL input field', () => {
+ let input;
+ let linkIdToUpdate;
+ let newUrl;
- expect(actions.updateAssetLinkUrl).not.toHaveBeenCalled();
+ beforeEach(() => {
+ input = wrapper.find({ ref: 'urlInput' }).element;
+ linkIdToUpdate = release.assets.links[0].id;
+ newUrl = 'updated url';
+ });
- wrapper.find({ ref: 'urlInput' }).vm.$emit('change', newUrl);
+ const expectStoreMethodNotToBeCalled = () => {
+ expect(actions.updateAssetLinkUrl).not.toHaveBeenCalled();
+ };
- expect(actions.updateAssetLinkUrl).toHaveBeenCalledTimes(1);
- expect(actions.updateAssetLinkUrl).toHaveBeenCalledWith(
- expect.anything(),
- {
- linkIdToUpdate,
- newUrl,
- },
- undefined,
- );
+ const dispatchKeydowEvent = eventParams => {
+ const event = new KeyboardEvent('keydown', eventParams);
+
+ input.dispatchEvent(event);
+ };
+
+ const expectStoreMethodToBeCalled = () => {
+ expect(actions.updateAssetLinkUrl).toHaveBeenCalledTimes(1);
+ expect(actions.updateAssetLinkUrl).toHaveBeenCalledWith(
+ expect.anything(),
+ {
+ linkIdToUpdate,
+ newUrl,
+ },
+ undefined,
+ );
+ };
+
+ it('calls the "updateAssetLinkUrl" store method when text is entered into the "URL" input field', () => {
+ expectStoreMethodNotToBeCalled();
+
+ wrapper.find({ ref: 'urlInput' }).vm.$emit('change', newUrl);
+
+ expectStoreMethodToBeCalled();
+ });
+
+ it('calls the "updateAssetLinkUrl" store method when Ctrl+Enter is pressed inside the "URL" input field', () => {
+ expectStoreMethodNotToBeCalled();
+
+ input.value = newUrl;
+
+ dispatchKeydowEvent({ key: ENTER_KEY, ctrlKey: true });
+
+ expectStoreMethodToBeCalled();
+ });
+
+ it('calls the "updateAssetLinkUrl" store method when Cmd+Enter is pressed inside the "URL" input field', () => {
+ expectStoreMethodNotToBeCalled();
+
+ input.value = newUrl;
+
+ dispatchKeydowEvent({ key: ENTER_KEY, metaKey: true });
+
+ expectStoreMethodToBeCalled();
+ });
});
- it('calls the "updateAssetLinkName" store method when text is entered into the "Link title" input field', () => {
- const linkIdToUpdate = release.assets.links[0].id;
- const newName = 'updated name';
+ describe('Link title field', () => {
+ let input;
+ let linkIdToUpdate;
+ let newName;
- expect(actions.updateAssetLinkName).not.toHaveBeenCalled();
+ beforeEach(() => {
+ input = wrapper.find({ ref: 'nameInput' }).element;
+ linkIdToUpdate = release.assets.links[0].id;
+ newName = 'updated name';
+ });
- wrapper.find({ ref: 'nameInput' }).vm.$emit('change', newName);
+ const expectStoreMethodNotToBeCalled = () => {
+ expect(actions.updateAssetLinkUrl).not.toHaveBeenCalled();
+ };
- expect(actions.updateAssetLinkName).toHaveBeenCalledTimes(1);
- expect(actions.updateAssetLinkName).toHaveBeenCalledWith(
- expect.anything(),
- {
- linkIdToUpdate,
- newName,
- },
- undefined,
- );
+ const dispatchKeydowEvent = eventParams => {
+ const event = new KeyboardEvent('keydown', eventParams);
+
+ input.dispatchEvent(event);
+ };
+
+ const expectStoreMethodToBeCalled = () => {
+ expect(actions.updateAssetLinkName).toHaveBeenCalledTimes(1);
+ expect(actions.updateAssetLinkName).toHaveBeenCalledWith(
+ expect.anything(),
+ {
+ linkIdToUpdate,
+ newName,
+ },
+ undefined,
+ );
+ };
+
+ it('calls the "updateAssetLinkName" store method when text is entered into the "Link title" input field', () => {
+ expectStoreMethodNotToBeCalled();
+
+ wrapper.find({ ref: 'nameInput' }).vm.$emit('change', newName);
+
+ expectStoreMethodToBeCalled();
+ });
+
+ it('calls the "updateAssetLinkName" store method when Ctrl+Enter is pressed inside the "Link title" input field', () => {
+ expectStoreMethodNotToBeCalled();
+
+ input.value = newName;
+
+ dispatchKeydowEvent({ key: ENTER_KEY, ctrlKey: true });
+
+ expectStoreMethodToBeCalled();
+ });
+
+ it('calls the "updateAssetLinkName" store method when Cmd+Enter is pressed inside the "Link title" input field', () => {
+ expectStoreMethodNotToBeCalled();
+
+ input.value = newName;
+
+ dispatchKeydowEvent({ key: ENTER_KEY, metaKey: true });
+
+ expectStoreMethodToBeCalled();
+ });
});
it('calls the "updateAssetLinkType" store method when an option is selected from the "Type" dropdown', () => {
diff --git a/spec/frontend/releases/components/release_block_assets_spec.js b/spec/frontend/releases/components/release_block_assets_spec.js
index a85532a8118..5e84290716c 100644
--- a/spec/frontend/releases/components/release_block_assets_spec.js
+++ b/spec/frontend/releases/components/release_block_assets_spec.js
@@ -1,10 +1,10 @@
import { mount } from '@vue/test-utils';
import { GlCollapse } from '@gitlab/ui';
+import { trimText } from 'helpers/text_helper';
+import { cloneDeep } from 'lodash';
import ReleaseBlockAssets from '~/releases/components/release_block_assets.vue';
import { ASSET_LINK_TYPE } from '~/releases/constants';
-import { trimText } from 'helpers/text_helper';
import { assets } from '../mock_data';
-import { cloneDeep } from 'lodash';
describe('Release block assets', () => {
let wrapper;
diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js
index b91cfb82b65..c066bfbf020 100644
--- a/spec/frontend/releases/components/release_block_footer_spec.js
+++ b/spec/frontend/releases/components/release_block_footer_spec.js
@@ -1,11 +1,11 @@
import { mount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
+import { cloneDeep } from 'lodash';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { release as originalRelease } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { cloneDeep } from 'lodash';
const mockFutureDate = new Date(9999, 0, 0).toISOString();
let mockIsFutureRelease = false;
diff --git a/spec/frontend/releases/components/release_block_metadata_spec.js b/spec/frontend/releases/components/release_block_metadata_spec.js
index cbe478bfa1f..6f184e45600 100644
--- a/spec/frontend/releases/components/release_block_metadata_spec.js
+++ b/spec/frontend/releases/components/release_block_metadata_spec.js
@@ -1,9 +1,9 @@
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
+import { cloneDeep } from 'lodash';
import ReleaseBlockMetadata from '~/releases/components/release_block_metadata.vue';
import { release as originalRelease } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { cloneDeep } from 'lodash';
const mockFutureDate = new Date(9999, 0, 0).toISOString();
let mockIsFutureRelease = false;
diff --git a/spec/frontend/releases/components/tag_field_exsting_spec.js b/spec/frontend/releases/components/tag_field_exsting_spec.js
new file mode 100644
index 00000000000..0a04f68bd67
--- /dev/null
+++ b/spec/frontend/releases/components/tag_field_exsting_spec.js
@@ -0,0 +1,78 @@
+import { GlFormInput } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
+import TagFieldExisting from '~/releases/components/tag_field_existing.vue';
+import createStore from '~/releases/stores';
+import createDetailModule from '~/releases/stores/modules/detail';
+
+const TEST_TAG_NAME = 'test-tag-name';
+const TEST_DOCS_PATH = '/help/test/docs/path';
+
+describe('releases/components/tag_field_existing', () => {
+ let store;
+ let wrapper;
+
+ const createComponent = (mountFn = shallowMount) => {
+ wrapper = mountFn(TagFieldExisting, {
+ store,
+ });
+ };
+
+ const findInput = () => wrapper.find(GlFormInput);
+ const findHelp = () => wrapper.find('[data-testid="tag-name-help"]');
+ const findHelpLink = () => {
+ const link = findHelp().find('a');
+
+ return {
+ text: link.text(),
+ href: link.attributes('href'),
+ target: link.attributes('target'),
+ };
+ };
+
+ beforeEach(() => {
+ store = createStore({
+ modules: {
+ detail: createDetailModule({
+ updateReleaseApiDocsPath: TEST_DOCS_PATH,
+ tagName: TEST_TAG_NAME,
+ }),
+ },
+ });
+
+ store.state.detail.release = {
+ tagName: TEST_TAG_NAME,
+ };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('default', () => {
+ it('shows the tag name', () => {
+ createComponent();
+
+ expect(findInput().attributes()).toMatchObject({
+ disabled: '',
+ value: TEST_TAG_NAME,
+ });
+ });
+
+ it('shows help', () => {
+ createComponent(mount);
+
+ expect(findHelp().text()).toMatchInterpolatedText(
+ 'Changing a Release tag is only supported via Releases API. More information',
+ );
+
+ const helpLink = findHelpLink();
+
+ expect(helpLink).toEqual({
+ text: 'More information',
+ href: TEST_DOCS_PATH,
+ target: '_blank',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js
new file mode 100644
index 00000000000..b6ebc496f33
--- /dev/null
+++ b/spec/frontend/releases/components/tag_field_new_spec.js
@@ -0,0 +1,144 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import { GlFormInput } from '@gitlab/ui';
+import TagFieldNew from '~/releases/components/tag_field_new.vue';
+import createStore from '~/releases/stores';
+import createDetailModule from '~/releases/stores/modules/detail';
+import RefSelector from '~/ref/components/ref_selector.vue';
+
+const TEST_TAG_NAME = 'test-tag-name';
+const TEST_PROJECT_ID = '1234';
+const TEST_CREATE_FROM = 'test-create-from';
+
+describe('releases/components/tag_field_new', () => {
+ let store;
+ let wrapper;
+
+ const createComponent = (mountFn = shallowMount) => {
+ wrapper = mountFn(TagFieldNew, {
+ store,
+ stubs: {
+ RefSelector: true,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ store = createStore({
+ modules: {
+ detail: createDetailModule({
+ projectId: TEST_PROJECT_ID,
+ }),
+ },
+ });
+
+ store.state.detail.createFrom = TEST_CREATE_FROM;
+
+ store.state.detail.release = {
+ tagName: TEST_TAG_NAME,
+ assets: {
+ links: [],
+ },
+ };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findTagNameFormGroup = () => wrapper.find('[data-testid="tag-name-field"]');
+ const findTagNameGlInput = () => findTagNameFormGroup().find(GlFormInput);
+ const findTagNameInput = () => findTagNameFormGroup().find('input');
+
+ const findCreateFromFormGroup = () => wrapper.find('[data-testid="create-from-field"]');
+ const findCreateFromDropdown = () => findCreateFromFormGroup().find(RefSelector);
+
+ describe('"Tag name" field', () => {
+ describe('rendering and behavior', () => {
+ beforeEach(() => createComponent());
+
+ it('renders a label', () => {
+ expect(findTagNameFormGroup().attributes().label).toBe('Tag name');
+ });
+
+ describe('when the user updates the field', () => {
+ it("updates the store's release.tagName property", () => {
+ const updatedTagName = 'updated-tag-name';
+ findTagNameGlInput().vm.$emit('input', updatedTagName);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.state.detail.release.tagName).toBe(updatedTagName);
+ });
+ });
+ });
+ });
+
+ describe('validation', () => {
+ beforeEach(() => {
+ createComponent(mount);
+ });
+
+ /**
+ * Utility function to test the visibility of the validation message
+ * @param {'shown' | 'hidden'} state The expected state of the validation message.
+ * Should be passed either 'shown' or 'hidden'
+ */
+ const expectValidationMessageToBe = state => {
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findTagNameFormGroup().element).toHaveClass(
+ state === 'shown' ? 'is-invalid' : 'is-valid',
+ );
+ expect(findTagNameFormGroup().element).not.toHaveClass(
+ state === 'shown' ? 'is-valid' : 'is-invalid',
+ );
+ });
+ };
+
+ describe('when the user has not yet interacted with the component', () => {
+ it('does not display a validation error', () => {
+ findTagNameInput().setValue('');
+
+ return expectValidationMessageToBe('hidden');
+ });
+ });
+
+ describe('when the user has interacted with the component and the value is not empty', () => {
+ it('does not display validation error', () => {
+ findTagNameInput().trigger('blur');
+
+ return expectValidationMessageToBe('hidden');
+ });
+ });
+
+ describe('when the user has interacted with the component and the value is empty', () => {
+ it('displays a validation error', () => {
+ const tagNameInput = findTagNameInput();
+
+ tagNameInput.setValue('');
+ tagNameInput.trigger('blur');
+
+ return expectValidationMessageToBe('shown');
+ });
+ });
+ });
+ });
+
+ describe('"Create from" field', () => {
+ beforeEach(() => createComponent());
+
+ it('renders a label', () => {
+ expect(findCreateFromFormGroup().attributes().label).toBe('Create from');
+ });
+
+ describe('when the user selects a git ref', () => {
+ it("updates the store's createFrom property", () => {
+ const updatedCreateFrom = 'update-create-from';
+ findCreateFromDropdown().vm.$emit('input', updatedCreateFrom);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.state.detail.createFrom).toBe(updatedCreateFrom);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/releases/components/tag_field_spec.js b/spec/frontend/releases/components/tag_field_spec.js
new file mode 100644
index 00000000000..c7909a2369b
--- /dev/null
+++ b/spec/frontend/releases/components/tag_field_spec.js
@@ -0,0 +1,59 @@
+import { shallowMount } from '@vue/test-utils';
+import TagField from '~/releases/components/tag_field.vue';
+import TagFieldNew from '~/releases/components/tag_field_new.vue';
+import TagFieldExisting from '~/releases/components/tag_field_existing.vue';
+import createStore from '~/releases/stores';
+import createDetailModule from '~/releases/stores/modules/detail';
+
+describe('releases/components/tag_field', () => {
+ let store;
+ let wrapper;
+
+ const createComponent = ({ tagName }) => {
+ store = createStore({
+ modules: {
+ detail: createDetailModule({}),
+ },
+ });
+
+ store.state.detail.tagName = tagName;
+
+ wrapper = shallowMount(TagField, { store });
+ };
+
+ const findTagFieldNew = () => wrapper.find(TagFieldNew);
+ const findTagFieldExisting = () => wrapper.find(TagFieldExisting);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when an existing release is being edited', () => {
+ beforeEach(() => {
+ createComponent({ tagName: 'v1.0' });
+ });
+
+ it('renders the TagFieldExisting component', () => {
+ expect(findTagFieldExisting().exists()).toBe(true);
+ });
+
+ it('does not render the TagFieldNew component', () => {
+ expect(findTagFieldNew().exists()).toBe(false);
+ });
+ });
+
+ describe('when a new release is being created', () => {
+ beforeEach(() => {
+ createComponent({ tagName: null });
+ });
+
+ it('renders the TagFieldNew component', () => {
+ expect(findTagFieldNew().exists()).toBe(true);
+ });
+
+ it('does not render the TagFieldExisting component', () => {
+ expect(findTagFieldExisting().exists()).toBe(false);
+ });
+ });
+});