diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-13 18:07:56 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-13 18:07:56 +0300 |
commit | 0d55697d64b5f053bbd0f69da2962e7478097de3 (patch) | |
tree | 33dc75892313554223fb7dadd88e1c8875053d88 /spec/frontend | |
parent | 9fdb3dbd6bacb125d40290aac8409da2f9fe19fc (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend')
13 files changed, 477 insertions, 71 deletions
diff --git a/spec/frontend/__helpers__/graphql_helpers.js b/spec/frontend/__helpers__/graphql_helpers.js deleted file mode 100644 index 63123aa046f..00000000000 --- a/spec/frontend/__helpers__/graphql_helpers.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Returns a clone of the given object with all __typename keys omitted, - * including deeply nested ones. - * - * Only works with JSON-serializable objects. - * - * @param {object} An object with __typename keys (e.g., a GraphQL response) - * @returns {object} A new object with no __typename keys - */ -export const stripTypenames = (object) => { - return JSON.parse( - JSON.stringify(object, (key, value) => (key === '__typename' ? undefined : value)), - ); -}; diff --git a/spec/frontend/__helpers__/graphql_helpers_spec.js b/spec/frontend/__helpers__/graphql_helpers_spec.js deleted file mode 100644 index dd23fbbf4e9..00000000000 --- a/spec/frontend/__helpers__/graphql_helpers_spec.js +++ /dev/null @@ -1,23 +0,0 @@ -import { stripTypenames } from './graphql_helpers'; - -describe('stripTypenames', () => { - it.each` - input | expected - ${{}} | ${{}} - ${{ __typename: 'Foo' }} | ${{}} - ${{ bar: 'bar', __typename: 'Foo' }} | ${{ bar: 'bar' }} - ${{ bar: { __typename: 'Bar' }, __typename: 'Foo' }} | ${{ bar: {} }} - ${{ bar: [{ __typename: 'Bar' }], __typename: 'Foo' }} | ${{ bar: [{}] }} - ${[]} | ${[]} - ${[{ __typename: 'Foo' }]} | ${[{}]} - ${[{ bar: [{ a: 1, __typename: 'Bar' }] }]} | ${[{ bar: [{ a: 1 }] }]} - `('given $input returns $expected, with all __typename keys removed', ({ input, expected }) => { - const actual = stripTypenames(input); - expect(actual).toEqual(expected); - expect(input).not.toBe(actual); - }); - - it('given null returns null', () => { - expect(stripTypenames(null)).toEqual(null); - }); -}); diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index 87366cdbfc5..9e0ffbf757f 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -606,6 +606,50 @@ describe('DiffsStoreActions', () => { params: { commit_id: '123', w: '0' }, }); }); + + describe('version parameters', () => { + const diffId = '4'; + const startSha = 'abc'; + const pathRoot = 'a/a/-/merge_requests/1'; + let file; + let getters; + + beforeAll(() => { + file = { load_collapsed_diff_url: '/load/collapsed/diff/url' }; + getters = {}; + }); + + beforeEach(() => { + jest.spyOn(axios, 'get').mockReturnValue(Promise.resolve({ data: {} })); + }); + + it('fetches the data when there is no mergeRequestDiff', () => { + diffActions.loadCollapsedDiff({ commit() {}, getters, state }, file); + + expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, { + params: expect.any(Object), + }); + }); + + it.each` + desc | versionPath | start_sha | diff_id + ${'no additional version information'} | ${`${pathRoot}?search=terms`} | ${undefined} | ${undefined} + ${'the diff_id'} | ${`${pathRoot}?diff_id=${diffId}`} | ${undefined} | ${diffId} + ${'the start_sha'} | ${`${pathRoot}?start_sha=${startSha}`} | ${startSha} | ${undefined} + ${'all available version information'} | ${`${pathRoot}?diff_id=${diffId}&start_sha=${startSha}`} | ${startSha} | ${diffId} + `('fetches the data and includes $desc', ({ versionPath, start_sha, diff_id }) => { + jest.spyOn(axios, 'get').mockReturnValue(Promise.resolve({ data: {} })); + + diffActions.loadCollapsedDiff( + { commit() {}, getters, state: { mergeRequestDiff: { version_path: versionPath } } }, + file, + ); + + expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, { + params: expect.objectContaining({ start_sha, diff_id }), + }); + }); + }); }); describe('toggleFileDiscussions', () => { diff --git a/spec/frontend/diffs/utils/merge_request_spec.js b/spec/frontend/diffs/utils/merge_request_spec.js index 8c7b1e1f2a5..c070e8c004d 100644 --- a/spec/frontend/diffs/utils/merge_request_spec.js +++ b/spec/frontend/diffs/utils/merge_request_spec.js @@ -2,30 +2,64 @@ import { getDerivedMergeRequestInformation } from '~/diffs/utils/merge_request'; import { diffMetadata } from '../mock_data/diff_metadata'; describe('Merge Request utilities', () => { - const derivedMrInfo = { + const derivedBaseInfo = { mrPath: '/gitlab-org/gitlab-test/-/merge_requests/4', userOrGroup: 'gitlab-org', project: 'gitlab-test', id: '4', }; + const derivedVersionInfo = { + diffId: '4', + startSha: 'eb227b3e214624708c474bdab7bde7afc17cefcc', + }; + const noVersion = { + diffId: undefined, + startSha: undefined, + }; const unparseableEndpoint = { mrPath: undefined, userOrGroup: undefined, project: undefined, id: undefined, + ...noVersion, }; describe('getDerivedMergeRequestInformation', () => { - const endpoint = `${diffMetadata.latest_version_path}.json?searchParam=irrelevant`; + let endpoint = `${diffMetadata.latest_version_path}.json?searchParam=irrelevant`; it.each` argument | response - ${{ endpoint }} | ${derivedMrInfo} + ${{ endpoint }} | ${{ ...derivedBaseInfo, ...noVersion }} ${{}} | ${unparseableEndpoint} ${{ endpoint: undefined }} | ${unparseableEndpoint} ${{ endpoint: null }} | ${unparseableEndpoint} `('generates the correct derived results based on $argument', ({ argument, response }) => { expect(getDerivedMergeRequestInformation(argument)).toStrictEqual(response); }); + + describe('version information', () => { + const bare = diffMetadata.latest_version_path; + endpoint = diffMetadata.merge_request_diffs[0].compare_path; + + it('still gets the correct derived information', () => { + expect(getDerivedMergeRequestInformation({ endpoint })).toMatchObject(derivedBaseInfo); + }); + + it.each` + url | versionPart + ${endpoint} | ${derivedVersionInfo} + ${`${bare}?diff_id=${derivedVersionInfo.diffId}`} | ${{ ...derivedVersionInfo, startSha: undefined }} + ${`${bare}?start_sha=${derivedVersionInfo.startSha}`} | ${{ ...derivedVersionInfo, diffId: undefined }} + `( + 'generates the correct derived version information based on $url', + ({ url, versionPart }) => { + expect(getDerivedMergeRequestInformation({ endpoint: url })).toMatchObject(versionPart); + }, + ); + + it('extracts nothing if there is no available version-like information in the URL', () => { + expect(getDerivedMergeRequestInformation({ endpoint: bare })).toMatchObject(noVersion); + }); + }); }); }); diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js index fb50d623543..329cc15df97 100644 --- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js +++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js @@ -14,7 +14,6 @@ import VueApollo from 'vue-apollo'; import MockAdapter from 'axios-mock-adapter'; import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { stripTypenames } from 'helpers/graphql_helpers'; import waitForPromises from 'helpers/wait_for_promises'; import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants'; import axios from '~/lib/utils/axios_utils'; @@ -190,7 +189,7 @@ describe('DependencyProxyApp', () => { it('shows list', () => { expect(findManifestList().props()).toMatchObject({ manifests: proxyManifests(), - pagination: stripTypenames(pagination()), + pagination: pagination(), }); }); diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js index 9e4c747a1bd..2f415bfd6f9 100644 --- a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js +++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js @@ -1,5 +1,4 @@ import { GlKeysetPagination } from '@gitlab/ui'; -import { stripTypenames } from 'helpers/graphql_helpers'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ManifestRow from '~/packages_and_registries/dependency_proxy/components/manifest_row.vue'; @@ -14,7 +13,7 @@ describe('Manifests List', () => { const defaultProps = { manifests: proxyManifests(), - pagination: stripTypenames(pagination()), + pagination: pagination(), }; const createComponent = (propsData = defaultProps) => { @@ -60,9 +59,8 @@ describe('Manifests List', () => { it('has the correct props', () => { createComponent(); - expect(findPagination().props()).toMatchObject({ - ...defaultProps.pagination, - }); + const { __typename, ...paginationProps } = defaultProps.pagination; + expect(findPagination().props()).toMatchObject(paginationProps); }); it('emits the next-page event', () => { diff --git a/spec/frontend/projects/project_new_spec.js b/spec/frontend/projects/project_new_spec.js index 4fcecc3a307..d69bfc4ec92 100644 --- a/spec/frontend/projects/project_new_spec.js +++ b/spec/frontend/projects/project_new_spec.js @@ -1,12 +1,14 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { TEST_HOST } from 'helpers/test_constants'; import projectNew from '~/projects/project_new'; +import { checkRules } from '~/projects/project_name_rules'; import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper'; describe('New Project', () => { let $projectImportUrl; let $projectPath; let $projectName; + let $projectNameError; const mockKeyup = (el) => el.dispatchEvent(new KeyboardEvent('keyup')); const mockChange = (el) => el.dispatchEvent(new Event('change')); @@ -29,6 +31,7 @@ describe('New Project', () => { </div> </div> <input id="project_name" /> + <div class="gl-field-error hidden" id="project_name_error" /> <input id="project_path" /> </div> <div class="js-user-readme-repo"></div> @@ -41,6 +44,7 @@ describe('New Project', () => { $projectImportUrl = document.querySelector('#project_import_url'); $projectPath = document.querySelector('#project_path'); $projectName = document.querySelector('#project_name'); + $projectNameError = document.querySelector('#project_name_error'); }); afterEach(() => { @@ -84,6 +88,57 @@ describe('New Project', () => { }); }); + describe('tracks manual name input', () => { + beforeEach(() => { + projectNew.bindEvents(); + }); + + afterEach(() => { + unmockTracking(); + }); + + it('no error message by default', () => { + expect($projectNameError.classList.contains('hidden')).toBe(true); + }); + + it('show error message if name is validate', () => { + $projectName.value = '.validate!Name'; + triggerEvent($projectName, 'change'); + + expect($projectNameError.innerText).toBe( + "Name must start with a letter, digit, emoji, or '_'", + ); + expect($projectNameError.classList.contains('hidden')).toBe(false); + }); + }); + + describe('project name rule', () => { + describe("Name must start with a letter, digit, emoji, or '_'", () => { + const errormsg = "Name must start with a letter, digit, emoji, or '_'"; + it("'.foo' should error", () => { + const text = '.foo'; + expect(checkRules(text)).toBe(errormsg); + }); + it('_foo should passed', () => { + const text = '_foo'; + expect(checkRules(text)).toBe(''); + }); + }); + + describe("Name can contain only letters, digits, emojis, '_', '.', '+', dashes, or spaces", () => { + const errormsg = + "Name can contain only letters, digits, emojis, '_', '.', '+', dashes, or spaces"; + it("'foo(#^.^#)foo' should error", () => { + const text = 'foo(#^.^#)foo'; + expect(checkRules(text)).toBe(errormsg); + }); + it("'foo123😊_.+- ' should passed", () => { + const text = 'foo123😊_.+- '; + expect(checkRules(text)).toBe(''); + }); + }); + }); + describe('deriveProjectPathFromUrl', () => { const dummyImportUrl = `${TEST_HOST}/dummy/import/url.git`; diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index 30475b36561..a2b34fe38a9 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -21,6 +21,7 @@ import WorkItemTitle from '~/work_items/components/work_item_title.vue'; import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue'; import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue'; +import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue'; import { i18n } from '~/work_items/constants'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; @@ -38,6 +39,7 @@ import { workItemAssigneesSubscriptionResponse, workItemMilestoneSubscriptionResponse, projectWorkItemResponse, + objectiveType, } from '../mock_data'; describe('WorkItemDetail component', () => { @@ -78,6 +80,7 @@ describe('WorkItemDetail component', () => { const findParentButton = () => findParent().findComponent(GlButton); const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]'); const findWorkItemType = () => wrapper.find('[data-testid="work-item-type"]'); + const findHierarchyTree = () => wrapper.findComponent(WorkItemTree); const createComponent = ({ isModal = false, @@ -638,4 +641,24 @@ describe('WorkItemDetail component', () => { iid: '1', }); }); + + describe('hierarchy widget', () => { + it('does not render children tree by default', async () => { + createComponent(); + await waitForPromises(); + + expect(findHierarchyTree().exists()).toBe(false); + }); + + it('renders children tree when work item is an Objective', async () => { + const objectiveWorkItem = workItemResponseFactory({ + workItemType: objectiveType, + }); + const handler = jest.fn().mockResolvedValue(objectiveWorkItem); + createComponent({ handler }); + await waitForPromises(); + + expect(findHierarchyTree().exists()).toBe(true); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js b/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js index 5563ba12a45..48711ddf15d 100644 --- a/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js +++ b/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js @@ -25,9 +25,13 @@ describe('RelatedItemsTree', () => { expect(wrapper.findAllComponents(GlDropdownSectionHeader).at(0).text()).toContain( 'Objective', ); - expect(wrapper.findAllComponents(GlDropdownSectionHeader).at(1).text()).toContain( - 'Key result', - ); + + // TODO: Uncomment once following two issues addressed + // https://gitlab.com/gitlab-org/gitlab/-/issues/381833 + // https://gitlab.com/gitlab-org/gitlab/-/issues/385084 + // expect(wrapper.findAllComponents(GlDropdownSectionHeader).at(1).text()).toContain( + // 'Key result', + // ); }); }); }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js index e345e5fc7cd..3a8e785bc80 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js @@ -1,33 +1,68 @@ import { GlButton, GlIcon } from '@gitlab/ui'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/flash'; import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue'; +import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql'; import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue'; import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue'; - -import { workItemTask, confidentialWorkItemTask, closedWorkItemTask } from '../../mock_data'; +import WorkItemTreeChildren from '~/work_items/components/work_item_links/work_item_tree_children.vue'; +import { + WIDGET_TYPE_HIERARCHY, + TASK_TYPE_NAME, + WORK_ITEM_TYPE_VALUE_OBJECTIVE, +} from '~/work_items/constants'; + +import { + workItemTask, + workItemObjectiveWithChild, + confidentialWorkItemTask, + closedWorkItemTask, + workItemHierarchyTreeResponse, + workItemHierarchyTreeFailureResponse, +} from '../../mock_data'; + +jest.mock('~/flash'); describe('WorkItemLinkChild', () => { const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2'; let wrapper; + let getWorkItemTreeQueryHandler; + + Vue.use(VueApollo); const createComponent = ({ projectPath = 'gitlab-org/gitlab-test', canUpdate = true, issuableGid = WORK_ITEM_ID, childItem = workItemTask, + workItemType = TASK_TYPE_NAME, + apolloProvider = null, } = {}) => { + getWorkItemTreeQueryHandler = jest.fn().mockResolvedValue(workItemHierarchyTreeResponse); + wrapper = shallowMountExtended(WorkItemLinkChild, { + apolloProvider: + apolloProvider || createMockApollo([[getWorkItemTreeQuery, getWorkItemTreeQueryHandler]]), propsData: { projectPath, canUpdate, issuableGid, childItem, + workItemType, }, }); }; + beforeEach(() => { + createAlert.mockClear(); + }); + afterEach(() => { wrapper.destroy(); }); @@ -121,7 +156,78 @@ describe('WorkItemLinkChild', () => { it('removeChild event on menu triggers `click-remove-child` event', () => { itemMenuEl.vm.$emit('removeChild'); - expect(wrapper.emitted('remove')).toEqual([[workItemTask.id]]); + expect(wrapper.emitted('removeChild')).toEqual([[workItemTask.id]]); + }); + }); + + describe('nested children', () => { + const findExpandButton = () => wrapper.findByTestId('expand-child'); + const findTreeChildren = () => wrapper.findComponent(WorkItemTreeChildren); + + beforeEach(() => { + getWorkItemTreeQueryHandler.mockClear(); + createComponent({ + childItem: workItemObjectiveWithChild, + workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE, + }); + }); + + it('displays expand button when item has children, children are not displayed by default', () => { + expect(findExpandButton().exists()).toBe(true); + expect(findTreeChildren().exists()).toBe(false); + }); + + it('fetches and displays children of item when clicking on expand button', async () => { + await findExpandButton().vm.$emit('click'); + + expect(findExpandButton().props('loading')).toBe(true); + await waitForPromises(); + + expect(getWorkItemTreeQueryHandler).toHaveBeenCalled(); + expect(findTreeChildren().exists()).toBe(true); + + const widgetHierarchy = workItemHierarchyTreeResponse.data.workItem.widgets.find( + (widget) => widget.type === WIDGET_TYPE_HIERARCHY, + ); + expect(findTreeChildren().props('children')).toEqual(widgetHierarchy.children.nodes); + }); + + it('does not fetch children if already fetched once while clicking expand button', async () => { + findExpandButton().vm.$emit('click'); // Expand for the first time + await waitForPromises(); + + expect(findTreeChildren().exists()).toBe(true); + + await findExpandButton().vm.$emit('click'); // Collapse + findExpandButton().vm.$emit('click'); // Expand again + await waitForPromises(); + + expect(getWorkItemTreeQueryHandler).toHaveBeenCalledTimes(1); // ensure children were fetched only once. + expect(findTreeChildren().exists()).toBe(true); + }); + + it('calls createAlert when children fetch request fails on clicking expand button', async () => { + const getWorkItemTreeQueryFailureHandler = jest + .fn() + .mockRejectedValue(workItemHierarchyTreeFailureResponse); + const apolloProvider = createMockApollo([ + [getWorkItemTreeQuery, getWorkItemTreeQueryFailureHandler], + ]); + + createComponent({ + childItem: workItemObjectiveWithChild, + workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE, + apolloProvider, + }); + + findExpandButton().vm.$emit('click'); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + captureError: true, + error: expect.any(Object), + message: 'Something went wrong while fetching children.', + }); }); }); }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js index fe95a985177..a61de78c623 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js @@ -257,7 +257,7 @@ describe('WorkItemLinks', () => { }); it('calls correct mutation with correct variables', async () => { - firstChild.vm.$emit('remove', firstChild.vm.childItem.id); + firstChild.vm.$emit('removeChild', firstChild.vm.childItem.id); await waitForPromises(); @@ -272,7 +272,7 @@ describe('WorkItemLinks', () => { }); it('shows toast when mutation succeeds', async () => { - firstChild.vm.$emit('remove', firstChild.vm.childItem.id); + firstChild.vm.$emit('removeChild', firstChild.vm.childItem.id); await waitForPromises(); @@ -284,7 +284,7 @@ describe('WorkItemLinks', () => { it('renders correct number of children after removal', async () => { expect(findWorkItemLinkChildItems()).toHaveLength(4); - firstChild.vm.$emit('remove', firstChild.vm.childItem.id); + firstChild.vm.$emit('removeChild', firstChild.vm.childItem.id); await waitForPromises(); expect(findWorkItemLinkChildItems()).toHaveLength(3); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js index 9c1e9ccb6e8..cc2e174dfda 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js @@ -2,12 +2,14 @@ import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue'; import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue'; +import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue'; import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue'; import { FORM_TYPES, WORK_ITEM_TYPE_ENUM_OBJECTIVE, WORK_ITEM_TYPE_ENUM_KEY_RESULT, } from '~/work_items/constants'; +import { childrenWorkItems } from '../../mock_data'; describe('WorkItemTree', () => { let wrapper; @@ -17,10 +19,16 @@ describe('WorkItemTree', () => { const findEmptyState = () => wrapper.findByTestId('tree-empty'); const findToggleFormSplitButton = () => wrapper.findComponent(OkrActionsSplitButton); const findForm = () => wrapper.findComponent(WorkItemLinksForm); + const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild); - const createComponent = () => { + const createComponent = ({ children = childrenWorkItems } = {}) => { wrapper = shallowMountExtended(WorkItemTree, { - propsData: { workItemType: 'Objective', workItemId: 'gid://gitlab/WorkItem/515' }, + propsData: { + workItemType: 'Objective', + workItemId: 'gid://gitlab/WorkItem/515', + children, + projectPath: 'test/project', + }, }); }; @@ -47,9 +55,14 @@ describe('WorkItemTree', () => { }); it('displays empty state if there are no children', () => { + createComponent({ children: [] }); expect(findEmptyState().exists()).toBe(true); }); + it('renders all hierarchy widget children', () => { + expect(findWorkItemLinkChildItems()).toHaveLength(4); + }); + it('does not display form by default', () => { expect(findForm().exists()).toBe(false); }); @@ -71,4 +84,11 @@ describe('WorkItemTree', () => { expect(findForm().props('childrenType')).toBe(childType); }, ); + + it('remove event on child triggers `removeChild` event', () => { + const firstChild = findWorkItemLinkChildItems().at(0); + firstChild.vm.$emit('removeChild', 'gid://gitlab/WorkItem/2'); + + expect(wrapper.emitted('removeChild')).toEqual([['gid://gitlab/WorkItem/2']]); + }); }); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index a4c16e014ef..7bade734586 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -85,6 +85,7 @@ export const workItemQueryResponse = { { __typename: 'WorkItemWidgetHierarchy', type: 'HIERARCHY', + hasChildren: true, parent: { id: 'gid://gitlab/Issue/1', iid: '5', @@ -108,7 +109,15 @@ export const workItemQueryResponse = { state: 'OPEN', workItemType: { id: '1', + name: 'Task', + iconName: 'issue-type-task', }, + widgets: [ + { + type: 'HIERARCHY', + hasChildren: false, + }, + ], }, ], }, @@ -150,6 +159,7 @@ export const updateWorkItemMutationResponse = { }, widgets: [ { + type: 'HIERARCHY', children: { nodes: [ { @@ -161,10 +171,13 @@ export const updateWorkItemMutationResponse = { state: 'OPEN', workItemType: { id: '1', + name: 'Task', + iconName: 'issue-type-task', }, }, ], }, + __typename: 'WorkItemConnection', }, { __typename: 'WorkItemWidgetAssignees', @@ -219,6 +232,20 @@ export const descriptionHtmlWithCheckboxes = ` </ul> `; +const taskType = { + __typename: 'WorkItemType', + id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + iconName: 'issue-type-task', +}; + +export const objectiveType = { + __typename: 'WorkItemType', + id: 'gid://gitlab/WorkItems::Type/2411', + name: 'Objective', + iconName: 'issue-type-objective', +}; + export const workItemResponseFactory = ({ canUpdate = false, canDelete = false, @@ -236,6 +263,7 @@ export const workItemResponseFactory = ({ lastEditedBy = null, withCheckboxes = false, parent = mockParent.parent, + workItemType = taskType, } = {}) => ({ data: { workItem: { @@ -253,12 +281,7 @@ export const workItemResponseFactory = ({ id: '1', fullPath: 'test-project-path', }, - workItemType: { - __typename: 'WorkItemType', - id: 'gid://gitlab/WorkItems::Type/5', - name: 'Task', - iconName: 'issue-type-task', - }, + workItemType, userPermissions: { deleteWorkItem: canDelete, updateWorkItem: canUpdate, @@ -338,6 +361,7 @@ export const workItemResponseFactory = ({ { __typename: 'WorkItemWidgetHierarchy', type: 'HIERARCHY', + hasChildren: true, children: { nodes: [ { @@ -349,7 +373,15 @@ export const workItemResponseFactory = ({ state: 'OPEN', workItemType: { id: '1', + name: 'Task', + iconName: 'issue-type-task', }, + widgets: [ + { + type: 'HIERARCHY', + hasChildren: false, + }, + ], }, ], }, @@ -669,6 +701,8 @@ export const workItemHierarchyEmptyResponse = { id: 'gid://gitlab/WorkItem/1', workItemType: { id: 'gid://gitlab/WorkItems::Type/6', + name: 'Issue', + iconName: 'issue-type-issue', __typename: 'WorkItemType', }, title: 'New title', @@ -692,6 +726,7 @@ export const workItemHierarchyEmptyResponse = { { type: 'HIERARCHY', parent: null, + hasChildren: false, children: { nodes: [], __typename: 'WorkItemConnection', @@ -710,6 +745,8 @@ export const workItemHierarchyNoUpdatePermissionResponse = { id: 'gid://gitlab/WorkItem/1', workItemType: { id: 'gid://gitlab/WorkItems::Type/6', + name: 'Issue', + iconName: 'issue-type-issue', __typename: 'WorkItemType', }, title: 'New title', @@ -731,6 +768,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = { { type: 'HIERARCHY', parent: null, + hasChildren: true, children: { nodes: [ { @@ -738,6 +776,8 @@ export const workItemHierarchyNoUpdatePermissionResponse = { iid: '2', workItemType: { id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + iconName: 'issue-type-task', __typename: 'WorkItemType', }, title: 'xyz', @@ -745,6 +785,12 @@ export const workItemHierarchyNoUpdatePermissionResponse = { confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + widgets: [ + { + type: 'HIERARCHY', + hasChildren: false, + }, + ], __typename: 'WorkItem', }, ], @@ -763,6 +809,8 @@ export const workItemTask = { iid: '4', workItemType: { id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + iconName: 'issue-type-task', __typename: 'WorkItemType', }, title: 'bar', @@ -778,6 +826,8 @@ export const confidentialWorkItemTask = { iid: '2', workItemType: { id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + iconName: 'issue-type-task', __typename: 'WorkItemType', }, title: 'xyz', @@ -793,6 +843,8 @@ export const closedWorkItemTask = { iid: '3', workItemType: { id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + iconName: 'issue-type-task', __typename: 'WorkItemType', }, title: 'abc', @@ -803,6 +855,28 @@ export const closedWorkItemTask = { __typename: 'WorkItem', }; +export const childrenWorkItems = [ + confidentialWorkItemTask, + closedWorkItemTask, + workItemTask, + { + id: 'gid://gitlab/WorkItem/5', + iid: '5', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + iconName: 'issue-type-task', + __typename: 'WorkItemType', + }, + title: 'foobar', + state: 'OPEN', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, + __typename: 'WorkItem', + }, +]; + export const workItemHierarchyResponse = { data: { workItem: { @@ -810,6 +884,8 @@ export const workItemHierarchyResponse = { iid: '1', workItemType: { id: 'gid://gitlab/WorkItems::Type/6', + name: 'Objective', + iconName: 'issue-type-objective', __typename: 'WorkItemType', }, title: 'New title', @@ -831,23 +907,97 @@ export const workItemHierarchyResponse = { { type: 'HIERARCHY', parent: null, + hasChildren: true, + children: { + nodes: childrenWorkItems, + __typename: 'WorkItemConnection', + }, + __typename: 'WorkItemWidgetHierarchy', + }, + ], + __typename: 'WorkItem', + }, + }, +}; + +export const workItemObjectiveWithChild = { + id: 'gid://gitlab/WorkItem/12', + iid: '12', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/2411', + name: 'Objective', + iconName: 'issue-type-objective', + __typename: 'WorkItemType', + }, + title: 'Objective', + state: 'OPEN', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, + widgets: [ + { + type: 'HIERARCHY', + hasChildren: true, + __typename: 'WorkItemWidgetHierarchy', + }, + ], + __typename: 'WorkItem', +}; + +export const workItemHierarchyTreeResponse = { + data: { + workItem: { + id: 'gid://gitlab/WorkItem/2', + iid: '2', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/2411', + name: 'Objective', + iconName: 'issue-type-objective', + __typename: 'WorkItemType', + }, + title: 'New title', + userPermissions: { + deleteWorkItem: true, + updateWorkItem: true, + }, + confidential: false, + project: { + __typename: 'Project', + id: '1', + fullPath: 'test-project-path', + }, + widgets: [ + { + type: 'DESCRIPTION', + __typename: 'WorkItemWidgetDescription', + }, + { + type: 'HIERARCHY', + parent: null, + hasChildren: true, children: { nodes: [ - confidentialWorkItemTask, - closedWorkItemTask, - workItemTask, { - id: 'gid://gitlab/WorkItem/5', - iid: '5', + id: 'gid://gitlab/WorkItem/13', + iid: '13', workItemType: { - id: 'gid://gitlab/WorkItems::Type/5', + id: 'gid://gitlab/WorkItems::Type/2411', + name: 'Objective', + iconName: 'issue-type-objective', __typename: 'WorkItemType', }, - title: 'foobar', + title: 'Objective 2', state: 'OPEN', confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + widgets: [ + { + type: 'HIERARCHY', + hasChildren: true, + __typename: 'WorkItemWidgetHierarchy', + }, + ], __typename: 'WorkItem', }, ], @@ -861,6 +1011,15 @@ export const workItemHierarchyResponse = { }, }; +export const workItemHierarchyTreeFailureResponse = { + data: {}, + errors: [ + { + message: 'Something went wrong', + }, + ], +}; + export const changeWorkItemParentMutationResponse = { data: { workItemUpdate: { @@ -894,6 +1053,7 @@ export const changeWorkItemParentMutationResponse = { __typename: 'WorkItemWidgetHierarchy', type: 'HIERARCHY', parent: null, + hasChildren: false, children: { nodes: [], }, |