From b75edbabe7746f0fa35dab9c32e7c1bcf9951b3e Mon Sep 17 00:00:00 2001 From: Deepika Guliani Date: Fri, 28 Nov 2025 14:36:48 +1100 Subject: [PATCH 1/5] Create work item types list Changelog: added --- .../components/work_item_types_list.vue | 131 ++++++++++++++++++ .../configurable_types_settings.vue | 47 ++++++- .../work_items/work_item_settings_home.vue | 11 +- locale/gitlab.pot | 27 ++++ 4 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/work_items/components/work_item_types_list.vue diff --git a/app/assets/javascripts/work_items/components/work_item_types_list.vue b/app/assets/javascripts/work_items/components/work_item_types_list.vue new file mode 100644 index 00000000000000..920c4935622b33 --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_types_list.vue @@ -0,0 +1,131 @@ + + + diff --git a/ee/app/assets/javascripts/groups/settings/work_items/configurable_types/configurable_types_settings.vue b/ee/app/assets/javascripts/groups/settings/work_items/configurable_types/configurable_types_settings.vue index d2ca3392540473..838118acc999aa 100644 --- a/ee/app/assets/javascripts/groups/settings/work_items/configurable_types/configurable_types_settings.vue +++ b/ee/app/assets/javascripts/groups/settings/work_items/configurable_types/configurable_types_settings.vue @@ -1,9 +1,54 @@ diff --git a/ee/app/assets/javascripts/groups/settings/work_items/work_item_settings_home.vue b/ee/app/assets/javascripts/groups/settings/work_items/work_item_settings_home.vue index 8fbebda2aee10b..d8063969dc7e6f 100644 --- a/ee/app/assets/javascripts/groups/settings/work_items/work_item_settings_home.vue +++ b/ee/app/assets/javascripts/groups/settings/work_items/work_item_settings_home.vue @@ -8,6 +8,7 @@ import ConfigurableTypesSettings from './configurable_types/configurable_types_s const STATUS_SECTION_ID = 'js-custom-status-settings'; const CUSTOM_FIELD_SECTION_ID = 'js-custom-fields-settings'; +const WORK_ITEM_TYPES_SECTION_ID = 'js-work-item-types-settings'; export default { components: { @@ -25,11 +26,13 @@ export default { }, STATUS_SECTION_ID, CUSTOM_FIELD_SECTION_ID, + WORK_ITEM_TYPES_SECTION_ID, data() { return { sectionsExpandedState: { [STATUS_SECTION_ID]: false, [CUSTOM_FIELD_SECTION_ID]: false, + [WORK_ITEM_TYPES_SECTION_ID]: false, }, searchRoot: null, }; @@ -107,7 +110,13 @@ export default {

