From 48a8fb609854db4dcac0031af33c460365ef2cfd Mon Sep 17 00:00:00 2001
From: Deepika Guliani <dguliani@gitlab.com>
Date: Mon, 26 Jun 2023 10:00:43 +0530
Subject: [PATCH] Refactoring of work item attributes into a separate component

Changelog: changed
EE: true
---
 .../work_item_attributes_wrapper.vue          | 180 ++++++++++++++++++
 .../components/work_item_detail.vue           | 123 +-----------
 ...s => work_item_attributes_wrapper_spec.js} | 108 +++++------
 .../work_item_attributes_wrapper_spec.js      | 107 +++++++++++
 .../components/work_item_detail_spec.js       | 111 +++--------
 5 files changed, 360 insertions(+), 269 deletions(-)
 create mode 100644 app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
 rename ee/spec/frontend/work_items/components/{work_item_detail_spec.js => work_item_attributes_wrapper_spec.js} (64%)
 create mode 100644 spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js

diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
new file mode 100644
index 000000000000..3e82d603c1df
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
@@ -0,0 +1,180 @@
+<script>
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import {
+  sprintfWorkItem,
+  WIDGET_TYPE_ASSIGNEES,
+  WIDGET_TYPE_HEALTH_STATUS,
+  WIDGET_TYPE_ITERATION,
+  WIDGET_TYPE_LABELS,
+  WIDGET_TYPE_MILESTONE,
+  WIDGET_TYPE_PROGRESS,
+  WIDGET_TYPE_START_AND_DUE_DATE,
+  WIDGET_TYPE_WEIGHT,
+} from '../constants';
+import WorkItemState from './work_item_state.vue';
+import WorkItemDueDate from './work_item_due_date.vue';
+import WorkItemAssignees from './work_item_assignees.vue';
+import WorkItemLabels from './work_item_labels.vue';
+import WorkItemMilestone from './work_item_milestone.vue';
+
+export default {
+  components: {
+    WorkItemLabels,
+    WorkItemMilestone,
+    WorkItemAssignees,
+    WorkItemDueDate,
+    WorkItemState,
+    WorkItemWeight: () => import('ee_component/work_items/components/work_item_weight.vue'),
+    WorkItemProgress: () => import('ee_component/work_items/components/work_item_progress.vue'),
+    WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
+    WorkItemHealthStatus: () =>
+      import('ee_component/work_items/components/work_item_health_status.vue'),
+  },
+  mixins: [glFeatureFlagMixin()],
+  inject: ['fullPath'],
+  props: {
+    workItem: {
+      type: Object,
+      required: true,
+    },
+    workItemParentId: {
+      type: String,
+      required: false,
+      default: null,
+    },
+  },
+  computed: {
+    workItemType() {
+      return this.workItem.workItemType?.name;
+    },
+    canUpdate() {
+      return this.workItem?.userPermissions?.updateWorkItem;
+    },
+    canDelete() {
+      return this.workItem?.userPermissions?.deleteWorkItem;
+    },
+    canSetWorkItemMetadata() {
+      return this.workItem?.userPermissions?.setWorkItemMetadata;
+    },
+    canAssignUnassignUser() {
+      return this.workItemAssignees && this.canSetWorkItemMetadata;
+    },
+    confidentialTooltip() {
+      return sprintfWorkItem(this.$options.i18n.confidentialTooltip, this.workItemType);
+    },
+    workItemAssignees() {
+      return this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
+    },
+    workItemLabels() {
+      return this.isWidgetPresent(WIDGET_TYPE_LABELS);
+    },
+    workItemDueDate() {
+      return this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE);
+    },
+    workItemWeight() {
+      return this.isWidgetPresent(WIDGET_TYPE_WEIGHT);
+    },
+    workItemProgress() {
+      return this.isWidgetPresent(WIDGET_TYPE_PROGRESS);
+    },
+    workItemIteration() {
+      return this.isWidgetPresent(WIDGET_TYPE_ITERATION);
+    },
+    workItemHealthStatus() {
+      return this.isWidgetPresent(WIDGET_TYPE_HEALTH_STATUS);
+    },
+    workItemMilestone() {
+      return this.isWidgetPresent(WIDGET_TYPE_MILESTONE);
+    },
+  },
+  methods: {
+    isWidgetPresent(type) {
+      return this.workItem?.widgets?.find((widget) => widget.type === type);
+    },
+  },
+};
+</script>
+
+<template>
+  <section>
+    <work-item-state
+      :work-item="workItem"
+      :work-item-parent-id="workItemParentId"
+      :can-update="canUpdate"
+      @error="$emit('error', $event)"
+    />
+    <work-item-assignees
+      v-if="workItemAssignees"
+      :can-update="canUpdate"
+      :work-item-id="workItem.id"
+      :assignees="workItemAssignees.assignees.nodes"
+      :allows-multiple-assignees="workItemAssignees.allowsMultipleAssignees"
+      :work-item-type="workItemType"
+      :can-invite-members="workItemAssignees.canInviteMembers"
+      @error="$emit('error', $event)"
+    />
+    <work-item-labels
+      v-if="workItemLabels"
+      :can-update="canUpdate"
+      :work-item-id="workItem.id"
+      :work-item-iid="workItem.iid"
+      @error="$emit('error', $event)"
+    />
+    <work-item-due-date
+      v-if="workItemDueDate"
+      :can-update="canUpdate"
+      :due-date="workItemDueDate.dueDate"
+      :start-date="workItemDueDate.startDate"
+      :work-item-id="workItem.id"
+      :work-item-type="workItemType"
+      @error="$emit('error', $event)"
+    />
+    <work-item-milestone
+      v-if="workItemMilestone"
+      :work-item-id="workItem.id"
+      :work-item-milestone="workItemMilestone.milestone"
+      :work-item-type="workItemType"
+      :can-update="canUpdate"
+      @error="$emit('error', $event)"
+    />
+    <work-item-weight
+      v-if="workItemWeight"
+      class="gl-mb-5"
+      :can-update="canUpdate"
+      :weight="workItemWeight.weight"
+      :work-item-id="workItem.id"
+      :work-item-iid="workItem.iid"
+      :work-item-type="workItemType"
+      @error="$emit('error', $event)"
+    />
+    <work-item-progress
+      v-if="workItemProgress"
+      class="gl-mb-5"
+      :can-update="canUpdate"
+      :progress="workItemProgress.progress"
+      :work-item-id="workItem.id"
+      :work-item-type="workItemType"
+      @error="$emit('error', $event)"
+    />
+    <work-item-iteration
+      v-if="workItemIteration"
+      class="gl-mb-5"
+      :iteration="workItemIteration.iteration"
+      :can-update="canUpdate"
+      :work-item-id="workItem.id"
+      :work-item-iid="workItem.iid"
+      :work-item-type="workItemType"
+      @error="$emit('error', $event)"
+    />
+    <work-item-health-status
+      v-if="workItemHealthStatus"
+      class="gl-mb-5"
+      :health-status="workItemHealthStatus.healthStatus"
+      :can-update="canUpdate"
+      :work-item-id="workItem.id"
+      :work-item-iid="workItem.iid"
+      :work-item-type="workItemType"
+      @error="$emit('error', $event)"
+    />
+  </section>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 5539226e84e1..3acdedf77aaf 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -22,18 +22,11 @@ import {
   sprintfWorkItem,
   i18n,
   WIDGET_TYPE_ASSIGNEES,
-  WIDGET_TYPE_LABELS,
   WIDGET_TYPE_NOTIFICATIONS,
   WIDGET_TYPE_CURRENT_USER_TODOS,
   WIDGET_TYPE_DESCRIPTION,
   WIDGET_TYPE_AWARD_EMOJI,
-  WIDGET_TYPE_START_AND_DUE_DATE,
-  WIDGET_TYPE_WEIGHT,
-  WIDGET_TYPE_PROGRESS,
   WIDGET_TYPE_HIERARCHY,
-  WIDGET_TYPE_MILESTONE,
-  WIDGET_TYPE_ITERATION,
-  WIDGET_TYPE_HEALTH_STATUS,
   WORK_ITEM_TYPE_VALUE_ISSUE,
   WORK_ITEM_TYPE_VALUE_OBJECTIVE,
   WIDGET_TYPE_NOTES,
@@ -48,17 +41,13 @@ import { findHierarchyWidgetChildren } from '../utils';
 import WorkItemTree from './work_item_links/work_item_tree.vue';
 import WorkItemActions from './work_item_actions.vue';
 import WorkItemTodos from './work_item_todos.vue';
-import WorkItemState from './work_item_state.vue';
 import WorkItemTitle from './work_item_title.vue';
+import WorkItemAttributesWrapper from './work_item_attributes_wrapper.vue';
 import WorkItemCreatedUpdated from './work_item_created_updated.vue';
 import WorkItemDescription from './work_item_description.vue';
-import WorkItemAwardEmoji from './work_item_award_emoji.vue';
-import WorkItemDueDate from './work_item_due_date.vue';
-import WorkItemAssignees from './work_item_assignees.vue';
-import WorkItemLabels from './work_item_labels.vue';
-import WorkItemMilestone from './work_item_milestone.vue';
 import WorkItemNotes from './work_item_notes.vue';
 import WorkItemDetailModal from './work_item_detail_modal.vue';
+import WorkItemAwardEmoji from './work_item_award_emoji.vue';
 
 export default {
   i18n,
@@ -74,23 +63,14 @@ export default {
     GlSkeletonLoader,
     GlIcon,
     GlEmptyState,
-    WorkItemAssignees,
     WorkItemActions,
     WorkItemTodos,
     WorkItemCreatedUpdated,
     WorkItemDescription,
     WorkItemAwardEmoji,
-    WorkItemDueDate,
-    WorkItemLabels,
     WorkItemTitle,
-    WorkItemState,
-    WorkItemWeight: () => import('ee_component/work_items/components/work_item_weight.vue'),
-    WorkItemProgress: () => import('ee_component/work_items/components/work_item_progress.vue'),
+    WorkItemAttributesWrapper,
     WorkItemTypeIcon,
-    WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
-    WorkItemHealthStatus: () =>
-      import('ee_component/work_items/components/work_item_health_status.vue'),
-    WorkItemMilestone,
     WorkItemTree,
     WorkItemNotes,
     WorkItemDetailModal,
@@ -256,33 +236,12 @@ export default {
     workItemAssignees() {
       return this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
     },
-    workItemLabels() {
-      return this.isWidgetPresent(WIDGET_TYPE_LABELS);
-    },
-    workItemDueDate() {
-      return this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE);
-    },
-    workItemWeight() {
-      return this.isWidgetPresent(WIDGET_TYPE_WEIGHT);
-    },
-    workItemProgress() {
-      return this.isWidgetPresent(WIDGET_TYPE_PROGRESS);
-    },
     workItemAwardEmoji() {
       return this.isWidgetPresent(WIDGET_TYPE_AWARD_EMOJI);
     },
     workItemHierarchy() {
       return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY);
     },
-    workItemIteration() {
-      return this.isWidgetPresent(WIDGET_TYPE_ITERATION);
-    },
-    workItemHealthStatus() {
-      return this.isWidgetPresent(WIDGET_TYPE_HEALTH_STATUS);
-    },
-    workItemMilestone() {
-      return this.isWidgetPresent(WIDGET_TYPE_MILESTONE);
-    },
     workItemNotes() {
       return this.isWidgetPresent(WIDGET_TYPE_NOTES);
     },
@@ -511,83 +470,9 @@ export default {
           @error="updateError = $event"
         />
         <work-item-created-updated :work-item-iid="workItemIid" />
-        <work-item-state
+        <work-item-attributes-wrapper
           :work-item="workItem"
           :work-item-parent-id="workItemParentId"
-          :can-update="canUpdate"
-          @error="updateError = $event"
-        />
-        <work-item-assignees
-          v-if="workItemAssignees"
-          :can-update="canUpdate"
-          :work-item-id="workItem.id"
-          :assignees="workItemAssignees.assignees.nodes"
-          :allows-multiple-assignees="workItemAssignees.allowsMultipleAssignees"
-          :work-item-type="workItemType"
-          :can-invite-members="workItemAssignees.canInviteMembers"
-          @error="updateError = $event"
-        />
-        <work-item-labels
-          v-if="workItemLabels"
-          :can-update="canUpdate"
-          :work-item-id="workItem.id"
-          :work-item-iid="workItem.iid"
-          @error="updateError = $event"
-        />
-        <work-item-due-date
-          v-if="workItemDueDate"
-          :can-update="canUpdate"
-          :due-date="workItemDueDate.dueDate"
-          :start-date="workItemDueDate.startDate"
-          :work-item-id="workItem.id"
-          :work-item-type="workItemType"
-          @error="updateError = $event"
-        />
-        <work-item-milestone
-          v-if="workItemMilestone"
-          :work-item-id="workItem.id"
-          :work-item-milestone="workItemMilestone.milestone"
-          :work-item-type="workItemType"
-          :can-update="canUpdate"
-          @error="updateError = $event"
-        />
-        <work-item-weight
-          v-if="workItemWeight"
-          class="gl-mb-5"
-          :can-update="canUpdate"
-          :weight="workItemWeight.weight"
-          :work-item-id="workItem.id"
-          :work-item-iid="workItem.iid"
-          :work-item-type="workItemType"
-          @error="updateError = $event"
-        />
-        <work-item-progress
-          v-if="workItemProgress"
-          class="gl-mb-5"
-          :can-update="canUpdate"
-          :progress="workItemProgress.progress"
-          :work-item-id="workItem.id"
-          :work-item-type="workItemType"
-          @error="updateError = $event"
-        />
-        <work-item-iteration
-          v-if="workItemIteration"
-          class="gl-mb-5"
-          :iteration="workItemIteration.iteration"
-          :can-update="canUpdate"
-          :work-item-id="workItem.id"
-          :work-item-iid="workItem.iid"
-          :work-item-type="workItemType"
-          @error="updateError = $event"
-        />
-        <work-item-health-status
-          v-if="workItemHealthStatus"
-          class="gl-mb-5"
-          :health-status="workItemHealthStatus.healthStatus"
-          :can-update="canUpdate"
-          :work-item-id="workItem.id"
-          :work-item-iid="workItem.iid"
-          :work-item-type="workItemType"
           @error="updateError = $event"
         />
         <work-item-description
diff --git a/ee/spec/frontend/work_items/components/work_item_detail_spec.js b/ee/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
similarity index 64%
rename from ee/spec/frontend/work_items/components/work_item_detail_spec.js
rename to ee/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
index 5ba69c1ea25a..1f9eb448249a 100644
--- a/ee/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/ee/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
@@ -1,61 +1,57 @@
-import { GlAlert } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
 import VueApollo from 'vue-apollo';
-import WorkItemWeight from 'ee/work_items/components/work_item_weight.vue';
+import { shallowMount } from '@vue/test-utils';
 import WorkItemProgress from 'ee/work_items/components/work_item_progress.vue';
-import WorkItemIteration from 'ee/work_items/components/work_item_iteration.vue';
 import WorkItemHealthStatus from 'ee/work_items/components/work_item_health_status.vue';
-import createMockApollo from 'helpers/mock_apollo_helper';
+import WorkItemWeight from 'ee/work_items/components/work_item_weight.vue';
+import WorkItemIteration from 'ee/work_items/components/work_item_iteration.vue';
 import waitForPromises from 'helpers/wait_for_promises';
-import { workItemByIidResponseFactory } from 'jest/work_items/mock_data';
-import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { workItemResponseFactory } from 'jest/work_items/mock_data';
+
+import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue';
 import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
 import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
 import workItemUpdatedSubscription from '~/work_items/graphql/work_item_updated.subscription.graphql';
 
-describe('WorkItemDetail component', () => {
+describe('EE WorkItemAttributesWrapper component', () => {
   let wrapper;
 
   Vue.use(VueApollo);
 
-  const workItemQueryResponse = workItemByIidResponseFactory({ canUpdate: true, canDelete: true });
+  const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true });
+
   const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
   const workItemUpdatedSubscriptionHandler = jest
     .fn()
     .mockResolvedValue({ data: { workItemUpdated: null } });
 
-  const findAlert = () => wrapper.findComponent(GlAlert);
+  const findWorkItemIteration = () => wrapper.findComponent(WorkItemIteration);
   const findWorkItemWeight = () => wrapper.findComponent(WorkItemWeight);
   const findWorkItemProgress = () => wrapper.findComponent(WorkItemProgress);
-  const findWorkItemIteration = () => wrapper.findComponent(WorkItemIteration);
   const findWorkItemHealthStatus = () => wrapper.findComponent(WorkItemHealthStatus);
 
   const createComponent = ({
+    workItem = workItemQueryResponse.data.workItem,
     handler = successHandler,
-    workItemsMvcEnabled = false,
     confidentialityMock = [updateWorkItemMutation, jest.fn()],
   } = {}) => {
-    wrapper = shallowMount(WorkItemDetail, {
+    wrapper = shallowMount(WorkItemAttributesWrapper, {
       apolloProvider: createMockApollo([
         [workItemByIidQuery, handler],
         [workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler],
         confidentialityMock,
       ]),
+      propsData: {
+        workItem,
+      },
       provide: {
-        glFeatures: {
-          workItemsMvc: workItemsMvcEnabled,
-        },
         hasIssueWeightsFeature: true,
         hasIterationsFeature: true,
-        hasIssuableHealthStatusFeature: true,
         hasOkrsFeature: true,
+        hasIssuableHealthStatusFeature: true,
         projectNamespace: 'namespace',
         fullPath: 'group/project',
-        reportAbusePath: '/report/abuse/path',
-      },
-      propsData: {
-        workItemIid: '1',
       },
     });
   };
@@ -69,24 +65,22 @@ describe('WorkItemDetail component', () => {
       it(`${
         iterationWidgetPresent ? 'renders' : 'does not render'
       } iteration component`, async () => {
-        const response = workItemByIidResponseFactory({ iterationWidgetPresent });
-        const handler = jest.fn().mockResolvedValue(response);
-        createComponent({ handler });
+        const response = workItemResponseFactory({ iterationWidgetPresent });
+        createComponent({ workItem: response.data.workItem });
         await waitForPromises();
 
         expect(findWorkItemIteration().exists()).toBe(exists);
       });
     });
 
-    it('shows an error message when it emits an `error` event', async () => {
+    it('emits an error event to the wrapper', async () => {
       createComponent();
-      await waitForPromises();
       const updateError = 'Failed to update';
 
       findWorkItemIteration().vm.$emit('error', updateError);
-      await waitForPromises();
+      await nextTick();
 
-      expect(findAlert().text()).toBe(updateError);
+      expect(wrapper.emitted('error')).toEqual([[updateError]]);
     });
   });
 
@@ -96,25 +90,23 @@ describe('WorkItemDetail component', () => {
       ${'when widget is returned from API'}     | ${true}             | ${true}
       ${'when widget is not returned from API'} | ${false}            | ${false}
     `('$description', ({ weightWidgetPresent, exists }) => {
-      it(`${weightWidgetPresent ? 'renders' : 'does not render'} weight component`, async () => {
-        const response = workItemByIidResponseFactory({ weightWidgetPresent });
-        const handler = jest.fn().mockResolvedValue(response);
-        createComponent({ handler });
-        await waitForPromises();
+      it(`${weightWidgetPresent ? 'renders' : 'does not render'} weight component`, () => {
+        const response = workItemResponseFactory({ weightWidgetPresent });
+        createComponent({ workItem: response.data.workItem });
 
         expect(findWorkItemWeight().exists()).toBe(exists);
       });
     });
 
-    it('shows an error message when it emits an `error` event', async () => {
-      createComponent();
-      await waitForPromises();
+    it('emits an error event to the wrapper', async () => {
+      const response = workItemResponseFactory({ weightWidgetPresent: true });
+      createComponent({ workItem: response.data.workItem });
       const updateError = 'Failed to update';
 
       findWorkItemWeight().vm.$emit('error', updateError);
-      await waitForPromises();
+      await nextTick();
 
-      expect(findAlert().text()).toBe(updateError);
+      expect(wrapper.emitted('error')).toEqual([[updateError]]);
     });
   });
 
@@ -126,25 +118,23 @@ describe('WorkItemDetail component', () => {
     `('$description', ({ healthStatusWidgetPresent, exists }) => {
       it(`${
         healthStatusWidgetPresent ? 'renders' : 'does not render'
-      } healthStatus component`, async () => {
-        const response = workItemByIidResponseFactory({ healthStatusWidgetPresent });
-        const handler = jest.fn().mockResolvedValue(response);
-        createComponent({ handler });
-        await waitForPromises();
+      } healthStatus component`, () => {
+        const response = workItemResponseFactory({ healthStatusWidgetPresent });
+        createComponent({ workItem: response.data.workItem });
 
         expect(findWorkItemHealthStatus().exists()).toBe(exists);
       });
     });
 
-    it('shows an error message when it emits an `error` event', async () => {
-      createComponent();
-      await waitForPromises();
+    it('emits an error event to the wrapper', async () => {
+      const response = workItemResponseFactory({ healthStatusWidgetPresent: true });
+      createComponent({ workItem: response.data.workItem });
       const updateError = 'Failed to update';
 
       findWorkItemHealthStatus().vm.$emit('error', updateError);
-      await waitForPromises();
+      await nextTick();
 
-      expect(findAlert().text()).toBe(updateError);
+      expect(wrapper.emitted('error')).toEqual([[updateError]]);
     });
   });
 
@@ -154,27 +144,23 @@ describe('WorkItemDetail component', () => {
       ${'when widget is returned from API'}     | ${true}               | ${true}
       ${'when widget is not returned from API'} | ${false}              | ${false}
     `('$description', ({ progressWidgetPresent, exists }) => {
-      it(`${
-        progressWidgetPresent ? 'renders' : 'does not render'
-      } progress component`, async () => {
-        const response = workItemByIidResponseFactory({ progressWidgetPresent });
-        const handler = jest.fn().mockResolvedValue(response);
-        createComponent({ handler });
-        await waitForPromises();
+      it(`${progressWidgetPresent ? 'renders' : 'does not render'} progress component`, () => {
+        const response = workItemResponseFactory({ progressWidgetPresent });
+        createComponent({ workItem: response.data.workItem });
 
         expect(findWorkItemProgress().exists()).toBe(exists);
       });
     });
 
-    it('shows an error message when it emits an `error` event', async () => {
-      createComponent();
-      await waitForPromises();
+    it('emits an error event to the wrapper', async () => {
+      const response = workItemResponseFactory({ progressWidgetPresent: true });
+      createComponent({ workItem: response.data.workItem });
       const updateError = 'Failed to update';
 
       findWorkItemProgress().vm.$emit('error', updateError);
-      await waitForPromises();
+      await nextTick();
 
-      expect(findAlert().text()).toBe(updateError);
+      expect(wrapper.emitted('error')).toEqual([[updateError]]);
     });
   });
 });
diff --git a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
new file mode 100644
index 000000000000..ba9af7b2b686
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
@@ -0,0 +1,107 @@
+import { shallowMount } from '@vue/test-utils';
+import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
+import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
+import WorkItemState from '~/work_items/components/work_item_state.vue';
+import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
+import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
+
+import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue';
+import { workItemResponseFactory } from '../mock_data';
+
+describe('WorkItemAttributesWrapper component', () => {
+  let wrapper;
+
+  const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true });
+
+  const findWorkItemState = () => wrapper.findComponent(WorkItemState);
+  const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
+  const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
+  const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels);
+  const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestone);
+
+  const createComponent = ({ workItem = workItemQueryResponse.data.workItem } = {}) => {
+    wrapper = shallowMount(WorkItemAttributesWrapper, {
+      propsData: {
+        workItem,
+      },
+      provide: {
+        hasIssueWeightsFeature: true,
+        hasIterationsFeature: true,
+        hasOkrsFeature: true,
+        hasIssuableHealthStatusFeature: true,
+        projectNamespace: 'namespace',
+        fullPath: 'group/project',
+      },
+      stubs: {
+        WorkItemWeight: true,
+        WorkItemIteration: true,
+        WorkItemHealthStatus: true,
+      },
+    });
+  };
+
+  describe('work item state', () => {
+    it('renders the work item state', () => {
+      createComponent();
+
+      expect(findWorkItemState().exists()).toBe(true);
+    });
+  });
+
+  describe('assignees widget', () => {
+    it('renders assignees component when widget is returned from the API', () => {
+      createComponent();
+
+      expect(findWorkItemAssignees().exists()).toBe(true);
+    });
+
+    it('does not render assignees component when widget is not returned from the API', () => {
+      createComponent({
+        workItem: workItemResponseFactory({ assigneesWidgetPresent: false }).data.workItem,
+      });
+
+      expect(findWorkItemAssignees().exists()).toBe(false);
+    });
+  });
+
+  describe('labels widget', () => {
+    it.each`
+      description                                               | labelsWidgetPresent | exists
+      ${'renders when widget is returned from API'}             | ${true}             | ${true}
+      ${'does not render when widget is not returned from API'} | ${false}            | ${false}
+    `('$description', ({ labelsWidgetPresent, exists }) => {
+      const response = workItemResponseFactory({ labelsWidgetPresent });
+      createComponent({ workItem: response.data.workItem });
+
+      expect(findWorkItemLabels().exists()).toBe(exists);
+    });
+  });
+
+  describe('dates widget', () => {
+    describe.each`
+      description                               | datesWidgetPresent | exists
+      ${'when widget is returned from API'}     | ${true}            | ${true}
+      ${'when widget is not returned from API'} | ${false}           | ${false}
+    `('$description', ({ datesWidgetPresent, exists }) => {
+      it(`${datesWidgetPresent ? 'renders' : 'does not render'} due date component`, () => {
+        const response = workItemResponseFactory({ datesWidgetPresent });
+        createComponent({ workItem: response.data.workItem });
+
+        expect(findWorkItemDueDate().exists()).toBe(exists);
+      });
+    });
+  });
+
+  describe('milestone widget', () => {
+    it.each`
+      description                                               | milestoneWidgetPresent | exists
+      ${'renders when widget is returned from API'}             | ${true}                | ${true}
+      ${'does not render when widget is not returned from API'} | ${false}               | ${false}
+    `('$description', ({ milestoneWidgetPresent, exists }) => {
+      const response = workItemResponseFactory({ milestoneWidgetPresent });
+      createComponent({ workItem: response.data.workItem });
+
+      expect(findWorkItemMilestone().exists()).toBe(exists);
+    });
+  });
+});
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 0266533a11ca..14a6ada16bd2 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -18,12 +18,8 @@ import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
 import WorkItemActions from '~/work_items/components/work_item_actions.vue';
 import WorkItemDescription from '~/work_items/components/work_item_description.vue';
 import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue';
-import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
-import WorkItemState from '~/work_items/components/work_item_state.vue';
+import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue';
 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 WorkItemNotes from '~/work_items/components/work_item_notes.vue';
 import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
@@ -69,12 +65,8 @@ describe('WorkItemDetail component', () => {
   const findWorkItemActions = () => wrapper.findComponent(WorkItemActions);
   const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
   const findCreatedUpdated = () => wrapper.findComponent(WorkItemCreatedUpdated);
-  const findWorkItemState = () => wrapper.findComponent(WorkItemState);
   const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription);
-  const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
-  const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
-  const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels);
-  const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestone);
+  const findWorkItemAttributesWrapper = () => wrapper.findComponent(WorkItemAttributesWrapper);
   const findParent = () => wrapper.find('[data-testid="work-item-parent"]');
   const findParentButton = () => findParent().findComponent(GlButton);
   const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]');
@@ -168,7 +160,6 @@ describe('WorkItemDetail component', () => {
 
     it('renders skeleton loader', () => {
       expect(findSkeleton().exists()).toBe(true);
-      expect(findWorkItemState().exists()).toBe(false);
       expect(findWorkItemTitle().exists()).toBe(false);
     });
   });
@@ -181,7 +172,6 @@ describe('WorkItemDetail component', () => {
 
     it('does not render skeleton', () => {
       expect(findSkeleton().exists()).toBe(false);
-      expect(findWorkItemState().exists()).toBe(true);
       expect(findWorkItemTitle().exists()).toBe(true);
     });
 
@@ -480,83 +470,6 @@ describe('WorkItemDetail component', () => {
 
     expect(findAlert().text()).toBe(updateError);
   });
-  describe('assignees widget', () => {
-    it('renders assignees component when widget is returned from the API', async () => {
-      createComponent();
-      await waitForPromises();
-
-      expect(findWorkItemAssignees().exists()).toBe(true);
-    });
-
-    it('does not render assignees component when widget is not returned from the API', async () => {
-      createComponent({
-        handler: jest
-          .fn()
-          .mockResolvedValue(workItemByIidResponseFactory({ assigneesWidgetPresent: false })),
-      });
-      await waitForPromises();
-
-      expect(findWorkItemAssignees().exists()).toBe(false);
-    });
-  });
-
-  describe('labels widget', () => {
-    it.each`
-      description                                               | labelsWidgetPresent | exists
-      ${'renders when widget is returned from API'}             | ${true}             | ${true}
-      ${'does not render when widget is not returned from API'} | ${false}            | ${false}
-    `('$description', async ({ labelsWidgetPresent, exists }) => {
-      const response = workItemByIidResponseFactory({ labelsWidgetPresent });
-      const handler = jest.fn().mockResolvedValue(response);
-      createComponent({ handler });
-      await waitForPromises();
-
-      expect(findWorkItemLabels().exists()).toBe(exists);
-    });
-  });
-
-  describe('dates widget', () => {
-    describe.each`
-      description                               | datesWidgetPresent | exists
-      ${'when widget is returned from API'}     | ${true}            | ${true}
-      ${'when widget is not returned from API'} | ${false}           | ${false}
-    `('$description', ({ datesWidgetPresent, exists }) => {
-      it(`${datesWidgetPresent ? 'renders' : 'does not render'} due date component`, async () => {
-        const response = workItemByIidResponseFactory({ datesWidgetPresent });
-        const handler = jest.fn().mockResolvedValue(response);
-        createComponent({ handler });
-        await waitForPromises();
-
-        expect(findWorkItemDueDate().exists()).toBe(exists);
-      });
-    });
-
-    it('shows an error message when it emits an `error` event', async () => {
-      createComponent();
-      await waitForPromises();
-      const updateError = 'Failed to update';
-
-      findWorkItemDueDate().vm.$emit('error', updateError);
-      await waitForPromises();
-
-      expect(findAlert().text()).toBe(updateError);
-    });
-  });
-
-  describe('milestone widget', () => {
-    it.each`
-      description                                               | milestoneWidgetPresent | exists
-      ${'renders when widget is returned from API'}             | ${true}                | ${true}
-      ${'does not render when widget is not returned from API'} | ${false}               | ${false}
-    `('$description', async ({ milestoneWidgetPresent, exists }) => {
-      const response = workItemByIidResponseFactory({ milestoneWidgetPresent });
-      const handler = jest.fn().mockResolvedValue(response);
-      createComponent({ handler });
-      await waitForPromises();
-
-      expect(findWorkItemMilestone().exists()).toBe(exists);
-    });
-  });
 
   it('calls the work item query', async () => {
     createComponent();
@@ -713,4 +626,24 @@ describe('WorkItemDetail component', () => {
       expect(findWorkItemTodos().exists()).toBe(false);
     });
   });
+
+  describe('work item attributes wrapper', () => {
+    beforeEach(async () => {
+      createComponent();
+      await waitForPromises();
+    });
+
+    it('renders the work item attributes wrapper', () => {
+      expect(findWorkItemAttributesWrapper().exists()).toBe(true);
+    });
+
+    it('shows an error message when it emits an `error` event', async () => {
+      const updateError = 'Failed to update';
+
+      findWorkItemAttributesWrapper().vm.$emit('error', updateError);
+      await waitForPromises();
+
+      expect(findAlert().text()).toBe(updateError);
+    });
+  });
 });
-- 
GitLab