diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 19:05:49 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 19:05:49 +0300 |
commit | 43a25d93ebdabea52f99b05e15b06250cd8f07d7 (patch) | |
tree | dceebdc68925362117480a5d672bcff122fb625b /spec/frontend/releases/components | |
parent | 20c84b99005abd1c82101dfeff264ac50d2df211 (diff) |
Add latest changes from gitlab-org/gitlab@16-0-stable-eev16.0.0-rc42
Diffstat (limited to 'spec/frontend/releases/components')
19 files changed, 373 insertions, 247 deletions
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js index bd61e4537f9..69d8969f0ad 100644 --- a/spec/frontend/releases/components/app_edit_new_spec.js +++ b/spec/frontend/releases/components/app_edit_new_spec.js @@ -16,6 +16,7 @@ import AssetLinksForm from '~/releases/components/asset_links_form.vue'; import ConfirmDeleteModal from '~/releases/components/confirm_delete_modal.vue'; import { BACK_URL_PARAM } from '~/releases/constants'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import { ValidationResult } from '~/lib/utils/ref_validator'; const originalRelease = originalOneReleaseForEditingQueryResponse.data.project.release; const originalMilestones = originalRelease.milestones; @@ -30,6 +31,8 @@ describe('Release edit/new component', () => { let actions; let getters; let state; + let refActions; + let refState; let mock; const factory = async ({ featureFlags = {}, store: storeUpdates = {} } = {}) => { @@ -58,8 +61,23 @@ describe('Release edit/new component', () => { assets: { links: [], }, + tagNameValidation: new ValidationResult(), }), formattedReleaseNotes: () => 'these notes are formatted', + isCreating: jest.fn(), + isSearching: jest.fn(), + isExistingTag: jest.fn(), + isNewTag: jest.fn(), + }; + + refState = { + matches: [], + }; + + refActions = { + setEnabledRefTypes: jest.fn(), + setProjectId: jest.fn(), + search: jest.fn(), }; const store = new Vuex.Store( @@ -72,6 +90,11 @@ describe('Release edit/new component', () => { state, getters, }, + ref: { + namespaced: true, + actions: refActions, + state: refState, + }, }, }, storeUpdates, @@ -101,11 +124,6 @@ describe('Release edit/new component', () => { release = convertOneReleaseGraphQLResponse(originalOneReleaseForEditingQueryResponse).data; }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findSubmitButton = () => wrapper.find('button[type=submit]'); const findForm = () => wrapper.find('form'); @@ -291,7 +309,7 @@ describe('Release edit/new component', () => { }); it('renders the submit button as disabled', () => { - expect(findSubmitButton().attributes('disabled')).toBe('disabled'); + expect(findSubmitButton().attributes('disabled')).toBeDefined(); }); it('does not allow the form to be submitted', () => { diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js index ef3bd5ca873..b8507dc5fb4 100644 --- a/spec/frontend/releases/components/app_index_spec.js +++ b/spec/frontend/releases/components/app_index_spec.js @@ -6,9 +6,8 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql'; -import { createAlert } from '~/flash'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; import { historyPushState } from '~/lib/utils/common_utils'; -import { sprintf, __ } from '~/locale'; import ReleasesIndexApp from '~/releases/components/app_index.vue'; import ReleaseBlock from '~/releases/components/release_block.vue'; import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue'; @@ -16,11 +15,11 @@ import ReleasesEmptyState from '~/releases/components/releases_empty_state.vue'; import ReleasesPagination from '~/releases/components/releases_pagination.vue'; import ReleasesSort from '~/releases/components/releases_sort.vue'; import { PAGE_SIZE, CREATED_ASC, DEFAULT_SORT } from '~/releases/constants'; -import { deleteReleaseSessionKey } from '~/releases/util'; +import { deleteReleaseSessionKey } from '~/releases/release_notification_service'; Vue.use(VueApollo); -jest.mock('~/flash'); +jest.mock('~/alert'); let mockQueryParams; jest.mock('~/lib/utils/common_utils', () => ({ @@ -114,7 +113,7 @@ describe('app_index.vue', () => { const toDescription = (bool) => (bool ? 'does' : 'does not'); describe.each` - description | singleResponseFn | fullResponseFn | loadingIndicator | emptyState | flashMessage | releaseCount | pagination + description | singleResponseFn | fullResponseFn | loadingIndicator | emptyState | alertMessage | releaseCount | pagination ${'both requests loading'} | ${getInProgressResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${0} | ${false} ${'both requests failed'} | ${getErrorResponse} | ${getErrorResponse} | ${false} | ${false} | ${true} | ${0} | ${false} ${'both requests loaded'} | ${getSingleRequestLoadedResponse} | ${getFullRequestLoadedResponse} | ${false} | ${false} | ${false} | ${2} | ${true} @@ -134,7 +133,7 @@ describe('app_index.vue', () => { fullResponseFn, loadingIndicator, emptyState, - flashMessage, + alertMessage, releaseCount, pagination, }) => { @@ -154,9 +153,9 @@ describe('app_index.vue', () => { expect(findEmptyState().exists()).toBe(emptyState); }); - it(`${toDescription(flashMessage)} show a flash message`, async () => { + it(`${toDescription(alertMessage)} show a flash message`, async () => { await waitForPromises(); - if (flashMessage) { + if (alertMessage) { expect(createAlert).toHaveBeenCalledWith({ message: ReleasesIndexApp.i18n.errorMessage, captureError: true, @@ -412,15 +411,15 @@ describe('app_index.vue', () => { await createComponent(); }); - it('shows a toast', async () => { - expect(toast).toHaveBeenCalledWith( - sprintf(__('Release %{release} has been successfully deleted.'), { - release, - }), - ); + it('shows a toast', () => { + expect(createAlert).toHaveBeenCalledTimes(1); + expect(createAlert).toHaveBeenCalledWith({ + message: `Release ${release} has been successfully deleted.`, + variant: VARIANT_SUCCESS, + }); }); - it('clears session storage', async () => { + it('clears session storage', () => { expect(window.sessionStorage.getItem(key)).toBe(null); }); }); diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js index efe72e8000a..942280cb6a2 100644 --- a/spec/frontend/releases/components/app_show_spec.js +++ b/spec/frontend/releases/components/app_show_spec.js @@ -4,14 +4,14 @@ import VueApollo from 'vue-apollo'; import oneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release.query.graphql.json'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { popCreateReleaseNotification } from '~/releases/release_notification_service'; import ReleaseShowApp from '~/releases/components/app_show.vue'; import ReleaseBlock from '~/releases/components/release_block.vue'; import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue'; import oneReleaseQuery from '~/releases/graphql/queries/one_release.query.graphql'; -jest.mock('~/flash'); +jest.mock('~/alert'); jest.mock('~/releases/release_notification_service'); Vue.use(VueApollo); @@ -33,11 +33,6 @@ describe('Release show component', () => { }); }; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const findLoadingSkeleton = () => wrapper.findComponent(ReleaseSkeletonLoader); const findReleaseBlock = () => wrapper.findComponent(ReleaseBlock); @@ -54,13 +49,13 @@ describe('Release show component', () => { }; const expectNoFlash = () => { - it('does not show a flash message', () => { + it('does not show an alert message', () => { expect(createAlert).not.toHaveBeenCalled(); }); }; const expectFlashWithMessage = (message) => { - it(`shows a flash message that reads "${message}"`, () => { + it(`shows an alert message that reads "${message}"`, () => { expect(createAlert).toHaveBeenCalledWith({ message, captureError: true, @@ -152,7 +147,7 @@ describe('Release show component', () => { beforeEach(async () => { // As we return a release as `null`, Apollo also throws an error to the console // about the missing field. We need to suppress console.error in order to check - // that flash message was called + // that alert message was called // eslint-disable-next-line no-console console.error = jest.fn(); diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js index b1e9d8d1256..8eee9acd808 100644 --- a/spec/frontend/releases/components/asset_links_form_spec.js +++ b/spec/frontend/releases/components/asset_links_form_spec.js @@ -60,11 +60,6 @@ describe('Release edit component', () => { release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('with a basic store state', () => { beforeEach(() => { factory(); diff --git a/spec/frontend/releases/components/confirm_delete_modal_spec.js b/spec/frontend/releases/components/confirm_delete_modal_spec.js index f7c526c1ced..b4699302779 100644 --- a/spec/frontend/releases/components/confirm_delete_modal_spec.js +++ b/spec/frontend/releases/components/confirm_delete_modal_spec.js @@ -42,10 +42,6 @@ describe('~/releases/components/confirm_delete_modal.vue', () => { factory(); }); - afterEach(() => { - wrapper.destroy(); - }); - describe('button', () => { it('should open the modal on click', async () => { await wrapper.findByRole('button', { name: 'Delete' }).trigger('click'); diff --git a/spec/frontend/releases/components/evidence_block_spec.js b/spec/frontend/releases/components/evidence_block_spec.js index 69443cb7a11..42eac31e5ac 100644 --- a/spec/frontend/releases/components/evidence_block_spec.js +++ b/spec/frontend/releases/components/evidence_block_spec.js @@ -27,10 +27,6 @@ describe('Evidence Block', () => { }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders the evidence icon', () => { expect(wrapper.findComponent(GlIcon).props('name')).toBe('review-list'); }); diff --git a/spec/frontend/releases/components/issuable_stats_spec.js b/spec/frontend/releases/components/issuable_stats_spec.js index 3ac75e138ee..c8cdf9cb951 100644 --- a/spec/frontend/releases/components/issuable_stats_spec.js +++ b/spec/frontend/releases/components/issuable_stats_spec.js @@ -34,11 +34,6 @@ describe('~/releases/components/issuable_stats.vue', () => { }; }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('matches snapshot', () => { createComponent(); diff --git a/spec/frontend/releases/components/release_block_assets_spec.js b/spec/frontend/releases/components/release_block_assets_spec.js index 6d53bf5a49e..8332e307ce9 100644 --- a/spec/frontend/releases/components/release_block_assets_spec.js +++ b/spec/frontend/releases/components/release_block_assets_spec.js @@ -124,13 +124,12 @@ describe('Release block assets', () => { }); describe('links', () => { - const containsExternalSourceIndicator = () => - wrapper.find('[data-testid="external-link-indicator"]').exists(); + const findAllExternalIcons = () => wrapper.findAll('[data-testid="external-link-indicator"]'); beforeEach(() => createComponent(defaultProps)); - it('renders with an external source indicator (except for sections with no title)', () => { - expect(containsExternalSourceIndicator()).toBe(true); + it('renders with an external source indicator', () => { + expect(findAllExternalIcons()).toHaveLength(defaultProps.assets.count); }); }); }); diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js index 19b41d05a44..12e3807c9fa 100644 --- a/spec/frontend/releases/components/release_block_footer_spec.js +++ b/spec/frontend/releases/components/release_block_footer_spec.js @@ -33,11 +33,6 @@ describe('Release block footer', () => { release = cloneDeep(originalRelease); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const commitInfoSection = () => wrapper.find('.js-commit-info'); const commitInfoSectionLink = () => commitInfoSection().findComponent(GlLink); const tagInfoSection = () => wrapper.find('.js-tag-info'); diff --git a/spec/frontend/releases/components/release_block_header_spec.js b/spec/frontend/releases/components/release_block_header_spec.js index fc421776d60..dd39a1bce53 100644 --- a/spec/frontend/releases/components/release_block_header_spec.js +++ b/spec/frontend/releases/components/release_block_header_spec.js @@ -25,10 +25,6 @@ describe('Release block header', () => { release = convertObjectPropsToCamelCase(originalRelease, { deep: true }); }); - afterEach(() => { - wrapper.destroy(); - }); - const findHeader = () => wrapper.find('h2'); const findHeaderLink = () => findHeader().findComponent(GlLink); const findEditButton = () => wrapper.find('.js-edit-button'); diff --git a/spec/frontend/releases/components/release_block_milestone_info_spec.js b/spec/frontend/releases/components/release_block_milestone_info_spec.js index 541d487091c..b8030ae1fd2 100644 --- a/spec/frontend/releases/components/release_block_milestone_info_spec.js +++ b/spec/frontend/releases/components/release_block_milestone_info_spec.js @@ -25,11 +25,6 @@ describe('Release block milestone info', () => { milestones = convertObjectPropsToCamelCase(originalMilestones, { deep: true }); }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - const milestoneProgressBarContainer = () => wrapper.find('.js-milestone-progress-bar-container'); const milestoneListContainer = () => wrapper.find('.js-milestone-list-container'); const issuesContainer = () => wrapper.find('[data-testid="issue-stats"]'); diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js index f1b8554fbc3..3355b5ab2c3 100644 --- a/spec/frontend/releases/components/release_block_spec.js +++ b/spec/frontend/releases/components/release_block_spec.js @@ -39,10 +39,6 @@ describe('Release block', () => { release = convertOneReleaseGraphQLResponse(originalOneReleaseQueryResponse).data; }); - afterEach(() => { - wrapper.destroy(); - }); - describe('with default props', () => { beforeEach(() => factory(release)); diff --git a/spec/frontend/releases/components/releases_pagination_spec.js b/spec/frontend/releases/components/releases_pagination_spec.js index 59be808c802..923d84ae2b3 100644 --- a/spec/frontend/releases/components/releases_pagination_spec.js +++ b/spec/frontend/releases/components/releases_pagination_spec.js @@ -29,10 +29,6 @@ describe('releases_pagination.vue', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const singlePageInfo = { hasPreviousPage: false, hasNextPage: false, diff --git a/spec/frontend/releases/components/releases_sort_spec.js b/spec/frontend/releases/components/releases_sort_spec.js index c6e1846d252..76907b4b8bb 100644 --- a/spec/frontend/releases/components/releases_sort_spec.js +++ b/spec/frontend/releases/components/releases_sort_spec.js @@ -1,5 +1,6 @@ import { GlSorting, GlSortingItem } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { assertProps } from 'helpers/assert_props'; import ReleasesSort from '~/releases/components/releases_sort.vue'; import { RELEASED_AT_ASC, RELEASED_AT_DESC, CREATED_ASC, CREATED_DESC } from '~/releases/constants'; @@ -17,10 +18,6 @@ describe('releases_sort.vue', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - const findSorting = () => wrapper.findComponent(GlSorting); const findSortingItems = () => wrapper.findAllComponents(GlSortingItem); const findReleasedDateItem = () => @@ -96,7 +93,7 @@ describe('releases_sort.vue', () => { describe('prop validation', () => { it('validates that the `value` prop is one of the expected sort strings', () => { expect(() => { - createComponent('not a valid value'); + assertProps(ReleasesSort, { value: 'not a valid value' }); }).toThrow('Invalid prop: custom validator check failed'); }); }); diff --git a/spec/frontend/releases/components/tag_create_spec.js b/spec/frontend/releases/components/tag_create_spec.js new file mode 100644 index 00000000000..0df2483bcf2 --- /dev/null +++ b/spec/frontend/releases/components/tag_create_spec.js @@ -0,0 +1,107 @@ +import { GlButton, GlFormInput, GlFormTextarea } from '@gitlab/ui'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import { __, s__ } from '~/locale'; +import TagCreate from '~/releases/components/tag_create.vue'; +import RefSelector from '~/ref/components/ref_selector.vue'; +import createStore from '~/releases/stores'; +import createEditNewModule from '~/releases/stores/modules/edit_new'; +import { createRefModule } from '~/ref/stores'; + +const TEST_PROJECT_ID = '1234'; + +const VALUE = 'new-tag'; + +describe('releases/components/tag_create', () => { + let store; + let wrapper; + let mock; + + const createWrapper = () => { + wrapper = shallowMount(TagCreate, { + store, + propsData: { value: VALUE }, + }); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + store = createStore({ + modules: { + editNew: createEditNewModule({ + projectId: TEST_PROJECT_ID, + }), + ref: createRefModule(), + }, + }); + store.state.editNew.release = { + tagMessage: 'test', + }; + store.state.editNew.createFrom = 'v1'; + createWrapper(); + }); + + afterEach(() => mock.restore()); + + const findTagInput = () => wrapper.findComponent(GlFormInput); + const findTagRef = () => wrapper.findComponent(RefSelector); + const findTagMessage = () => wrapper.findComponent(GlFormTextarea); + const findSave = () => wrapper.findAllComponents(GlButton).at(-2); + const findCancel = () => wrapper.findAllComponents(GlButton).at(-1); + + it('initializes the input with value prop', () => { + expect(findTagInput().attributes('value')).toBe(VALUE); + }); + + it('emits a change event when the tag name chagnes', () => { + findTagInput().vm.$emit('input', 'new-value'); + + expect(wrapper.emitted('change')).toEqual([['new-value']]); + }); + + it('binds the store value to the ref selector', () => { + const ref = findTagRef(); + expect(ref.props('value')).toBe('v1'); + + ref.vm.$emit('input', 'v2'); + + expect(ref.props('value')).toBe('v1'); + }); + + it('passes the project id tot he ref selector', () => { + expect(findTagRef().props('projectId')).toBe(TEST_PROJECT_ID); + }); + + it('binds the store value to the message', async () => { + const message = findTagMessage(); + expect(message.attributes('value')).toBe('test'); + + message.vm.$emit('input', 'hello'); + + await nextTick(); + + expect(message.attributes('value')).toBe('hello'); + }); + + it('emits create event when Save is clicked', () => { + const button = findSave(); + + expect(button.text()).toBe(__('Save')); + + button.vm.$emit('click'); + + expect(wrapper.emitted('create')).toEqual([[]]); + }); + + it('emits cancel event when Select another tag is clicked', () => { + const button = findCancel(); + + expect(button.text()).toBe(s__('Release|Select another tag')); + + button.vm.$emit('click'); + + expect(wrapper.emitted('cancel')).toEqual([[]]); + }); +}); diff --git a/spec/frontend/releases/components/tag_field_exsting_spec.js b/spec/frontend/releases/components/tag_field_exsting_spec.js index 8105aa4f6f2..0e896eb645c 100644 --- a/spec/frontend/releases/components/tag_field_exsting_spec.js +++ b/spec/frontend/releases/components/tag_field_exsting_spec.js @@ -37,11 +37,6 @@ describe('releases/components/tag_field_existing', () => { }; }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('default', () => { it('shows the tag name', () => { createComponent(); diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js index fcba0da3462..3468338b8a7 100644 --- a/spec/frontend/releases/components/tag_field_new_spec.js +++ b/spec/frontend/releases/components/tag_field_new_spec.js @@ -1,17 +1,19 @@ -import { GlDropdownItem, GlFormGroup, GlSprintf } from '@gitlab/ui'; -import { mount, shallowMount } from '@vue/test-utils'; +import { GlFormGroup, GlDropdown, GlPopover } from '@gitlab/ui'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import Vue, { nextTick } from 'vue'; -import { trimText } from 'helpers/text_helper'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { __ } from '~/locale'; import TagFieldNew from '~/releases/components/tag_field_new.vue'; +import TagSearch from '~/releases/components/tag_search.vue'; +import TagCreate from '~/releases/components/tag_create.vue'; import createStore from '~/releases/stores'; import createEditNewModule from '~/releases/stores/modules/edit_new'; +import { CREATE } from '~/releases/stores/modules/edit_new/constants'; +import { createRefModule } from '~/ref/stores'; +import { i18n } from '~/releases/constants'; const TEST_TAG_NAME = 'test-tag-name'; -const TEST_TAG_MESSAGE = 'Test tag message'; const TEST_PROJECT_ID = '1234'; const TEST_CREATE_FROM = 'test-create-from'; const NONEXISTENT_TAG_NAME = 'nonexistent-tag'; @@ -20,38 +22,12 @@ describe('releases/components/tag_field_new', () => { let store; let wrapper; let mock; - let RefSelectorStub; - - const createComponent = ( - mountFn = shallowMount, - { searchQuery } = { searchQuery: NONEXISTENT_TAG_NAME }, - ) => { - // A mock version of the RefSelector component that just renders the - // #footer slot, so that the content inside this slot can be tested. - RefSelectorStub = Vue.component('RefSelectorStub', { - data() { - return { - footerSlotProps: { - isLoading: false, - matches: { - tags: { - totalCount: 1, - list: [{ name: TEST_TAG_NAME }], - }, - }, - query: searchQuery, - }, - }; - }, - template: '<div><slot name="footer" v-bind="footerSlotProps"></slot></div>', - }); - wrapper = mountFn(TagFieldNew, { + const createComponent = () => { + wrapper = shallowMountExtended(TagFieldNew, { store, stubs: { - RefSelector: RefSelectorStub, GlFormGroup, - GlSprintf, }, }); }; @@ -62,11 +38,12 @@ describe('releases/components/tag_field_new', () => { editNew: createEditNewModule({ projectId: TEST_PROJECT_ID, }), + ref: createRefModule(), }, }); store.state.editNew.createFrom = TEST_CREATE_FROM; - store.state.editNew.showCreateFrom = true; + store.state.editNew.step = CREATE; store.state.editNew.release = { tagName: TEST_TAG_NAME, @@ -80,21 +57,13 @@ describe('releases/components/tag_field_new', () => { gon.api_version = 'v4'; }); - afterEach(() => { - wrapper.destroy(); - mock.restore(); - }); - - const findTagNameFormGroup = () => wrapper.find('[data-testid="tag-name-field"]'); - const findTagNameDropdown = () => findTagNameFormGroup().findComponent(RefSelectorStub); + afterEach(() => mock.restore()); - const findCreateFromFormGroup = () => wrapper.find('[data-testid="create-from-field"]'); - const findCreateFromDropdown = () => findCreateFromFormGroup().findComponent(RefSelectorStub); - - const findCreateNewTagOption = () => wrapper.findComponent(GlDropdownItem); - - const findAnnotatedTagMessageFormGroup = () => - wrapper.find('[data-testid="annotated-tag-message-field"]'); + const findTagNameFormGroup = () => wrapper.findComponent(GlFormGroup); + const findTagNameInput = () => wrapper.findComponent(GlDropdown); + const findTagNamePopover = () => wrapper.findComponent(GlPopover); + const findTagNameSearch = () => wrapper.findComponent(TagSearch); + const findTagNameCreate = () => wrapper.findComponent(TagCreate); describe('"Tag name" field', () => { describe('rendering and behavior', () => { @@ -102,20 +71,37 @@ describe('releases/components/tag_field_new', () => { it('renders a label', () => { expect(findTagNameFormGroup().attributes().label).toBe(__('Tag name')); - expect(findTagNameFormGroup().props().labelDescription).toBe(__('*Required')); + expect(findTagNameFormGroup().props().optionalText).toBe(__('(required)')); + }); + + it('flips between search and create, passing the searched value', async () => { + let create = findTagNameCreate(); + let search = findTagNameSearch(); + + expect(create.exists()).toBe(true); + expect(search.exists()).toBe(false); + + await create.vm.$emit('cancel'); + + search = findTagNameSearch(); + expect(create.exists()).toBe(false); + expect(search.exists()).toBe(true); + + await search.vm.$emit('create', TEST_TAG_NAME); + + create = findTagNameCreate(); + expect(create.exists()).toBe(true); + expect(create.props('value')).toBe(TEST_TAG_NAME); + expect(search.exists()).toBe(false); }); describe('when the user selects a new tag name', () => { - beforeEach(async () => { - findCreateNewTagOption().vm.$emit('click'); - }); + it("updates the store's release.tagName property", async () => { + findTagNameCreate().vm.$emit('change', NONEXISTENT_TAG_NAME); + await findTagNameCreate().vm.$emit('create'); - it("updates the store's release.tagName property", () => { expect(store.state.editNew.release.tagName).toBe(NONEXISTENT_TAG_NAME); - }); - - it('hides the "Create from" field', () => { - expect(findCreateFromFormGroup().exists()).toBe(true); + expect(findTagNameInput().props('text')).toBe(NONEXISTENT_TAG_NAME); }); }); @@ -123,19 +109,17 @@ describe('releases/components/tag_field_new', () => { const updatedTagName = 'updated-tag-name'; beforeEach(async () => { - findTagNameDropdown().vm.$emit('input', updatedTagName); + await findTagNameCreate().vm.$emit('cancel'); + findTagNameSearch().vm.$emit('select', updatedTagName); }); it("updates the store's release.tagName property", () => { expect(store.state.editNew.release.tagName).toBe(updatedTagName); + expect(findTagNameInput().props('text')).toBe(updatedTagName); }); it('hides the "Create from" field', () => { - expect(findCreateFromFormGroup().exists()).toBe(false); - }); - - it('hides the "Tag message" field', () => { - expect(findAnnotatedTagMessageFormGroup().exists()).toBe(false); + expect(findTagNameCreate().exists()).toBe(false); }); it('fetches the release notes for the tag', () => { @@ -145,133 +129,66 @@ describe('releases/components/tag_field_new', () => { }); }); - describe('"Create tag" option', () => { - describe('when the search query exactly matches one of the search results', () => { - beforeEach(async () => { - createComponent(mount, { searchQuery: TEST_TAG_NAME }); - }); - - it('does not show the "Create tag" option', () => { - expect(findCreateNewTagOption().exists()).toBe(false); - }); - }); - - describe('when the search query does not exactly match one of the search results', () => { - beforeEach(async () => { - createComponent(mount, { searchQuery: NONEXISTENT_TAG_NAME }); - }); - - it('shows the "Create tag" option', () => { - expect(findCreateNewTagOption().exists()).toBe(true); - }); - }); - }); - describe('validation', () => { beforeEach(() => { - createComponent(mount); + createComponent(); + findTagNameCreate().vm.$emit('cancel'); }); /** * 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' + * @param {boolean} isShown Whether or not the message is shown. */ - const expectValidationMessageToBe = async (state) => { + const expectValidationMessageToBeShown = async (isShown) => { await nextTick(); - expect(findTagNameFormGroup().element).toHaveClass( - state === 'shown' ? 'is-invalid' : 'is-valid', - ); - expect(findTagNameFormGroup().element).not.toHaveClass( - state === 'shown' ? 'is-valid' : 'is-invalid', - ); + const state = findTagNameFormGroup().attributes('state'); + + if (isShown) { + expect(state).toBeUndefined(); + } else { + expect(state).toBe('true'); + } }; describe('when the user has not yet interacted with the component', () => { it('does not display a validation error', async () => { - findTagNameDropdown().vm.$emit('input', ''); - - await expectValidationMessageToBe('hidden'); + await expectValidationMessageToBeShown(false); }); }); describe('when the user has interacted with the component and the value is not empty', () => { it('does not display validation error', async () => { - findTagNameDropdown().vm.$emit('hide'); + findTagNameSearch().vm.$emit('select', 'vTest'); + findTagNamePopover().vm.$emit('hide'); - await expectValidationMessageToBe('hidden'); + await expectValidationMessageToBeShown(false); }); it('displays a validation error if the tag has an associated release', async () => { - findTagNameDropdown().vm.$emit('input', 'vTest'); - findTagNameDropdown().vm.$emit('hide'); + findTagNameSearch().vm.$emit('select', 'vTest'); + findTagNamePopover().vm.$emit('hide'); store.state.editNew.existingRelease = {}; - await expectValidationMessageToBe('shown'); - expect(findTagNameFormGroup().text()).toContain( - __('Selected tag is already in use. Choose another option.'), + await expectValidationMessageToBeShown(true); + expect(findTagNameFormGroup().attributes('invalidfeedback')).toBe( + i18n.tagIsAlredyInUseMessage, ); }); }); describe('when the user has interacted with the component and the value is empty', () => { it('displays a validation error', async () => { - findTagNameDropdown().vm.$emit('input', ''); - findTagNameDropdown().vm.$emit('hide'); + findTagNameSearch().vm.$emit('select', ''); + findTagNamePopover().vm.$emit('hide'); - await expectValidationMessageToBe('shown'); - expect(findTagNameFormGroup().text()).toContain(__('Tag name is required.')); + await expectValidationMessageToBeShown(true); + expect(findTagNameFormGroup().attributes('invalidfeedback')).toContain( + i18n.tagNameIsRequiredMessage, + ); }); }); }); }); - - 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", async () => { - const updatedCreateFrom = 'update-create-from'; - findCreateFromDropdown().vm.$emit('input', updatedCreateFrom); - - expect(store.state.editNew.createFrom).toBe(updatedCreateFrom); - }); - }); - }); - - describe('"Annotated Tag" field', () => { - beforeEach(() => { - createComponent(mountExtended); - }); - - it('renders a label', () => { - expect(wrapper.findByRole('textbox', { name: 'Set tag message' }).exists()).toBe(true); - }); - - it('renders a description', () => { - expect(trimText(findAnnotatedTagMessageFormGroup().text())).toContain( - 'Add a message to the tag. Leaving this blank creates a lightweight tag.', - ); - }); - - it('updates the store', async () => { - await findAnnotatedTagMessageFormGroup().find('textarea').setValue(TEST_TAG_MESSAGE); - - expect(store.state.editNew.release.tagMessage).toBe(TEST_TAG_MESSAGE); - }); - - it('shows a link', () => { - const link = wrapper.findByRole('link', { - name: 'lightweight tag', - }); - - expect(link.attributes('href')).toBe('https://git-scm.com/book/en/v2/Git-Basics-Tagging/'); - }); - }); }); diff --git a/spec/frontend/releases/components/tag_field_spec.js b/spec/frontend/releases/components/tag_field_spec.js index 85a40f02c53..8509c347291 100644 --- a/spec/frontend/releases/components/tag_field_spec.js +++ b/spec/frontend/releases/components/tag_field_spec.js @@ -24,11 +24,6 @@ describe('releases/components/tag_field', () => { const findTagFieldNew = () => wrapper.findComponent(TagFieldNew); const findTagFieldExisting = () => wrapper.findComponent(TagFieldExisting); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - describe('when an existing release is being edited', () => { beforeEach(() => { createComponent({ isExistingRelease: true }); diff --git a/spec/frontend/releases/components/tag_search_spec.js b/spec/frontend/releases/components/tag_search_spec.js new file mode 100644 index 00000000000..4144a9cc297 --- /dev/null +++ b/spec/frontend/releases/components/tag_search_spec.js @@ -0,0 +1,144 @@ +import { GlButton, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; +import { mount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import { DEFAULT_PER_PAGE } from '~/api'; +import { __, s__, sprintf } from '~/locale'; +import TagSearch from '~/releases/components/tag_search.vue'; +import createStore from '~/releases/stores'; +import createEditNewModule from '~/releases/stores/modules/edit_new'; +import { createRefModule } from '~/ref/stores'; + +const TEST_TAG_NAME = 'test-tag-name'; +const TEST_PROJECT_ID = '1234'; +const TAGS = [{ name: 'v1' }, { name: 'v2' }, { name: 'v3' }]; + +describe('releases/components/tag_search', () => { + let store; + let wrapper; + let mock; + + const createWrapper = (propsData = {}) => { + wrapper = mount(TagSearch, { + store, + propsData, + }); + }; + + beforeEach(() => { + store = createStore({ + modules: { + editNew: createEditNewModule({ + projectId: TEST_PROJECT_ID, + }), + ref: createRefModule(), + }, + }); + + store.state.editNew.release = {}; + + mock = new MockAdapter(axios); + gon.api_version = 'v4'; + }); + + afterEach(() => mock.restore()); + + const findSearch = () => wrapper.findComponent(GlSearchBoxByType); + const findCreate = () => wrapper.findAllComponents(GlButton).at(-1); + const findResults = () => wrapper.findAllComponents(GlDropdownItem); + + describe('init', () => { + beforeEach(async () => { + mock + .onGet(`/api/v4/projects/${TEST_PROJECT_ID}/repository/tags`) + .reply(200, TAGS, { 'x-total': TAGS.length }); + + createWrapper(); + + await waitForPromises(); + }); + + it('displays a set of results immediately', () => { + findResults().wrappers.forEach((w, i) => expect(w.text()).toBe(TAGS[i].name)); + }); + + it('has a disabled button', () => { + const button = findCreate(); + expect(button.text()).toBe(s__('Release|Or type a new tag name')); + expect(button.props('disabled')).toBe(true); + }); + + it('has an empty search input', () => { + expect(findSearch().props('value')).toBe(''); + }); + + describe('searching', () => { + const query = TEST_TAG_NAME; + + beforeEach(async () => { + mock.reset(); + mock + .onGet(`/api/v4/projects/${TEST_PROJECT_ID}/repository/tags`, { + params: { search: query, per_page: DEFAULT_PER_PAGE }, + }) + .reply(200, [], { 'x-total': 0 }); + + findSearch().vm.$emit('input', query); + + await nextTick(); + await waitForPromises(); + }); + + it('shows "No results found" when there are no results', () => { + expect(wrapper.text()).toContain(__('No results found')); + }); + + it('searches with the given input', () => { + expect(mock.history.get[0].params.search).toBe(query); + }); + + it('emits the query', () => { + expect(wrapper.emitted('change')).toEqual([[query]]); + }); + }); + }); + + describe('with query', () => { + const query = TEST_TAG_NAME; + + beforeEach(async () => { + mock + .onGet(`/api/v4/projects/${TEST_PROJECT_ID}/repository/tags`, { + params: { search: query, per_page: DEFAULT_PER_PAGE }, + }) + .reply(200, TAGS, { 'x-total': TAGS.length }); + + createWrapper({ query }); + + await waitForPromises(); + }); + + it('displays a set of results immediately', () => { + findResults().wrappers.forEach((w, i) => expect(w.text()).toBe(TAGS[i].name)); + }); + + it('has an enabled button', () => { + const button = findCreate(); + expect(button.text()).toMatchInterpolatedText( + sprintf(s__('Release|Create tag %{tag}'), { tag: query }), + ); + expect(button.props('disabled')).toBe(false); + }); + + it('emits create event when button clicked', () => { + findCreate().vm.$emit('click'); + expect(wrapper.emitted('create')).toEqual([[query]]); + }); + + it('has an empty search input', () => { + expect(findSearch().props('value')).toBe(query); + }); + }); +}); |