- + Date: Thu, 11 Dec 2025 14:40:08 +1100 Subject: [PATCH 2/5] Apply UX review comments --- .../javascripts/work_items/components/work_item_types_list.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/work_items/components/work_item_types_list.vue b/app/assets/javascripts/work_items/components/work_item_types_list.vue index 920c4935622b33..1733718deeca81 100644 --- a/app/assets/javascripts/work_items/components/work_item_types_list.vue +++ b/app/assets/javascripts/work_items/components/work_item_types_list.vue @@ -98,7 +98,7 @@ export default {
-- GitLab From fd79b92176b2022f90773ee47a6d376b7a9992f0 Mon Sep 17 00:00:00 2001 From: Deepika Guliani Date: Thu, 11 Dec 2025 15:47:11 +1100 Subject: [PATCH 3/5] Add tests --- .../configurable_types_settings_spec.js | 170 ++++++++++++ .../work_item_settings_home_spec.js | 22 +- .../components/work_item_types_list_spec.js | 247 ++++++++++++++++++ 3 files changed, 438 insertions(+), 1 deletion(-) create mode 100644 ee/spec/frontend/groups/settings/work_items/configurable_types/configurable_types_settings_spec.js create mode 100644 spec/frontend/work_items/components/work_item_types_list_spec.js diff --git a/ee/spec/frontend/groups/settings/work_items/configurable_types/configurable_types_settings_spec.js b/ee/spec/frontend/groups/settings/work_items/configurable_types/configurable_types_settings_spec.js new file mode 100644 index 00000000000000..7191a6ad19df8d --- /dev/null +++ b/ee/spec/frontend/groups/settings/work_items/configurable_types/configurable_types_settings_spec.js @@ -0,0 +1,170 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ConfigurableTypesSettings from 'ee/groups/settings/work_items/configurable_types/configurable_types_settings.vue'; +import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; +import HelpPageLink from '~/vue_shared/components/help_page_link/help_page_link.vue'; +import WorkItemTypesList from '~/work_items/components/work_item_types_list.vue'; + +describe('ConfigurableTypesSettings', () => { + let wrapper; + + const defaultProps = { + fullPath: 'test-group', + id: 'work-item-types-settings', + expanded: false, + }; + + const createWrapper = (props = {}) => { + wrapper = shallowMountExtended(ConfigurableTypesSettings, { + propsData: { + ...defaultProps, + ...props, + }, + stubs: { + SettingsBlock, + HelpPageLink, + WorkItemTypesList: true, + }, + }); + }; + + const findSettingsBlock = () => wrapper.findComponent(SettingsBlock); + const findWorkItemTypesList = () => wrapper.findComponent(WorkItemTypesList); + const findHelpPageLink = () => wrapper.findComponent(HelpPageLink); + const findDescription = () => wrapper.find('.gl-mb-3'); + + describe('default rendering', () => { + beforeEach(() => { + createWrapper(); + }); + + it('renders the component', () => { + expect(wrapper.exists()).toBe(true); + }); + + it('renders SettingsBlock component', () => { + expect(findSettingsBlock().exists()).toBe(true); + }); + + it('passes correct props to SettingsBlock', () => { + expect(findSettingsBlock().props()).toEqual( + expect.objectContaining({ + id: defaultProps.id, + title: 'Work item types', + expanded: defaultProps.expanded, + }), + ); + }); + + it('renders WorkItemTypesList component', () => { + expect(findWorkItemTypesList().exists()).toBe(true); + }); + + it('passes fullPath prop to WorkItemTypesList', () => { + expect(findWorkItemTypesList().props('fullPath')).toBe(defaultProps.fullPath); + }); + + it('renders description text', () => { + expect(findDescription().exists()).toBe(true); + expect(findDescription().text()).toContain( + 'Work item types are used to track different kinds of work', + ); + }); + + it('renders help page link', () => { + expect(findHelpPageLink().exists()).toBe(true); + expect(findHelpPageLink().props('href')).toBe('user/work_items/_index.md'); + expect(findHelpPageLink().text()).toContain('How do I use configure work item types?'); + }); + + it('applies correct CSS classes to description', () => { + expect(findDescription().classes()).toContain('gl-mb-3'); + expect(findDescription().classes()).toContain('gl-text-subtle'); + }); + + it('description has appropriate text styling', () => { + const description = findDescription(); + expect(description.classes()).toContain('gl-text-subtle'); + }); + }); + + describe('props handling', () => { + it('renders with expanded prop as true', () => { + createWrapper({ expanded: true }); + + expect(findSettingsBlock().props('expanded')).toBe(true); + }); + + it('renders with expanded prop as false', () => { + createWrapper({ expanded: false }); + + expect(findSettingsBlock().props('expanded')).toBe(false); + }); + + it('renders with different fullPath', () => { + const customPath = 'my-group/sub-group/project'; + createWrapper({ fullPath: customPath }); + + expect(findWorkItemTypesList().props('fullPath')).toBe(customPath); + }); + + it('renders with different id', () => { + const customId = 'custom-id-123'; + createWrapper({ id: customId }); + + expect(findSettingsBlock().props('id')).toBe(customId); + }); + }); + + describe('event handling', () => { + beforeEach(() => { + createWrapper(); + }); + + it('emits toggle-expand event when SettingsBlock emits toggle-expand', () => { + findSettingsBlock().vm.$emit('toggle-expand', true); + + expect(wrapper.emitted('toggle-expand')).toHaveLength(1); + expect(wrapper.emitted('toggle-expand')[0]).toEqual([true]); + }); + + it('emits toggle-expand event with false value', () => { + findSettingsBlock().vm.$emit('toggle-expand', false); + + expect(wrapper.emitted('toggle-expand')).toHaveLength(1); + expect(wrapper.emitted('toggle-expand')[0]).toEqual([false]); + }); + + it('emits multiple toggle-expand events', () => { + findSettingsBlock().vm.$emit('toggle-expand', true); + findSettingsBlock().vm.$emit('toggle-expand', false); + findSettingsBlock().vm.$emit('toggle-expand', true); + + expect(wrapper.emitted('toggle-expand')).toHaveLength(3); + }); + }); + + describe('template structure', () => { + beforeEach(() => { + createWrapper(); + }); + + it('renders description slot within SettingsBlock', () => { + const settingsBlock = findSettingsBlock(); + expect(settingsBlock.find('.gl-mb-3').exists()).toBe(true); + }); + + it('renders default slot with WorkItemTypesList', () => { + const settingsBlock = findSettingsBlock(); + expect(settingsBlock.findComponent(WorkItemTypesList).exists()).toBe(true); + }); + + it('maintains correct slot hierarchy', () => { + const settingsBlock = findSettingsBlock(); + const descriptionSlot = settingsBlock.find('.gl-mb-3'); + const listComponent = settingsBlock.findComponent(WorkItemTypesList); + + expect(descriptionSlot.exists()).toBe(true); + expect(listComponent.exists()).toBe(true); + }); + }); +}); diff --git a/ee/spec/frontend/groups/settings/work_items/work_item_settings_home_spec.js b/ee/spec/frontend/groups/settings/work_items/work_item_settings_home_spec.js index 38c77104dd181b..96eab2decaefa4 100644 --- a/ee/spec/frontend/groups/settings/work_items/work_item_settings_home_spec.js +++ b/ee/spec/frontend/groups/settings/work_items/work_item_settings_home_spec.js @@ -2,6 +2,7 @@ import { nextTick } from 'vue'; import { shallowMount } from '@vue/test-utils'; import CustomFieldsList from 'ee/groups/settings/work_items/custom_fields/custom_fields_list.vue'; import CustomStatusSettings from 'ee/groups/settings/work_items/custom_status/custom_status_settings.vue'; +import ConfigurableTypesSettings from 'ee/groups/settings/work_items/configurable_types/configurable_types_settings.vue'; import SearchSettings from '~/search_settings/components/search_settings.vue'; import WorkItemSettingsHome from 'ee/groups/settings/work_items/work_item_settings_home.vue'; @@ -9,7 +10,10 @@ describe('WorkItemSettingsHome', () => { let wrapper; const fullPath = 'group/project'; - const createComponent = ({ mocks = {}, glFeatures = {} } = {}) => { + const createComponent = ({ + mocks = {}, + glFeatures = { workItemConfigurableTypes: true }, + } = {}) => { wrapper = shallowMount(WorkItemSettingsHome, { propsData: { fullPath, @@ -26,9 +30,25 @@ describe('WorkItemSettingsHome', () => { const findCustomFieldsList = () => wrapper.findComponent(CustomFieldsList); const findCustomStatusSettings = () => wrapper.findComponent(CustomStatusSettings); + const findConfigurableTypesSettings = () => wrapper.findComponent(ConfigurableTypesSettings); const findSearchSettings = () => wrapper.findComponent(SearchSettings); const findPageTitle = () => wrapper.find('h1'); + it('renders ConfigurableTypesSettings component with correct props when FF is switched on', () => { + createComponent(); + + expect(findConfigurableTypesSettings().exists()).toBe(true); + expect(findConfigurableTypesSettings().props('fullPath')).toBe(fullPath); + }); + + it('does not render', () => { + createComponent({ + glFeatures: { workItemConfigurableTypes: false }, + }); + + expect(findConfigurableTypesSettings().exists()).toBe(false); + }); + it('always renders CustomFieldsList component with correct props', () => { createComponent(); diff --git a/spec/frontend/work_items/components/work_item_types_list_spec.js b/spec/frontend/work_items/components/work_item_types_list_spec.js new file mode 100644 index 00000000000000..d6ae307a6517db --- /dev/null +++ b/spec/frontend/work_items/components/work_item_types_list_spec.js @@ -0,0 +1,247 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlButton, GlDisclosureDropdown } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import WorkItemTypesList from '~/work_items/components/work_item_types_list.vue'; +import CrudComponent from '~/vue_shared/components/crud_component.vue'; +import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; +import namespaceWorkItemTypesQuery from '~/work_items/graphql/namespace_work_item_types.query.graphql'; +import { namespaceWorkItemTypesQueryResponse } from 'ee_else_ce_jest/work_items/mock_data'; + +Vue.use(VueApollo); + +describe('WorkItemTypesList', () => { + let wrapper; + let mockApollo; + + const mockEmptyResponse = { + data: { + workspace: { + workItemTypes: { + nodes: [], + __typename: 'WorkItemTypeConnection', + }, + __typename: 'Namespace', + }, + }, + }; + + const getMockWorkItemTypes = () => + namespaceWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes; + const mockWorkItemTypes = getMockWorkItemTypes(); + + const createWrapper = (mockResponse = namespaceWorkItemTypesQueryResponse, props = {}) => { + const queryHandler = jest.fn().mockResolvedValue(mockResponse); + mockApollo = createMockApollo([[namespaceWorkItemTypesQuery, queryHandler]]); + + wrapper = shallowMountExtended(WorkItemTypesList, { + apolloProvider: mockApollo, + propsData: { + fullPath: 'test-group', + ...props, + }, + stubs: { + CrudComponent, + GlButton, + GlDisclosureDropdown, + WorkItemTypeIcon, + }, + }); + + return queryHandler; + }; + + const findCrudComponent = () => wrapper.findComponent(CrudComponent); + const findWorkItemTypesTable = () => wrapper.findByTestId('work-item-types-table'); + const findWorkItemTypeRows = () => wrapper.findAll('.work-item-type-row'); + const findWorkItemTypeRow = (id) => wrapper.findByTestId(`work-item-type-row-${id}`); + const findNewTypeButton = () => wrapper.findByTestId('work-item-type-add-button'); + const findDropdownForType = (id) => findWorkItemTypeRow(id).findComponent(GlDisclosureDropdown); + + describe('default rendering', () => { + beforeEach(async () => { + createWrapper(); + await waitForPromises(); + }); + + it('renders the component with CrudComponent', () => { + expect(findCrudComponent().exists()).toBe(true); + }); + + it('renders with correct title and count', () => { + expect(findCrudComponent().props('title')).toBe('Types'); + expect(findCrudComponent().props('count')).toBe(mockWorkItemTypes.length); + }); + + it('renders the work item types table', () => { + expect(findWorkItemTypesTable().exists()).toBe(true); + }); + + it('renders a row for each work item type', () => { + expect(findWorkItemTypeRows()).toHaveLength(mockWorkItemTypes.length); + }); + + it('displays work item type name in each row', () => { + const rows = findWorkItemTypeRows(); + + mockWorkItemTypes.forEach((type, index) => { + expect(rows.at(index).text()).toContain(type.name); + }); + }); + + it('renders WorkItemTypeIcon for each type', () => { + const icons = wrapper.findAllComponents(WorkItemTypeIcon); + + expect(icons).toHaveLength(mockWorkItemTypes.length); + icons.wrappers.forEach((icon, index) => { + expect(icon.props('workItemType')).toBe(mockWorkItemTypes[index].name); + }); + }); + + it('renders New type button', () => { + expect(findNewTypeButton().exists()).toBe(true); + expect(findNewTypeButton().text()).toContain('New type'); + }); + + it('renders dropdown for each work item type', () => { + const dropdowns = wrapper.findAllComponents(GlDisclosureDropdown); + + expect(dropdowns).toHaveLength(mockWorkItemTypes.length); + }); + + it('renders dropdown with correct items', () => { + const dropdown = findDropdownForType(mockWorkItemTypes[0].id); + + expect(dropdown.props('items')).toHaveLength(2); + expect(dropdown.props('items')[0].text).toContain('Edit name and icon'); + expect(dropdown.props('items')[1].text).toContain('Delete'); + }); + + it('renders dropdown with correct toggle attributes', () => { + const dropdown = findDropdownForType(mockWorkItemTypes[0].id); + + expect(dropdown.props('toggleId')).toBe(`work-item-type-actions-${mockWorkItemTypes[0].id}`); + expect(dropdown.props('icon')).toBe('ellipsis_v'); + expect(dropdown.props('noCaret')).toBe(true); + expect(dropdown.props('placement')).toBe('bottom-end'); + }); + + it('passes correct props to CrudComponent', () => { + expect(findCrudComponent().attributes('data-testid')).toBe('work-item-types-crud'); + }); + }); + + describe('loading state', () => { + beforeEach(() => { + createWrapper(); + }); + + it('shows loading state when query is loading', () => { + expect(wrapper.text()).toContain('Loading...'); + expect(findWorkItemTypesTable().exists()).toBe(false); + }); + + it('hides loading state after query resolves', async () => { + expect(wrapper.text()).toContain('Loading...'); + + await waitForPromises(); + + expect(wrapper.text()).not.toContain('Loading...'); + expect(findWorkItemTypesTable().exists()).toBe(true); + }); + }); + + describe('empty state', () => { + beforeEach(async () => { + createWrapper(mockEmptyResponse); + await waitForPromises(); + }); + + it('renders table even when no work item types exist', () => { + expect(findWorkItemTypesTable().exists()).toBe(true); + }); + + it('displays zero count', () => { + expect(findCrudComponent().props('count')).toBe(0); + }); + + it('does not render any work item type rows', () => { + expect(findWorkItemTypeRows()).toHaveLength(0); + }); + + it('still renders New type button', () => { + expect(findNewTypeButton().exists()).toBe(true); + }); + }); + + describe('query behavior', () => { + it('passes correct fullPath to query', async () => { + const queryHandler = createWrapper(namespaceWorkItemTypesQueryResponse, { + fullPath: 'my-group/sub-group', + }); + + await waitForPromises(); + + expect(queryHandler).toHaveBeenCalledWith({ + fullPath: 'my-group/sub-group', + onlyAvailable: false, + }); + }); + + it('updates when fullPath prop changes', async () => { + const queryHandler = createWrapper(namespaceWorkItemTypesQueryResponse, { + fullPath: 'initial-path', + }); + + await waitForPromises(); + expect(queryHandler).toHaveBeenCalledTimes(1); + + await wrapper.setProps({ fullPath: 'updated-path' }); + await waitForPromises(); + + expect(queryHandler).toHaveBeenCalledWith({ fullPath: 'updated-path', onlyAvailable: false }); + }); + }); + + describe('table structure and styling', () => { + beforeEach(async () => { + createWrapper(); + await waitForPromises(); + }); + + it('renders each row with correct test id', () => { + mockWorkItemTypes.forEach((type) => { + expect(findWorkItemTypeRow(type.id).exists()).toBe(true); + }); + }); + + it('renders type information in first column', () => { + const firstRow = findWorkItemTypeRows().at(0); + const typeColumn = firstRow.find('.gl-w-15'); + + expect(typeColumn.exists()).toBe(true); + expect(typeColumn.classes()).toContain('gl-flex'); + expect(typeColumn.classes()).toContain('gl-gap-3'); + }); + + it('renders actions in second column', () => { + const firstRow = findWorkItemTypeRows().at(0); + const actionColumn = firstRow.find('.gl-w-40'); + + expect(actionColumn.exists()).toBe(true); + expect(actionColumn.findComponent(GlDisclosureDropdown).exists()).toBe(true); + }); + + it('applies correct border classes to rows', () => { + const rows = findWorkItemTypeRows(); + + rows.wrappers.forEach((row) => { + expect(row.classes()).toContain('gl-border-b'); + expect(row.classes()).toContain('gl-flex'); + expect(row.classes()).toContain('gl-justify-between'); + }); + }); + }); +}); -- GitLab From aa118a4c0d01cb02196845793f495dfdf9fd4cb5 Mon Sep 17 00:00:00 2001 From: Deepika Guliani Date: Mon, 15 Dec 2025 12:22:06 +1100 Subject: [PATCH 4/5] Apply reviewer suggestions --- .../components/work_item_types_list.vue | 74 ++++++------ .../configurable_types_settings_spec.js | 41 +------ .../work_item_settings_home_spec.js | 13 ++- locale/gitlab.pot | 9 +- .../components/work_item_types_list_spec.js | 107 +++++------------- 5 files changed, 81 insertions(+), 163 deletions(-) diff --git a/app/assets/javascripts/work_items/components/work_item_types_list.vue b/app/assets/javascripts/work_items/components/work_item_types_list.vue index 1733718deeca81..0e1752ac5bb136 100644 --- a/app/assets/javascripts/work_items/components/work_item_types_list.vue +++ b/app/assets/javascripts/work_items/components/work_item_types_list.vue @@ -1,5 +1,5 @@ diff --git a/ee/spec/frontend/groups/settings/work_items/configurable_types/configurable_types_settings_spec.js b/ee/spec/frontend/groups/settings/work_items/configurable_types/configurable_types_settings_spec.js index 7191a6ad19df8d..4a6713c4e23067 100644 --- a/ee/spec/frontend/groups/settings/work_items/configurable_types/configurable_types_settings_spec.js +++ b/ee/spec/frontend/groups/settings/work_items/configurable_types/configurable_types_settings_spec.js @@ -37,10 +37,6 @@ describe('ConfigurableTypesSettings', () => { createWrapper(); }); - it('renders the component', () => { - expect(wrapper.exists()).toBe(true); - }); - it('renders SettingsBlock component', () => { expect(findSettingsBlock().exists()).toBe(true); }); @@ -66,7 +62,7 @@ describe('ConfigurableTypesSettings', () => { it('renders description text', () => { expect(findDescription().exists()).toBe(true); expect(findDescription().text()).toContain( - 'Work item types are used to track different kinds of work', + 'Work item types are used to track different kinds of work. Each work item type can have different lifecycles and fields.', ); }); @@ -75,16 +71,6 @@ describe('ConfigurableTypesSettings', () => { expect(findHelpPageLink().props('href')).toBe('user/work_items/_index.md'); expect(findHelpPageLink().text()).toContain('How do I use configure work item types?'); }); - - it('applies correct CSS classes to description', () => { - expect(findDescription().classes()).toContain('gl-mb-3'); - expect(findDescription().classes()).toContain('gl-text-subtle'); - }); - - it('description has appropriate text styling', () => { - const description = findDescription(); - expect(description.classes()).toContain('gl-text-subtle'); - }); }); describe('props handling', () => { @@ -142,29 +128,4 @@ describe('ConfigurableTypesSettings', () => { expect(wrapper.emitted('toggle-expand')).toHaveLength(3); }); }); - - describe('template structure', () => { - beforeEach(() => { - createWrapper(); - }); - - it('renders description slot within SettingsBlock', () => { - const settingsBlock = findSettingsBlock(); - expect(settingsBlock.find('.gl-mb-3').exists()).toBe(true); - }); - - it('renders default slot with WorkItemTypesList', () => { - const settingsBlock = findSettingsBlock(); - expect(settingsBlock.findComponent(WorkItemTypesList).exists()).toBe(true); - }); - - it('maintains correct slot hierarchy', () => { - const settingsBlock = findSettingsBlock(); - const descriptionSlot = settingsBlock.find('.gl-mb-3'); - const listComponent = settingsBlock.findComponent(WorkItemTypesList); - - expect(descriptionSlot.exists()).toBe(true); - expect(listComponent.exists()).toBe(true); - }); - }); }); diff --git a/ee/spec/frontend/groups/settings/work_items/work_item_settings_home_spec.js b/ee/spec/frontend/groups/settings/work_items/work_item_settings_home_spec.js index 96eab2decaefa4..c79554ccfcd3a5 100644 --- a/ee/spec/frontend/groups/settings/work_items/work_item_settings_home_spec.js +++ b/ee/spec/frontend/groups/settings/work_items/work_item_settings_home_spec.js @@ -41,7 +41,7 @@ describe('WorkItemSettingsHome', () => { expect(findConfigurableTypesSettings().props('fullPath')).toBe(fullPath); }); - it('does not render', () => { + it('does not render ConfigurableTypesSettings when `workItemTypesConfigurableTypes` FF is off', () => { createComponent({ glFeatures: { workItemConfigurableTypes: false }, }); @@ -112,6 +112,17 @@ describe('WorkItemSettingsHome', () => { }); }); + it('navigates to hash when ConfigurableTypesSettings toggle-expand emits true', async () => { + createComponent({ mocks: { $router: mockRouter } }); + + await findConfigurableTypesSettings().vm.$emit('toggle-expand', true); + + expect(mockRouter.push).toHaveBeenCalledWith({ + name: 'workItemSettingsHome', + hash: '#js-work-item-types-settings', + }); + }); + it('clears hash when toggling to false without existing hash', async () => { createComponent({ mocks: { $router: mockRouter, $route: { hash: '' } } }); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 39ed6516f0e78f..5b6f81e80f2db4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -40154,9 +40154,6 @@ msgstr "" msgid "Loading users" msgstr "" -msgid "Loading..." -msgstr "" - msgid "Loading…" msgstr "" @@ -77130,6 +77127,9 @@ msgstr "" msgid "WorkItem|Failed to fetch namespace metadata." msgstr "" +msgid "WorkItem|Failed to fetch work item types." +msgstr "" + msgid "WorkItem|Failed to load custom fields." msgstr "" @@ -77464,9 +77464,6 @@ msgstr "" msgid "WorkItem|Open default" msgstr "" -msgid "WorkItem|Options" -msgstr "" - msgid "WorkItem|Options:" msgstr "" diff --git a/spec/frontend/work_items/components/work_item_types_list_spec.js b/spec/frontend/work_items/components/work_item_types_list_spec.js index d6ae307a6517db..5ab54b885f3e21 100644 --- a/spec/frontend/work_items/components/work_item_types_list_spec.js +++ b/spec/frontend/work_items/components/work_item_types_list_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { GlButton, GlDisclosureDropdown } from '@gitlab/ui'; +import { GlAlert, GlButton, GlDisclosureDropdown, GlLoadingIcon } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; @@ -31,9 +31,10 @@ describe('WorkItemTypesList', () => { const getMockWorkItemTypes = () => namespaceWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes; const mockWorkItemTypes = getMockWorkItemTypes(); + const namespaceQueryHandler = jest.fn().mockResolvedValue(namespaceWorkItemTypesQueryResponse); + const mockEmptyResponseHandler = jest.fn().mockResolvedValue(mockEmptyResponse); - const createWrapper = (mockResponse = namespaceWorkItemTypesQueryResponse, props = {}) => { - const queryHandler = jest.fn().mockResolvedValue(mockResponse); + const createWrapper = (queryHandler = namespaceQueryHandler, props = {}) => { mockApollo = createMockApollo([[namespaceWorkItemTypesQuery, queryHandler]]); wrapper = shallowMountExtended(WorkItemTypesList, { @@ -47,6 +48,7 @@ describe('WorkItemTypesList', () => { GlButton, GlDisclosureDropdown, WorkItemTypeIcon, + GlAlert, }, }); @@ -54,11 +56,13 @@ describe('WorkItemTypesList', () => { }; const findCrudComponent = () => wrapper.findComponent(CrudComponent); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findWorkItemTypesTable = () => wrapper.findByTestId('work-item-types-table'); const findWorkItemTypeRows = () => wrapper.findAll('.work-item-type-row'); const findWorkItemTypeRow = (id) => wrapper.findByTestId(`work-item-type-row-${id}`); const findNewTypeButton = () => wrapper.findByTestId('work-item-type-add-button'); const findDropdownForType = (id) => findWorkItemTypeRow(id).findComponent(GlDisclosureDropdown); + const findErrorAlert = () => wrapper.findComponent(GlAlert); describe('default rendering', () => { beforeEach(async () => { @@ -79,24 +83,14 @@ describe('WorkItemTypesList', () => { expect(findWorkItemTypesTable().exists()).toBe(true); }); - it('renders a row for each work item type', () => { - expect(findWorkItemTypeRows()).toHaveLength(mockWorkItemTypes.length); - }); - - it('displays work item type name in each row', () => { - const rows = findWorkItemTypeRows(); - - mockWorkItemTypes.forEach((type, index) => { - expect(rows.at(index).text()).toContain(type.name); - }); - }); - it('renders WorkItemTypeIcon for each type', () => { const icons = wrapper.findAllComponents(WorkItemTypeIcon); expect(icons).toHaveLength(mockWorkItemTypes.length); icons.wrappers.forEach((icon, index) => { - expect(icon.props('workItemType')).toBe(mockWorkItemTypes[index].name); + expect(icon.props()).toMatchObject({ + workItemType: mockWorkItemTypes[index].name, + }); }); }); @@ -111,12 +105,13 @@ describe('WorkItemTypesList', () => { expect(dropdowns).toHaveLength(mockWorkItemTypes.length); }); - it('renders dropdown with correct items', () => { - const dropdown = findDropdownForType(mockWorkItemTypes[0].id); - - expect(dropdown.props('items')).toHaveLength(2); - expect(dropdown.props('items')[0].text).toContain('Edit name and icon'); - expect(dropdown.props('items')[1].text).toContain('Delete'); + it('renders dropdowns with correct items', () => { + mockWorkItemTypes.forEach((mockWorkItemType) => { + const dropdown = findDropdownForType(mockWorkItemType.id); + expect(dropdown.props('items')).toHaveLength(2); + expect(dropdown.props('items')[0].text).toContain('Edit name and icon'); + expect(dropdown.props('items')[1].text).toContain('Delete'); + }); }); it('renders dropdown with correct toggle attributes', () => { @@ -127,10 +122,6 @@ describe('WorkItemTypesList', () => { expect(dropdown.props('noCaret')).toBe(true); expect(dropdown.props('placement')).toBe('bottom-end'); }); - - it('passes correct props to CrudComponent', () => { - expect(findCrudComponent().attributes('data-testid')).toBe('work-item-types-crud'); - }); }); describe('loading state', () => { @@ -139,23 +130,23 @@ describe('WorkItemTypesList', () => { }); it('shows loading state when query is loading', () => { - expect(wrapper.text()).toContain('Loading...'); + expect(findLoadingIcon().exists()).toBe(true); expect(findWorkItemTypesTable().exists()).toBe(false); }); it('hides loading state after query resolves', async () => { - expect(wrapper.text()).toContain('Loading...'); + expect(findLoadingIcon().exists()).toBe(true); await waitForPromises(); - expect(wrapper.text()).not.toContain('Loading...'); + expect(findLoadingIcon().exists()).toBe(false); expect(findWorkItemTypesTable().exists()).toBe(true); }); }); describe('empty state', () => { beforeEach(async () => { - createWrapper(mockEmptyResponse); + createWrapper(mockEmptyResponseHandler); await waitForPromises(); }); @@ -178,7 +169,7 @@ describe('WorkItemTypesList', () => { describe('query behavior', () => { it('passes correct fullPath to query', async () => { - const queryHandler = createWrapper(namespaceWorkItemTypesQueryResponse, { + const queryHandler = createWrapper(namespaceQueryHandler, { fullPath: 'my-group/sub-group', }); @@ -190,58 +181,14 @@ describe('WorkItemTypesList', () => { }); }); - it('updates when fullPath prop changes', async () => { - const queryHandler = createWrapper(namespaceWorkItemTypesQueryResponse, { - fullPath: 'initial-path', - }); - - await waitForPromises(); - expect(queryHandler).toHaveBeenCalledTimes(1); + it('error handling', async () => { + const errorQueryHandler = jest.fn().mockRejectedValue('Network error'); + createWrapper(errorQueryHandler); - await wrapper.setProps({ fullPath: 'updated-path' }); await waitForPromises(); - expect(queryHandler).toHaveBeenCalledWith({ fullPath: 'updated-path', onlyAvailable: false }); - }); - }); - - describe('table structure and styling', () => { - beforeEach(async () => { - createWrapper(); - await waitForPromises(); - }); - - it('renders each row with correct test id', () => { - mockWorkItemTypes.forEach((type) => { - expect(findWorkItemTypeRow(type.id).exists()).toBe(true); - }); - }); - - it('renders type information in first column', () => { - const firstRow = findWorkItemTypeRows().at(0); - const typeColumn = firstRow.find('.gl-w-15'); - - expect(typeColumn.exists()).toBe(true); - expect(typeColumn.classes()).toContain('gl-flex'); - expect(typeColumn.classes()).toContain('gl-gap-3'); - }); - - it('renders actions in second column', () => { - const firstRow = findWorkItemTypeRows().at(0); - const actionColumn = firstRow.find('.gl-w-40'); - - expect(actionColumn.exists()).toBe(true); - expect(actionColumn.findComponent(GlDisclosureDropdown).exists()).toBe(true); - }); - - it('applies correct border classes to rows', () => { - const rows = findWorkItemTypeRows(); - - rows.wrappers.forEach((row) => { - expect(row.classes()).toContain('gl-border-b'); - expect(row.classes()).toContain('gl-flex'); - expect(row.classes()).toContain('gl-justify-between'); - }); + expect(findErrorAlert().exists()).toBe(true); + expect(findErrorAlert().text()).toContain('Failed to fetch work item types'); }); }); }); -- GitLab From c906ed854cd266163616ee3af31306e23c61c498 Mon Sep 17 00:00:00 2001 From: Deepika Guliani Date: Tue, 16 Dec 2025 10:52:48 +1100 Subject: [PATCH 5/5] Apply maintainer comments --- .../components/work_item_types_list.vue | 25 ++++++------------- .../configurable_types_settings_spec.js | 25 ++++++------------- locale/gitlab.pot | 3 --- .../components/work_item_types_list_spec.js | 23 ++++++----------- 4 files changed, 21 insertions(+), 55 deletions(-) diff --git a/app/assets/javascripts/work_items/components/work_item_types_list.vue b/app/assets/javascripts/work_items/components/work_item_types_list.vue index 0e1752ac5bb136..6397ce351cf8f1 100644 --- a/app/assets/javascripts/work_items/components/work_item_types_list.vue +++ b/app/assets/javascripts/work_items/components/work_item_types_list.vue @@ -72,27 +72,17 @@ export default { ]; }, }, - i18n: { - title: s__('WorkItem|Types'), - emptyText: s__('WorkItem|No work item types found'), - }, };