summaryrefslogtreecommitdiff
path: root/spec/frontend
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-04-14 18:09:54 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-04-14 18:09:54 +0000
commitf697dc5e76dfc5894df006d53b2b7e751653cf05 (patch)
tree1387cd225039e611f3683f96b318bb17d4c422cb /spec/frontend
parent874ead9c3a50de4c4ca4551eaf5b7eb976d26b50 (diff)
downloadgitlab-ce-f697dc5e76dfc5894df006d53b2b7e751653cf05.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js14
-rw-r--r--spec/frontend/releases/components/app_edit_spec.js85
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js229
-rw-r--r--spec/frontend/releases/stores/modules/detail/getters_spec.js154
4 files changed, 470 insertions, 12 deletions
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index dc8f6c64136..4969c591dcd 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -224,4 +224,18 @@ describe('text_utility', () => {
});
});
});
+
+ describe('hasContent', () => {
+ it.each`
+ txt | result
+ ${null} | ${false}
+ ${undefined} | ${false}
+ ${{ an: 'object' }} | ${false}
+ ${''} | ${false}
+ ${' \t\r\n'} | ${false}
+ ${'hello'} | ${true}
+ `('returns $result for input $txt', ({ result, txt }) => {
+ expect(textUtils.hasContent(txt)).toEqual(result);
+ });
+ });
});
diff --git a/spec/frontend/releases/components/app_edit_spec.js b/spec/frontend/releases/components/app_edit_spec.js
index bf66f5a5183..09bafe4aa9b 100644
--- a/spec/frontend/releases/components/app_edit_spec.js
+++ b/spec/frontend/releases/components/app_edit_spec.js
@@ -5,14 +5,16 @@ import { release as originalRelease } 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';
describe('Release edit component', () => {
let wrapper;
let release;
let actions;
+ let getters;
let state;
- const factory = (featureFlags = {}) => {
+ const factory = ({ featureFlags = {}, store: storeUpdates = {} } = {}) => {
state = {
release,
markdownDocsPath: 'path/to/markdown/docs',
@@ -26,15 +28,30 @@ describe('Release edit component', () => {
addEmptyAssetLink: jest.fn(),
};
- const store = new Vuex.Store({
- modules: {
- detail: {
- namespaced: true,
- actions,
- state,
+ getters = {
+ isValid: () => true,
+ validationErrors: () => ({
+ assets: {
+ links: [],
},
- },
- });
+ }),
+ };
+
+ const store = new Vuex.Store(
+ merge(
+ {
+ modules: {
+ detail: {
+ namespaced: true,
+ actions,
+ state,
+ getters,
+ },
+ },
+ },
+ storeUpdates,
+ ),
+ );
wrapper = mount(ReleaseEditApp, {
store,
@@ -55,6 +72,8 @@ describe('Release edit component', () => {
wrapper = null;
});
+ const findSubmitButton = () => wrapper.find('button[type=submit]');
+
describe(`basic functionality tests: all tests unrelated to the "${BACK_URL_PARAM}" parameter`, () => {
beforeEach(() => {
factory();
@@ -101,7 +120,7 @@ describe('Release edit component', () => {
});
it('renders the "Save changes" button as type="submit"', () => {
- expect(wrapper.find('.js-submit-button').attributes('type')).toBe('submit');
+ expect(findSubmitButton().attributes('type')).toBe('submit');
});
it('calls updateRelease when the form is submitted', () => {
@@ -143,7 +162,7 @@ describe('Release edit component', () => {
describe('when the release_asset_link_editing feature flag is disabled', () => {
beforeEach(() => {
- factory({ releaseAssetLinkEditing: false });
+ factory({ featureFlags: { releaseAssetLinkEditing: false } });
});
it('does not render the asset links portion of the form', () => {
@@ -153,7 +172,7 @@ describe('Release edit component', () => {
describe('when the release_asset_link_editing feature flag is enabled', () => {
beforeEach(() => {
- factory({ releaseAssetLinkEditing: true });
+ factory({ featureFlags: { releaseAssetLinkEditing: true } });
});
it('renders the asset links portion of the form', () => {
@@ -161,4 +180,46 @@ describe('Release edit component', () => {
});
});
});
+
+ describe('validation', () => {
+ describe('when the form is valid', () => {
+ beforeEach(() => {
+ factory({
+ store: {
+ modules: {
+ detail: {
+ getters: {
+ isValid: () => true,
+ },
+ },
+ },
+ },
+ });
+ });
+
+ it('renders the submit button as enabled', () => {
+ expect(findSubmitButton().attributes('disabled')).toBeUndefined();
+ });
+ });
+
+ describe('when the form is invalid', () => {
+ beforeEach(() => {
+ factory({
+ store: {
+ modules: {
+ detail: {
+ getters: {
+ isValid: () => false,
+ },
+ },
+ },
+ },
+ });
+ });
+
+ it('renders the submit button as disabled', () => {
+ expect(findSubmitButton().attributes('disabled')).toBe('disabled');
+ });
+ });
+ });
});
diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js
new file mode 100644
index 00000000000..44542868cfe
--- /dev/null
+++ b/spec/frontend/releases/components/asset_links_form_spec.js
@@ -0,0 +1,229 @@
+import Vuex from 'vuex';
+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';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Release edit component', () => {
+ let wrapper;
+ let release;
+ let actions;
+ let getters;
+ let state;
+
+ const factory = ({ release: overriddenRelease, linkErrors } = {}) => {
+ state = {
+ release: overriddenRelease || release,
+ releaseAssetsDocsPath: 'path/to/release/assets/docs',
+ };
+
+ actions = {
+ addEmptyAssetLink: jest.fn(),
+ updateAssetLinkUrl: jest.fn(),
+ updateAssetLinkName: jest.fn(),
+ removeAssetLink: jest.fn().mockImplementation((_context, linkId) => {
+ state.release.assets.links = state.release.assets.links.filter(l => l.id !== linkId);
+ }),
+ };
+
+ getters = {
+ validationErrors: () => ({
+ assets: {
+ links: linkErrors || {},
+ },
+ }),
+ };
+
+ const store = new Vuex.Store({
+ modules: {
+ detail: {
+ namespaced: true,
+ actions,
+ state,
+ getters,
+ },
+ },
+ });
+
+ wrapper = mount(AssetLinksForm, {
+ localVue,
+ store,
+ });
+ };
+
+ beforeEach(() => {
+ release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('with a basic store state', () => {
+ beforeEach(() => {
+ factory();
+ });
+
+ it('calls the "addEmptyAssetLink" store method when the "Add another link" button is clicked', () => {
+ expect(actions.addEmptyAssetLink).not.toHaveBeenCalled();
+
+ wrapper.find({ ref: 'addAnotherLinkButton' }).vm.$emit('click');
+
+ expect(actions.addEmptyAssetLink).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls the "removeAssetLinks" store method when the remove button is clicked', () => {
+ expect(actions.removeAssetLink).not.toHaveBeenCalled();
+
+ wrapper.find('.remove-button').vm.$emit('click');
+
+ 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';
+
+ expect(actions.updateAssetLinkUrl).not.toHaveBeenCalled();
+
+ wrapper.find({ ref: 'urlInput' }).vm.$emit('change', newUrl);
+
+ expect(actions.updateAssetLinkUrl).toHaveBeenCalledTimes(1);
+ expect(actions.updateAssetLinkUrl).toHaveBeenCalledWith(
+ expect.anything(),
+ {
+ linkIdToUpdate,
+ newUrl,
+ },
+ undefined,
+ );
+ });
+
+ it('calls the "updateAssetLinName" store method when text is entered into the "Link title" input field', () => {
+ const linkIdToUpdate = release.assets.links[0].id;
+ const newName = 'updated name';
+
+ expect(actions.updateAssetLinkName).not.toHaveBeenCalled();
+
+ wrapper.find({ ref: 'nameInput' }).vm.$emit('change', newName);
+
+ expect(actions.updateAssetLinkName).toHaveBeenCalledTimes(1);
+ expect(actions.updateAssetLinkName).toHaveBeenCalledWith(
+ expect.anything(),
+ {
+ linkIdToUpdate,
+ newName,
+ },
+ undefined,
+ );
+ });
+ });
+
+ describe('validation', () => {
+ let linkId;
+
+ beforeEach(() => {
+ linkId = release.assets.links[0].id;
+ });
+
+ const findUrlValidationMessage = () => wrapper.find('.url-field .invalid-feedback');
+ const findNameValidationMessage = () => wrapper.find('.link-title-field .invalid-feedback');
+
+ it('does not show any validation messages if there are no validation errors', () => {
+ factory();
+
+ expect(findUrlValidationMessage().exists()).toBe(false);
+ expect(findNameValidationMessage().exists()).toBe(false);
+ });
+
+ it('shows a validation error message when two links have the same URLs', () => {
+ factory({
+ linkErrors: {
+ [linkId]: { isDuplicate: true },
+ },
+ });
+
+ expect(findUrlValidationMessage().text()).toBe(
+ 'This URL is already used for another link; duplicate URLs are not allowed',
+ );
+ });
+
+ it('shows a validation error message when a URL has a bad format', () => {
+ factory({
+ linkErrors: {
+ [linkId]: { isBadFormat: true },
+ },
+ });
+
+ expect(findUrlValidationMessage().text()).toBe(
+ 'URL must start with http://, https://, or ftp://',
+ );
+ });
+
+ it('shows a validation error message when the URL is empty (and the title is not empty)', () => {
+ factory({
+ linkErrors: {
+ [linkId]: { isUrlEmpty: true },
+ },
+ });
+
+ expect(findUrlValidationMessage().text()).toBe('URL is required');
+ });
+
+ it('shows a validation error message when the title is empty (and the URL is not empty)', () => {
+ factory({
+ linkErrors: {
+ [linkId]: { isNameEmpty: true },
+ },
+ });
+
+ expect(findNameValidationMessage().text()).toBe('Link title is required');
+ });
+ });
+
+ describe('empty state', () => {
+ describe('when the release fetched from the API has no links', () => {
+ beforeEach(() => {
+ factory({
+ release: {
+ ...release,
+ assets: {
+ links: [],
+ },
+ },
+ });
+ });
+
+ it('calls the addEmptyAssetLink store method when the component is created', () => {
+ expect(actions.addEmptyAssetLink).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when the release fetched from the API has one link', () => {
+ beforeEach(() => {
+ factory({
+ release: {
+ ...release,
+ assets: {
+ links: release.assets.links.slice(0, 1),
+ },
+ },
+ });
+ });
+
+ it('does not call the addEmptyAssetLink store method when the component is created', () => {
+ expect(actions.addEmptyAssetLink).not.toHaveBeenCalled();
+ });
+
+ it('calls addEmptyAssetLink when the final link is deleted by the user', () => {
+ wrapper.find('.remove-button').vm.$emit('click');
+
+ expect(actions.addEmptyAssetLink).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js
index 7dc95c24055..8945ad97c93 100644
--- a/spec/frontend/releases/stores/modules/detail/getters_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js
@@ -56,4 +56,158 @@ describe('Release detail getters', () => {
expect(getters.releaseLinksToDelete(state)).toEqual(originalLinks);
});
});
+
+ describe('validationErrors', () => {
+ describe('when the form is valid', () => {
+ it('returns no validation errors', () => {
+ const state = {
+ release: {
+ assets: {
+ links: [
+ { id: 1, url: 'https://example.com/valid', name: 'Link 1' },
+ { id: 2, url: '', name: '' },
+ { id: 3, url: '', name: ' ' },
+ { id: 4, url: ' ', name: '' },
+ { id: 5, url: ' ', name: ' ' },
+ ],
+ },
+ },
+ };
+
+ const expectedErrors = {
+ assets: {
+ links: {
+ 1: {},
+ 2: {},
+ 3: {},
+ 4: {},
+ 5: {},
+ },
+ },
+ };
+
+ expect(getters.validationErrors(state)).toEqual(expectedErrors);
+ });
+ });
+
+ describe('when the form is invalid', () => {
+ let actualErrors;
+
+ beforeEach(() => {
+ const state = {
+ release: {
+ assets: {
+ links: [
+ // Duplicate URLs
+ { id: 1, url: 'https://example.com/duplicate', name: 'Link 1' },
+ { id: 2, url: 'https://example.com/duplicate', name: 'Link 2' },
+
+ // the validation check ignores leading/trailing
+ // whitespace and is case-insensitive
+ { id: 3, url: ' \tHTTPS://EXAMPLE.COM/DUPLICATE\n\r\n ', name: 'Link 3' },
+
+ // Invalid URL format
+ { id: 4, url: 'invalid', name: 'Link 4' },
+
+ // Missing URL
+ { id: 5, url: '', name: 'Link 5' },
+ { id: 6, url: ' ', name: 'Link 6' },
+
+ // Missing title
+ { id: 7, url: 'https://example.com/valid/1', name: '' },
+ { id: 8, url: 'https://example.com/valid/2', name: ' ' },
+ ],
+ },
+ },
+ };
+
+ actualErrors = getters.validationErrors(state);
+ });
+
+ it('returns a validation errors if links share a URL', () => {
+ const expectedErrors = {
+ assets: {
+ links: {
+ 1: { isDuplicate: true },
+ 2: { isDuplicate: true },
+ 3: { isDuplicate: true },
+ },
+ },
+ };
+
+ expect(actualErrors).toMatchObject(expectedErrors);
+ });
+
+ it('returns a validation error if the URL is in the wrong format', () => {
+ const expectedErrors = {
+ assets: {
+ links: {
+ 4: { isBadFormat: true },
+ },
+ },
+ };
+
+ expect(actualErrors).toMatchObject(expectedErrors);
+ });
+
+ it('returns a validation error if the URL missing (and the title is populated)', () => {
+ const expectedErrors = {
+ assets: {
+ links: {
+ 6: { isUrlEmpty: true },
+ 5: { isUrlEmpty: true },
+ },
+ },
+ };
+
+ expect(actualErrors).toMatchObject(expectedErrors);
+ });
+
+ it('returns a validation error if the title missing (and the URL is populated)', () => {
+ const expectedErrors = {
+ assets: {
+ links: {
+ 7: { isNameEmpty: true },
+ 8: { isNameEmpty: true },
+ },
+ },
+ };
+
+ expect(actualErrors).toMatchObject(expectedErrors);
+ });
+ });
+ });
+
+ describe('isValid', () => {
+ // the value of state is not actually used by this getter
+ const state = {};
+
+ it('returns true when the form is valid', () => {
+ const mockGetters = {
+ validationErrors: {
+ assets: {
+ links: {
+ 1: {},
+ },
+ },
+ },
+ };
+
+ expect(getters.isValid(state, mockGetters)).toBe(true);
+ });
+
+ it('returns false when the form is invalid', () => {
+ const mockGetters = {
+ validationErrors: {
+ assets: {
+ links: {
+ 1: { isNameEmpty: true },
+ },
+ },
+ },
+ };
+
+ expect(getters.isValid(state, mockGetters)).toBe(false);
+ });
+ });
});