diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-14 18:09:54 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-14 18:09:54 +0000 |
commit | f697dc5e76dfc5894df006d53b2b7e751653cf05 (patch) | |
tree | 1387cd225039e611f3683f96b318bb17d4c422cb /spec/frontend | |
parent | 874ead9c3a50de4c4ca4551eaf5b7eb976d26b50 (diff) | |
download | gitlab-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.js | 14 | ||||
-rw-r--r-- | spec/frontend/releases/components/app_edit_spec.js | 85 | ||||
-rw-r--r-- | spec/frontend/releases/components/asset_links_form_spec.js | 229 | ||||
-rw-r--r-- | spec/frontend/releases/stores/modules/detail/getters_spec.js | 154 |
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); + }); + }); }); |