Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 19:05:49 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 19:05:49 +0300
commit43a25d93ebdabea52f99b05e15b06250cd8f07d7 (patch)
treedceebdc68925362117480a5d672bcff122fb625b /spec/frontend/releases/components
parent20c84b99005abd1c82101dfeff264ac50d2df211 (diff)
Add latest changes from gitlab-org/gitlab@16-0-stable-eev16.0.0-rc42
Diffstat (limited to 'spec/frontend/releases/components')
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js30
-rw-r--r--spec/frontend/releases/components/app_index_spec.js29
-rw-r--r--spec/frontend/releases/components/app_show_spec.js15
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js5
-rw-r--r--spec/frontend/releases/components/confirm_delete_modal_spec.js4
-rw-r--r--spec/frontend/releases/components/evidence_block_spec.js4
-rw-r--r--spec/frontend/releases/components/issuable_stats_spec.js5
-rw-r--r--spec/frontend/releases/components/release_block_assets_spec.js7
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js5
-rw-r--r--spec/frontend/releases/components/release_block_header_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_milestone_info_spec.js5
-rw-r--r--spec/frontend/releases/components/release_block_spec.js4
-rw-r--r--spec/frontend/releases/components/releases_pagination_spec.js4
-rw-r--r--spec/frontend/releases/components/releases_sort_spec.js7
-rw-r--r--spec/frontend/releases/components/tag_create_spec.js107
-rw-r--r--spec/frontend/releases/components/tag_field_exsting_spec.js5
-rw-r--r--spec/frontend/releases/components/tag_field_new_spec.js231
-rw-r--r--spec/frontend/releases/components/tag_field_spec.js5
-rw-r--r--spec/frontend/releases/components/tag_search_spec.js144
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);
+ });
+ });
+});