diff --git a/app/assets/javascripts/homepage/components/homepage_app.vue b/app/assets/javascripts/homepage/components/homepage_app.vue
index 1b9176a1c5915320985cda327199bb6dd208ecf9..009d66d31a77e67ba3742e285d7d5e60ba486f5e 100644
--- a/app/assets/javascripts/homepage/components/homepage_app.vue
+++ b/app/assets/javascripts/homepage/components/homepage_app.vue
@@ -1,26 +1,47 @@
@@ -65,16 +193,52 @@ export default {
-
-
+
+
+
-
-
+
diff --git a/app/assets/javascripts/homepage/components/merge_requests_widget.vue b/app/assets/javascripts/homepage/components/merge_requests_widget.vue
deleted file mode 100644
index d1267d63bcaf97a6c2f2d8db4f94dd52f5b2811c..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/homepage/components/merge_requests_widget.vue
+++ /dev/null
@@ -1,196 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- {{
- s__(
- 'HomePageMergeRequestsWidget|The number of merge requests is not available. Please refresh the page to try again, or visit the dashboard.',
- )
- }}
-
-
-
-
-
-
- {{ reviewRequestedCount }}
-
-
-
-
- {{ s__('HomePageMergeRequestsWidget|Merge requests waiting for your review') }}
-
-
- {{ timeFormatted(reviewRequestedLastUpdatedAt) }}
-
-
-
-
-
-
-
-
-
-
- {{
- s__(
- 'HomePageMergeRequestsWidget|The number of merge requests is not available. Please refresh the page to try again, or visit the dashboard.',
- )
- }}
-
-
-
-
-
-
- {{ assignedCount }}
-
-
-
-
- {{ s__('HomePageMergeRequestsWidget|Merge requests assigned to you') }}
-
-
- {{ timeFormatted(assignedLastUpdatedAt) }}
-
-
-
-
-
-
-
diff --git a/app/assets/javascripts/homepage/components/user_items_count_widget.vue b/app/assets/javascripts/homepage/components/user_items_count_widget.vue
new file mode 100644
index 0000000000000000000000000000000000000000..3099f8c63ae152f0c474dc5185230430977f8a60
--- /dev/null
+++ b/app/assets/javascripts/homepage/components/user_items_count_widget.vue
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+ {{ errorText }}
+
+
+
+
+
+
+ {{ formattedCount }}
+
+
+
+
+ {{ linkText }}
+
+
+ {{ timeFormatted(lastUpdatedAt) }}
+
+
+
+
diff --git a/app/assets/javascripts/homepage/components/work_items_widget.vue b/app/assets/javascripts/homepage/components/work_items_widget.vue
deleted file mode 100644
index 892ff20a90d86ac23a795067e9bccad7993fd50d..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/homepage/components/work_items_widget.vue
+++ /dev/null
@@ -1,207 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- {{
- s__(
- 'HomePageWorkItemsWidget|The number of issues is not available. Please refresh the page to try again, or visit the issue list.',
- )
- }}
-
-
-
-
-
-
- {{ s__('HomePageWorkItemsWidget|Issues assigned to you') }}
-
-
-
- {{ assignedCount }}
-
-
-
-
-
- {{ timeFormatted(assignedLastUpdatedAt) }}
-
-
-
-
-
-
-
-
-
-
- {{
- s__(
- 'HomePageWorkItemsWidget|The number of issues is not available. Please refresh the page to try again, or visit the issue list.',
- )
- }}
-
-
-
-
-
-
- {{ s__('HomePageWorkItemsWidget|Issues authored by you') }}
-
-
-
- {{ authoredCount }}
-
-
-
-
-
- {{ timeFormatted(authoredLastUpdatedAt) }}
-
-
-
-
-
-
-
diff --git a/app/assets/javascripts/homepage/tracking_constants.js b/app/assets/javascripts/homepage/tracking_constants.js
index 256a0307cf5f157fb74898289e28cafcef62f979..b5ba8506d5f6de782539740c954b0d128dc40e08 100644
--- a/app/assets/javascripts/homepage/tracking_constants.js
+++ b/app/assets/javascripts/homepage/tracking_constants.js
@@ -4,7 +4,7 @@ export const EVENT_USER_CLICKS_LINK_ON_ACTIVITY_FEED = 'user_clicks_link_in_acti
// Labels
export const TRACKING_LABEL_MERGE_REQUESTS = 'Merge requests';
-export const TRACKING_LABEL_ISSUES = 'Issues';
+export const TRACKING_LABEL_WORK_ITEMS = 'Issues';
export const TRACKING_LABEL_TODO_ITEMS = 'To-do items';
export const TRACKING_LABEL_RECENTLY_VIEWED = 'Recently viewed';
export const TRACKING_LABEL_FEEDBACK = 'Feedback';
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ac84afc016beb35c36566f915ac66f09562b2227..0e284bc7e5feead3ee028b8555146fd68097c940 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -33442,9 +33442,6 @@ msgstr ""
msgid "HomePageMergeRequestsWidget|Merge requests waiting for your review"
msgstr ""
-msgid "HomePageMergeRequestsWidget|Merge requests with review requested"
-msgstr ""
-
msgid "HomePageMergeRequestsWidget|The number of merge requests is not available. Please refresh the page to try again, or visit the dashboard."
msgstr ""
@@ -33463,12 +33460,6 @@ msgstr ""
msgid "HomePageWorkItemsWidget|The number of issues is not available. Please refresh the page to try again, or visit the issue list."
msgstr ""
-msgid "HomePageWorkItemsWidget|Work items assigned to you"
-msgstr ""
-
-msgid "HomePageWorkItemsWidget|Work items authored by you"
-msgstr ""
-
msgid "Homepage"
msgstr ""
diff --git a/spec/frontend/homepage/components/homepage_app_spec.js b/spec/frontend/homepage/components/homepage_app_spec.js
index 5df38b7a4500368f2bf1a08a988489ef044f7a14..4d225c3786a371f3cd6fcc2625fa600e25b5208a 100644
--- a/spec/frontend/homepage/components/homepage_app_spec.js
+++ b/spec/frontend/homepage/components/homepage_app_spec.js
@@ -1,27 +1,74 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
import HomepageApp from '~/homepage/components/homepage_app.vue';
-import MergeRequestsWidget from '~/homepage/components/merge_requests_widget.vue';
-import WorkItemsWidget from '~/homepage/components/work_items_widget.vue';
import PickUpWidget from '~/homepage/components/pick_up_widget.vue';
import FeedbackWidget from '~/homepage/components/feedback_widget.vue';
+import BaseWidget from '~/homepage/components/base_widget.vue';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import mergeRequestsWidgetMetadataQuery from '~/homepage/graphql/queries/merge_requests_widget_metadata.query.graphql';
+import workItemsWidgetMetadataQuery from '~/homepage/graphql/queries/work_items_widget_metadata.query.graphql';
+import {
+ EVENT_USER_FOLLOWS_LINK_ON_HOMEPAGE,
+ TRACKING_LABEL_MERGE_REQUESTS,
+ TRACKING_LABEL_WORK_ITEMS,
+ TRACKING_PROPERTY_REVIEW_REQUESTED,
+ TRACKING_PROPERTY_ASSIGNED_TO_YOU,
+ TRACKING_PROPERTY_AUTHORED_BY_YOU,
+} from '~/homepage/tracking_constants';
+import { userCounts } from '~/super_sidebar/user_counts_manager';
import { lastPushEvent } from './mocks/last_push_event_mock';
+import { mergeRequestsDataWithItems } from './mocks/merge_requests_widget_metadata_query_mocks';
+import { workItemsDataWithItems } from './mocks/work_items_widget_metadata_query_mocks';
+
+jest.mock('~/super_sidebar/user_counts_manager', () => ({
+ userCounts: { assigned_issues: 0 },
+ createUserCountsManager: jest.fn(),
+ useCachedUserCounts: jest.fn(),
+}));
+jest.mock('~/sentry/sentry_browser_wrapper');
+jest.mock('~/super_sidebar/user_counts_fetch');
describe('HomepageApp', () => {
const MOCK_MERGE_REQUESTS_REVIEW_REQUESTED_PATH = '/merge/requests/review/requested/path';
+ const MOCK_MERGE_REQUESTS_REVIEW_REQUESTED_ERROR_TEXT =
+ 'The number of merge requests is not available. Please refresh the page to try again, or visit the dashboard.';
+ const MOCK_MERGE_REQUESTS_REVIEW_REQUESTED_TEXT = 'Merge requests waiting for your review';
const MOCK_ASSIGNED_MERGE_REQUESTS_PATH = '/merge/requests/assigned/to/you/path';
+ const MOCK_ASSIGNED_MERGE_REQUESTS_ERROR_TEXT =
+ 'The number of merge requests is not available. Please refresh the page to try again, or visit the dashboard.';
+ const MOCK_ASSIGNED_MERGE_REQUESTS_TEXT = 'Merge requests assigned to you';
const MOCK_ASSIGNED_WORK_ITEMS_PATH = '/work/items/assigned/to/you/path';
+ const MOCK_ASSIGNED_WORK_ITEMS_ERROR_TEXT =
+ 'The number of issues is not available. Please refresh the page to try again, or visit the issue list.';
+ const MOCK_ASSIGNED_WORK_ITEMS_TEXT = 'Issues assigned to you';
const MOCK_AUTHORED_WORK_ITEMS_PATH = '/work/items/authored/to/you/path';
+ const MOCK_AUTHORED_WORK_ITEMS_ERROR_TEXT =
+ 'The number of issues is not available. Please refresh the page to try again, or visit the issue list.';
+ const MOCK_AUTHORED_WORK_ITEMS_TEXT = 'Issues authored by you';
const MOCK_ACTIVITY_PATH = '/activity/path';
+ const MOCK_DUO_CODE_REVIEW_BOT_USERNAME = 'GitLabDuo';
let wrapper;
- const findMergeRequestsWidget = () => wrapper.findComponent(MergeRequestsWidget);
- const findWorkItemsWidget = () => wrapper.findComponent(WorkItemsWidget);
+ const findReviewRequestedWidget = () => wrapper.findByTestId('review-requested-widget');
+ const findAssignedMergeRequestsWidget = () =>
+ wrapper.findByTestId('assigned-merge-requests-widget');
+ const findAssignedWorkItemsWidget = () => wrapper.findByTestId('assigned-work-items-widget');
+ const findAuthoredWorkItemsWidget = () => wrapper.findByTestId('authored-work-items-widget');
+ const findBaseWidget = () => wrapper.findComponent(BaseWidget);
const findPickUpWidget = () => wrapper.findComponent(PickUpWidget);
const findFeedbackWidget = () => wrapper.findComponent(FeedbackWidget);
function createWrapper(props = {}) {
wrapper = shallowMountExtended(HomepageApp, {
+ provide: {
+ duoCodeReviewBotUsername: MOCK_DUO_CODE_REVIEW_BOT_USERNAME,
+ },
propsData: {
reviewRequestedPath: MOCK_MERGE_REQUESTS_REVIEW_REQUESTED_PATH,
assignedMergeRequestsPath: MOCK_ASSIGNED_MERGE_REQUESTS_PATH,
@@ -34,21 +81,263 @@ describe('HomepageApp', () => {
});
}
+ const mergeRequestsWidgetMetadataQuerySuccessHandler = (data) =>
+ jest.fn().mockResolvedValue(data);
+
+ const workItemsWidgetMetadataQuerySuccessHandler = (data) => jest.fn().mockResolvedValue(data);
+
+ function createApolloWrapper({
+ mergeRequestsWidgetMetadataQueryHandler = mergeRequestsWidgetMetadataQuerySuccessHandler(
+ mergeRequestsDataWithItems,
+ ),
+ workItemsWidgetMetadataQueryHandler = workItemsWidgetMetadataQuerySuccessHandler(
+ workItemsDataWithItems,
+ ),
+ assignedWorkItemsCount = 5,
+ } = {}) {
+ userCounts.assigned_issues = assignedWorkItemsCount;
+
+ const mockApollo = createMockApollo([
+ [mergeRequestsWidgetMetadataQuery, mergeRequestsWidgetMetadataQueryHandler],
+ [workItemsWidgetMetadataQuery, workItemsWidgetMetadataQueryHandler],
+ ]);
+ wrapper = shallowMountExtended(HomepageApp, {
+ apolloProvider: mockApollo,
+ provide: {
+ duoCodeReviewBotUsername: MOCK_DUO_CODE_REVIEW_BOT_USERNAME,
+ },
+ propsData: {
+ reviewRequestedPath: MOCK_MERGE_REQUESTS_REVIEW_REQUESTED_PATH,
+ assignedMergeRequestsPath: MOCK_ASSIGNED_MERGE_REQUESTS_PATH,
+ assignedWorkItemsPath: MOCK_ASSIGNED_WORK_ITEMS_PATH,
+ authoredWorkItemsPath: MOCK_AUTHORED_WORK_ITEMS_PATH,
+ activityPath: MOCK_ACTIVITY_PATH,
+ lastPushEvent,
+ },
+ stubs: {
+ GlSprintf,
+ BaseWidget,
+ },
+ });
+ }
+
beforeEach(() => {
createWrapper();
});
- it('passes the correct props to the `MergeRequestsWidget` component', () => {
- expect(findMergeRequestsWidget().props()).toEqual({
- reviewRequestedPath: MOCK_MERGE_REQUESTS_REVIEW_REQUESTED_PATH,
- assignedToYouPath: MOCK_ASSIGNED_MERGE_REQUESTS_PATH,
- });
+ afterEach(() => {
+ userCounts.assigned_issues = 0;
});
- it('passes the correct props to the `WorkItemsWidget` component', () => {
- expect(findWorkItemsWidget().props()).toEqual({
- assignedToYouPath: MOCK_ASSIGNED_WORK_ITEMS_PATH,
- authoredByYouPath: MOCK_AUTHORED_WORK_ITEMS_PATH,
+ describe('userItemsCountWidgets', () => {
+ Vue.use(VueApollo);
+
+ beforeEach(() => {
+ createApolloWrapper();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('passes the correct props to the `review requested widget`', async () => {
+ await waitForPromises();
+
+ expect(findReviewRequestedWidget().props()).toEqual({
+ errorText: MOCK_MERGE_REQUESTS_REVIEW_REQUESTED_ERROR_TEXT,
+ hasError: false,
+ linkText: MOCK_MERGE_REQUESTS_REVIEW_REQUESTED_TEXT,
+ path: MOCK_MERGE_REQUESTS_REVIEW_REQUESTED_PATH,
+ userItems: mergeRequestsDataWithItems.data.currentUser.reviewRequestedMergeRequests,
+ iconName: 'merge-request',
+ });
+ });
+
+ it('passes the correct props to the `assigned merge requests widget`', async () => {
+ await waitForPromises();
+
+ expect(findAssignedMergeRequestsWidget().props()).toEqual({
+ errorText: MOCK_ASSIGNED_MERGE_REQUESTS_ERROR_TEXT,
+ hasError: false,
+ linkText: MOCK_ASSIGNED_MERGE_REQUESTS_TEXT,
+ path: MOCK_ASSIGNED_MERGE_REQUESTS_PATH,
+ userItems: mergeRequestsDataWithItems.data.currentUser.assignedMergeRequests,
+ iconName: 'merge-request',
+ });
+ });
+
+ it('passes the correct props to the `assigned work items widget`', async () => {
+ await waitForPromises();
+
+ expect(findAssignedWorkItemsWidget().props()).toEqual({
+ errorText: MOCK_ASSIGNED_WORK_ITEMS_ERROR_TEXT,
+ hasError: false,
+ linkText: MOCK_ASSIGNED_WORK_ITEMS_TEXT,
+ path: MOCK_ASSIGNED_WORK_ITEMS_PATH,
+ userItems: workItemsDataWithItems.data.currentUser.assigned,
+ iconName: 'work-item-issue',
+ });
+ });
+
+ it('passes the correct props to the `authored work items widget`', async () => {
+ await waitForPromises();
+
+ expect(findAuthoredWorkItemsWidget().props()).toEqual({
+ errorText: MOCK_AUTHORED_WORK_ITEMS_ERROR_TEXT,
+ hasError: false,
+ linkText: MOCK_AUTHORED_WORK_ITEMS_TEXT,
+ path: MOCK_AUTHORED_WORK_ITEMS_PATH,
+ userItems: workItemsDataWithItems.data.currentUser.authored,
+ iconName: 'work-item-issue',
+ });
+ });
+
+ describe('query errors', () => {
+ it('provides error to both merge request widgets, if the query errors out', async () => {
+ createApolloWrapper({
+ mergeRequestsWidgetMetadataQueryHandler: () => jest.fn().mockRejectedValue(),
+ });
+
+ await waitForPromises();
+
+ expect(findReviewRequestedWidget().props()).toEqual(
+ expect.objectContaining({
+ hasError: true,
+ userItems: null,
+ }),
+ );
+ expect(findAssignedMergeRequestsWidget().props()).toEqual(
+ expect.objectContaining({
+ hasError: true,
+ userItems: null,
+ }),
+ );
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
+
+ it('provides error to both work item widgets, if the query errors out', async () => {
+ createApolloWrapper({
+ workItemsWidgetMetadataQueryHandler: () => jest.fn().mockRejectedValue(),
+ });
+
+ await waitForPromises();
+
+ expect(findAssignedWorkItemsWidget().props()).toEqual(
+ expect.objectContaining({
+ hasError: true,
+ userItems: null,
+ }),
+ );
+ expect(findAuthoredWorkItemsWidget().props()).toEqual(
+ expect.objectContaining({
+ hasError: true,
+ userItems: null,
+ }),
+ );
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
+ });
+
+ describe('tracking', () => {
+ const { bindInternalEventDocument } = useMockInternalEventsTracking();
+
+ beforeEach(async () => {
+ createWrapper();
+ await waitForPromises();
+ });
+
+ it('tracks emit of review requested widget', () => {
+ const { trackEventSpy } = bindInternalEventDocument(wrapper.element);
+
+ findReviewRequestedWidget().vm.$emit('click-link');
+
+ expect(trackEventSpy).toHaveBeenCalledWith(
+ EVENT_USER_FOLLOWS_LINK_ON_HOMEPAGE,
+ {
+ label: TRACKING_LABEL_MERGE_REQUESTS,
+ property: TRACKING_PROPERTY_REVIEW_REQUESTED,
+ },
+ undefined,
+ );
+ });
+
+ it('tracks emit of assigned merge requests widget', () => {
+ const { trackEventSpy } = bindInternalEventDocument(wrapper.element);
+
+ findAssignedMergeRequestsWidget().vm.$emit('click-link');
+
+ expect(trackEventSpy).toHaveBeenCalledWith(
+ EVENT_USER_FOLLOWS_LINK_ON_HOMEPAGE,
+ {
+ label: TRACKING_LABEL_MERGE_REQUESTS,
+ property: TRACKING_PROPERTY_ASSIGNED_TO_YOU,
+ },
+ undefined,
+ );
+ });
+
+ it('tracks emit of assigned work items widget', () => {
+ const { trackEventSpy } = bindInternalEventDocument(wrapper.element);
+
+ findAssignedWorkItemsWidget().vm.$emit('click-link');
+
+ expect(trackEventSpy).toHaveBeenCalledWith(
+ EVENT_USER_FOLLOWS_LINK_ON_HOMEPAGE,
+ {
+ label: TRACKING_LABEL_WORK_ITEMS,
+ property: TRACKING_PROPERTY_ASSIGNED_TO_YOU,
+ },
+ undefined,
+ );
+ });
+
+ it('tracks emit of authored work items widget', () => {
+ const { trackEventSpy } = bindInternalEventDocument(wrapper.element);
+
+ findAuthoredWorkItemsWidget().vm.$emit('click-link');
+
+ expect(trackEventSpy).toHaveBeenCalledWith(
+ EVENT_USER_FOLLOWS_LINK_ON_HOMEPAGE,
+ {
+ label: TRACKING_LABEL_WORK_ITEMS,
+ property: TRACKING_PROPERTY_AUTHORED_BY_YOU,
+ },
+ undefined,
+ );
+ });
+ });
+
+ describe('BaseWidget integration', () => {
+ it('renders BaseWidget without styling', () => {
+ const baseWidget = findBaseWidget();
+
+ expect(baseWidget.exists()).toBe(true);
+ expect(baseWidget.props('applyDefaultStyling')).toBe(false);
+ });
+
+ it('handles visible event from BaseWidget', async () => {
+ const mergeRequestsHandler = jest.fn().mockResolvedValue(mergeRequestsDataWithItems);
+ const workItemsHandler = jest.fn().mockResolvedValue(workItemsDataWithItems);
+
+ createApolloWrapper({
+ mergeRequestsWidgetMetadataQueryHandler: mergeRequestsHandler,
+ workItemsWidgetMetadataQueryHandler: workItemsHandler,
+ });
+
+ await waitForPromises();
+
+ // queried on initial mount
+ expect(mergeRequestsHandler).toHaveBeenCalledTimes(1);
+ expect(workItemsHandler).toHaveBeenCalledTimes(1);
+
+ const baseWidget = findBaseWidget();
+ baseWidget.vm.$emit('visible');
+
+ await waitForPromises();
+
+ // queried after visibility change
+ expect(mergeRequestsHandler).toHaveBeenCalledTimes(2);
+ });
});
});
diff --git a/spec/frontend/homepage/components/merge_requests_widget_spec.js b/spec/frontend/homepage/components/merge_requests_widget_spec.js
deleted file mode 100644
index a351786e9bdc986082d790d4528832285155f157..0000000000000000000000000000000000000000
--- a/spec/frontend/homepage/components/merge_requests_widget_spec.js
+++ /dev/null
@@ -1,420 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import { GlSprintf, GlLink } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { useFakeDate } from 'helpers/fake_date';
-import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
-import MergeRequestsWidget from '~/homepage/components/merge_requests_widget.vue';
-import BaseWidget from '~/homepage/components/base_widget.vue';
-import * as Sentry from '~/sentry/sentry_browser_wrapper';
-import mergeRequestsWidgetMetadataQuery from '~/homepage/graphql/queries/merge_requests_widget_metadata.query.graphql';
-import {
- EVENT_USER_FOLLOWS_LINK_ON_HOMEPAGE,
- TRACKING_LABEL_MERGE_REQUESTS,
- TRACKING_PROPERTY_REVIEW_REQUESTED,
- TRACKING_PROPERTY_ASSIGNED_TO_YOU,
-} from '~/homepage/tracking_constants';
-import { withItems, withoutItems } from './mocks/merge_requests_widget_metadata_query_mocks';
-
-jest.mock('~/sentry/sentry_browser_wrapper');
-
-describe('MergeRequestsWidget', () => {
- Vue.use(VueApollo);
-
- const MOCK_DUO_CODE_REVIEW_BOT_USERNAME = 'GitLabDuo';
- const MOCK_REVIEW_REQUESTED_PATH = '/review/requested/path';
- const MOCK_ASSIGNED_TO_YOU_PATH = '/assigned/to/you/path';
- const MOCK_CURRENT_TIME = new Date('2025-06-12T18:13:25Z');
-
- useFakeDate(MOCK_CURRENT_TIME);
-
- const mergeRequestsWidgetMetadataQuerySuccessHandler = (data) =>
- jest.fn().mockResolvedValue(data);
-
- let wrapper;
-
- const findBaseWidget = () => wrapper.findComponent(BaseWidget);
- const findReviewRequestedCard = () => wrapper.findAllComponents(GlLink).at(0);
- const findAssignedToYouCard = () => wrapper.findAllComponents(GlLink).at(1);
- const findReviewRequestedCount = () => wrapper.findByTestId('review-requested-count');
- const findReviewRequestedLastUpdatedAt = () =>
- wrapper.findByTestId('review-requested-last-updated-at');
- const findAssignedCount = () => wrapper.findByTestId('assigned-count');
- const findAssignedLastUpdatedAt = () => wrapper.findByTestId('assigned-last-updated-at');
-
- afterEach(() => {
- jest.restoreAllMocks();
- });
-
- function createWrapper({
- mergeRequestsWidgetMetadataQueryHandler = mergeRequestsWidgetMetadataQuerySuccessHandler(
- withItems,
- ),
- } = {}) {
- const mockApollo = createMockApollo([
- [mergeRequestsWidgetMetadataQuery, mergeRequestsWidgetMetadataQueryHandler],
- ]);
- wrapper = shallowMountExtended(MergeRequestsWidget, {
- apolloProvider: mockApollo,
- provide: {
- duoCodeReviewBotUsername: MOCK_DUO_CODE_REVIEW_BOT_USERNAME,
- },
- propsData: {
- reviewRequestedPath: MOCK_REVIEW_REQUESTED_PATH,
- assignedToYouPath: MOCK_ASSIGNED_TO_YOU_PATH,
- },
- stubs: {
- GlSprintf,
- BaseWidget,
- },
- });
- }
-
- describe('cards', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('renders the "Review requested" card', () => {
- const card = findReviewRequestedCard();
-
- expect(card.exists()).toBe(true);
- expect(card.text()).toMatch('Merge requests waiting for your review');
- });
-
- it('renders the "Assigned to you" card', () => {
- const card = findAssignedToYouCard();
-
- expect(card.exists()).toBe(true);
- expect(card.text()).toMatch('Merge requests assigned to you');
- });
- });
-
- describe('metadata', () => {
- it("shows the counts' loading state and no timestamp until the query has resolved", () => {
- createWrapper();
-
- expect(findReviewRequestedLastUpdatedAt().exists()).toBe(false);
- expect(findAssignedLastUpdatedAt().exists()).toBe(false);
-
- expect(findReviewRequestedCount().text()).toBe('-');
- expect(findAssignedCount().text()).toBe('-');
- });
-
- it('shows the metadata once the query has resolved', async () => {
- createWrapper();
- await waitForPromises();
-
- expect(findReviewRequestedCount().text()).toBe('12');
- expect(findReviewRequestedLastUpdatedAt().text()).toBe('3 hours ago');
- expect(findAssignedCount().text()).toBe('4');
- expect(findAssignedLastUpdatedAt().text()).toBe('2 days ago');
- });
-
- it('shows partial metadata when the user has no relevant items', async () => {
- createWrapper({
- mergeRequestsWidgetMetadataQueryHandler:
- mergeRequestsWidgetMetadataQuerySuccessHandler(withoutItems),
- });
- await waitForPromises();
-
- expect(findReviewRequestedLastUpdatedAt().exists()).toBe(false);
- expect(findAssignedLastUpdatedAt().exists()).toBe(false);
-
- expect(findReviewRequestedCount().text()).toBe('0');
- expect(findAssignedCount().text()).toBe('0');
- });
-
- it('shows error messages in both cards if the query errors out', async () => {
- createWrapper({
- mergeRequestsWidgetMetadataQueryHandler: () => jest.fn().mockRejectedValue(),
- });
- await waitForPromises();
-
- expect(findReviewRequestedCard().text()).toContain(
- 'The number of merge requests is not available. Please refresh the page to try again, or visit the dashboard.',
- );
- expect(findAssignedToYouCard().text()).toContain(
- 'The number of merge requests is not available. Please refresh the page to try again, or visit the dashboard.',
- );
- expect(Sentry.captureException).toHaveBeenCalled();
-
- expect(findReviewRequestedCard().text()).not.toMatch(
- 'Merge requests waiting for your review',
- );
- expect(findAssignedToYouCard().text()).not.toMatch('Merge requests assigned to you');
- });
-
- it('shows error icons in both cards when in error state', async () => {
- createWrapper({
- mergeRequestsWidgetMetadataQueryHandler: () => jest.fn().mockRejectedValue(),
- });
- await waitForPromises();
-
- const allIcons = wrapper.findAllComponents({ name: 'GlIcon' });
-
- let errorIconCount = 0;
- for (let i = 0; i < allIcons.length; i += 1) {
- const icon = allIcons.at(i);
- if (icon.props('name') === 'error') {
- expect(icon.props('size')).toBe(16);
- expect(icon.classes('gl-text-red-500')).toBe(true);
- errorIconCount += 1;
- }
- }
-
- expect(errorIconCount).toBe(2);
- });
- });
-
- describe('BaseWidget integration', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('renders BaseWidget without styling', () => {
- const baseWidget = findBaseWidget();
-
- expect(baseWidget.exists()).toBe(true);
- expect(baseWidget.props('applyDefaultStyling')).toBe(false);
- });
-
- it('handles visible event from BaseWidget', async () => {
- const mergeRequestsWidgetMetadataQueryHandler =
- mergeRequestsWidgetMetadataQuerySuccessHandler(withItems);
-
- createWrapper({ mergeRequestsWidgetMetadataQueryHandler });
- await waitForPromises();
-
- // queried on initial mount
- expect(mergeRequestsWidgetMetadataQueryHandler).toHaveBeenCalledTimes(1);
-
- const baseWidget = findBaseWidget();
- baseWidget.vm.$emit('visible');
-
- await waitForPromises();
-
- // queried after visibility change
- expect(mergeRequestsWidgetMetadataQueryHandler).toHaveBeenCalledTimes(2);
- });
-
- it('calls reload method directly', async () => {
- createWrapper();
- await waitForPromises();
-
- const refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.metadata, 'refetch');
-
- wrapper.vm.reload();
-
- expect(wrapper.vm.hasError).toBe(false);
- expect(refetchSpy).toHaveBeenCalled();
- });
-
- it('handles visibility change when document is not hidden', async () => {
- let mockTime = Date.now();
- jest.spyOn(Date, 'now').mockImplementation(() => mockTime);
-
- const mergeRequestsWidgetMetadataQueryHandler =
- mergeRequestsWidgetMetadataQuerySuccessHandler(withItems);
-
- createWrapper({ mergeRequestsWidgetMetadataQueryHandler });
- await waitForPromises();
-
- Object.defineProperty(document, 'hidden', {
- writable: true,
- value: false,
- });
-
- // Initial visibility change - sets timestamp but doesn't reload
- document.dispatchEvent(new Event('visibilitychange'));
- expect(mergeRequestsWidgetMetadataQueryHandler).toHaveBeenCalledTimes(1);
-
- // Advance time and trigger another visibility change
- mockTime += 6000; // 6 seconds
- document.dispatchEvent(new Event('visibilitychange'));
-
- await waitForPromises();
- expect(mergeRequestsWidgetMetadataQueryHandler).toHaveBeenCalledTimes(2);
-
- Date.now.mockRestore();
- });
-
- it('does not reload when document is hidden', async () => {
- const mergeRequestsWidgetMetadataQueryHandler =
- mergeRequestsWidgetMetadataQuerySuccessHandler(withItems);
-
- createWrapper({ mergeRequestsWidgetMetadataQueryHandler });
- await waitForPromises();
-
- Object.defineProperty(document, 'hidden', {
- writable: true,
- value: true,
- });
-
- document.dispatchEvent(new Event('visibilitychange'));
- await waitForPromises();
-
- // Should not have queried again since document is hidden
- expect(mergeRequestsWidgetMetadataQueryHandler).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('number formatting', () => {
- it('formats large counts using formatCount', async () => {
- const mockData = {
- data: {
- currentUser: {
- id: 'gid://gitlab/User/1',
- reviewRequestedMergeRequests: {
- count: 25000,
- nodes: [
- {
- id: 'gid://gitlab/MergeRequest/1',
- updatedAt: '2025-06-11T18:13:25Z',
- __typename: 'MergeRequest',
- },
- ],
- __typename: 'MergeRequestConnection',
- },
- assignedMergeRequests: {
- count: 750000,
- nodes: [
- {
- id: 'gid://gitlab/MergeRequest/2',
- updatedAt: '2025-06-10T18:13:25Z',
- __typename: 'MergeRequest',
- },
- ],
- __typename: 'MergeRequestConnection',
- },
- __typename: 'CurrentUser',
- },
- },
- };
-
- createWrapper({
- mergeRequestsWidgetMetadataQueryHandler:
- mergeRequestsWidgetMetadataQuerySuccessHandler(mockData),
- });
- await waitForPromises();
-
- expect(findReviewRequestedCount().text()).toBe('25K');
- expect(findAssignedCount().text()).toBe('750K');
- });
-
- it('formats small counts without grouping', async () => {
- const mockData = {
- data: {
- currentUser: {
- id: 'gid://gitlab/User/1',
- reviewRequestedMergeRequests: {
- count: 1234,
- nodes: [
- {
- id: 'gid://gitlab/MergeRequest/1',
- updatedAt: '2025-06-11T18:13:25Z',
- __typename: 'MergeRequest',
- },
- ],
- __typename: 'MergeRequestConnection',
- },
- assignedMergeRequests: {
- count: 5678,
- nodes: [
- {
- id: 'gid://gitlab/MergeRequest/2',
- updatedAt: '2025-06-10T18:13:25Z',
- __typename: 'MergeRequest',
- },
- ],
- __typename: 'MergeRequestConnection',
- },
- __typename: 'CurrentUser',
- },
- },
- };
-
- createWrapper({
- mergeRequestsWidgetMetadataQueryHandler:
- mergeRequestsWidgetMetadataQuerySuccessHandler(mockData),
- });
- await waitForPromises();
-
- expect(findReviewRequestedCount().text()).toBe('1234');
- expect(findAssignedCount().text()).toBe('5678');
- });
-
- it('formats negative counts correctly', async () => {
- const mockData = {
- data: {
- currentUser: {
- id: 'gid://gitlab/User/1',
- reviewRequestedMergeRequests: {
- count: -1234,
- nodes: [],
- __typename: 'MergeRequestConnection',
- },
- assignedMergeRequests: {
- count: -25000,
- nodes: [],
- __typename: 'MergeRequestConnection',
- },
- __typename: 'CurrentUser',
- },
- },
- };
-
- createWrapper({
- mergeRequestsWidgetMetadataQueryHandler:
- mergeRequestsWidgetMetadataQuerySuccessHandler(mockData),
- });
- await waitForPromises();
-
- expect(findReviewRequestedCount().text()).toBe('-1234');
- expect(findAssignedCount().text()).toBe('-25K');
- });
- });
-
- describe('tracking', () => {
- const { bindInternalEventDocument } = useMockInternalEventsTracking();
-
- beforeEach(async () => {
- createWrapper();
- await waitForPromises();
- });
-
- it('tracks click on "Review requested" card', () => {
- const { trackEventSpy } = bindInternalEventDocument(wrapper.element);
- const reviewRequestedCard = findReviewRequestedCard();
-
- reviewRequestedCard.vm.$emit('click');
-
- expect(trackEventSpy).toHaveBeenCalledWith(
- EVENT_USER_FOLLOWS_LINK_ON_HOMEPAGE,
- {
- label: TRACKING_LABEL_MERGE_REQUESTS,
- property: TRACKING_PROPERTY_REVIEW_REQUESTED,
- },
- undefined,
- );
- });
-
- it('tracks click on "Assigned to you" card', () => {
- const { trackEventSpy } = bindInternalEventDocument(wrapper.element);
- const assignedCard = findAssignedToYouCard();
-
- assignedCard.vm.$emit('click');
-
- expect(trackEventSpy).toHaveBeenCalledWith(
- EVENT_USER_FOLLOWS_LINK_ON_HOMEPAGE,
- {
- label: TRACKING_LABEL_MERGE_REQUESTS,
- property: TRACKING_PROPERTY_ASSIGNED_TO_YOU,
- },
- undefined,
- );
- });
- });
-});
diff --git a/spec/frontend/homepage/components/mocks/merge_requests_widget_metadata_query_mocks.js b/spec/frontend/homepage/components/mocks/merge_requests_widget_metadata_query_mocks.js
index b94ec8bcc59a50d5d37aab01928f95d24c0ec4ef..da716076b676491886676be1b54affa0f00d9cbd 100644
--- a/spec/frontend/homepage/components/mocks/merge_requests_widget_metadata_query_mocks.js
+++ b/spec/frontend/homepage/components/mocks/merge_requests_widget_metadata_query_mocks.js
@@ -1,4 +1,4 @@
-export const withItems = {
+export const mergeRequestsDataWithItems = {
data: {
currentUser: {
id: 'gid://gitlab/User/1',
@@ -29,7 +29,7 @@ export const withItems = {
},
};
-export const withoutItems = {
+export const mergeRequestsDataWithoutItems = {
data: {
currentUser: {
id: 'gid://gitlab/User/1',
@@ -47,3 +47,34 @@ export const withoutItems = {
},
},
};
+
+export const mergeRequestsDataWithHugeCount = {
+ data: {
+ currentUser: {
+ id: 'gid://gitlab/User/1',
+ reviewRequestedMergeRequests: {
+ count: 750000,
+ nodes: [
+ {
+ __typename: 'MergeRequest',
+ id: 'gid://gitlab/MergeRequest/30',
+ updatedAt: '2025-06-12T15:13:25Z',
+ },
+ ],
+ __typename: 'MergeRequestConnection',
+ },
+ assignedMergeRequests: {
+ count: 25000,
+ nodes: [
+ {
+ __typename: 'MergeRequest',
+ id: 'gid://gitlab/MergeRequest/30',
+ updatedAt: '2025-06-12T15:13:25Z',
+ },
+ ],
+ __typename: 'MergeRequestConnection',
+ },
+ __typename: 'CurrentUser',
+ },
+ },
+};
diff --git a/spec/frontend/homepage/components/mocks/work_items_widget_metadata_query_mocks.js b/spec/frontend/homepage/components/mocks/work_items_widget_metadata_query_mocks.js
index 69b49171fc28add09a565a4fecc155c9d0e96e2b..18cab5501cdfbb4f83be500192fabfc9711cafd9 100644
--- a/spec/frontend/homepage/components/mocks/work_items_widget_metadata_query_mocks.js
+++ b/spec/frontend/homepage/components/mocks/work_items_widget_metadata_query_mocks.js
@@ -1,4 +1,4 @@
-export const withItems = {
+export const workItemsDataWithItems = {
data: {
currentUser: {
id: 'gid://gitlab/User/1',
@@ -29,7 +29,7 @@ export const withItems = {
},
};
-export const withoutItems = {
+export const workItemsDataWithoutItems = {
data: {
currentUser: {
id: 'gid://gitlab/User/1',
diff --git a/spec/frontend/homepage/components/user_items_count_widget_spec.js b/spec/frontend/homepage/components/user_items_count_widget_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..57dd3fe32d6202b5ed63d4b7535b7233bed7b5a3
--- /dev/null
+++ b/spec/frontend/homepage/components/user_items_count_widget_spec.js
@@ -0,0 +1,114 @@
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { useFakeDate } from 'helpers/fake_date';
+import BaseWidget from '~/homepage/components/base_widget.vue';
+import UserItemsCountWidget from '~/homepage/components/user_items_count_widget.vue';
+import {
+ mergeRequestsDataWithItems,
+ mergeRequestsDataWithoutItems,
+ mergeRequestsDataWithHugeCount,
+} from './mocks/merge_requests_widget_metadata_query_mocks';
+
+describe('UserItemsCountWidget', () => {
+ const MOCK_DUO_CODE_REVIEW_BOT_USERNAME = 'GitLabDuo';
+ const MOCK_PATH = '/merge/requests/review/requested/path';
+ const MOCK_ARIA_LABEL = 'Merge requests with review requested';
+ const MOCK_ERROR_TEXT =
+ 'The number of merge requests is not available. Please refresh the page to try again, or visit the dashboard.';
+ const MOCK_LINK_TEXT = 'Merge requests waiting for your review';
+ const MOCK_CURRENT_TIME = new Date('2025-06-12T18:13:25Z');
+
+ useFakeDate(MOCK_CURRENT_TIME);
+
+ let wrapper;
+
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findCount = () => wrapper.findByTestId('count');
+ const findLastUpdatedAt = () => wrapper.findByTestId('last-updated-at');
+
+ function createWrapper(props = {}) {
+ wrapper = shallowMountExtended(UserItemsCountWidget, {
+ provide: {
+ duoCodeReviewBotUsername: MOCK_DUO_CODE_REVIEW_BOT_USERNAME,
+ },
+ propsData: {
+ hasError: false,
+ path: MOCK_PATH,
+ linkAriaLabel: MOCK_ARIA_LABEL,
+ errorText: MOCK_ERROR_TEXT,
+ linkText: MOCK_LINK_TEXT,
+ userItems: mergeRequestsDataWithItems.data.currentUser.reviewRequestedMergeRequests,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ BaseWidget,
+ },
+ });
+ }
+
+ describe('with valid data', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('shows the metadata', () => {
+ expect(findCount().text()).toBe('12');
+ expect(findLastUpdatedAt().text()).toBe('3 hours ago');
+ });
+
+ it('formats large counts using formatCount', async () => {
+ createWrapper({
+ userItems: mergeRequestsDataWithHugeCount.data.currentUser.reviewRequestedMergeRequests,
+ });
+ await waitForPromises();
+
+ expect(findCount().text()).toBe('750K');
+ });
+
+ it('tracks click on card', () => {
+ findLink().vm.$emit('click');
+
+ expect(wrapper.emitted('click-link')).toHaveLength(1);
+ });
+ });
+
+ describe('with missing data', () => {
+ it("shows the counts' loading state and no timestamp when workitems is null", () => {
+ createWrapper({ userItems: null });
+
+ expect(findLastUpdatedAt().exists()).toBe(false);
+
+ expect(findCount().text()).toBe('-');
+ });
+
+ it('shows partial metadata when the user has no relevant items', () => {
+ createWrapper({
+ userItems: mergeRequestsDataWithoutItems.data.currentUser.reviewRequestedMergeRequests,
+ });
+
+ expect(findLastUpdatedAt().exists()).toBe(false);
+
+ expect(findCount().text()).toBe('0');
+ });
+ });
+
+ describe('with error', () => {
+ it('shows error message', () => {
+ createWrapper({ hasError: true });
+
+ expect(findLink().text()).toContain(MOCK_ERROR_TEXT);
+
+ expect(findLink().text()).not.toMatch(MOCK_LINK_TEXT);
+ });
+
+ it('shows error icon', () => {
+ createWrapper({ hasError: true });
+
+ const errorIcon = wrapper.findComponent({ name: 'GlIcon' });
+
+ expect(errorIcon.exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/homepage/components/work_items_widget_spec.js b/spec/frontend/homepage/components/work_items_widget_spec.js
deleted file mode 100644
index 53424222c509daf7fe372931cc4b1eaea1bed411..0000000000000000000000000000000000000000
--- a/spec/frontend/homepage/components/work_items_widget_spec.js
+++ /dev/null
@@ -1,407 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import { GlLink, GlSprintf } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { useFakeDate } from 'helpers/fake_date';
-import WorkItemsWidget from '~/homepage/components/work_items_widget.vue';
-import BaseWidget from '~/homepage/components/base_widget.vue';
-import * as Sentry from '~/sentry/sentry_browser_wrapper';
-import workItemsWidgetMetadataQuery from '~/homepage/graphql/queries/work_items_widget_metadata.query.graphql';
-import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
-import {
- EVENT_USER_FOLLOWS_LINK_ON_HOMEPAGE,
- TRACKING_LABEL_ISSUES,
- TRACKING_PROPERTY_ASSIGNED_TO_YOU,
- TRACKING_PROPERTY_AUTHORED_BY_YOU,
-} from '~/homepage/tracking_constants';
-import { userCounts } from '~/super_sidebar/user_counts_manager';
-import { withItems, withoutItems } from './mocks/work_items_widget_metadata_query_mocks';
-
-jest.mock('~/super_sidebar/user_counts_manager', () => ({
- userCounts: { assigned_issues: 0 },
- createUserCountsManager: jest.fn(),
- useCachedUserCounts: jest.fn(),
-}));
-jest.mock('~/sentry/sentry_browser_wrapper');
-jest.mock('~/super_sidebar/user_counts_fetch');
-
-describe('WorkItemsWidget', () => {
- Vue.use(VueApollo);
-
- const MOCK_ASSIGNED_TO_YOU_PATH = '/assigned/to/you/path';
- const MOCK_AUTHORED_BY_YOU_PATH = '/authored/to/you/path';
- const MOCK_CURRENT_TIME = new Date('2025-06-29T18:13:25Z');
-
- useFakeDate(MOCK_CURRENT_TIME);
-
- const workItemsWidgetMetadataQuerySuccessHandler = (data) => jest.fn().mockResolvedValue(data);
-
- let wrapper;
-
- const findBaseWidget = () => wrapper.findComponent(BaseWidget);
- const findAssignedCard = () => wrapper.findAllComponents(GlLink).at(0);
- const findAuthoredCard = () => wrapper.findAllComponents(GlLink).at(1);
- const findAssignedCount = () => wrapper.findByTestId('assigned-count');
- const findAssignedLastUpdatedAt = () => wrapper.findByTestId('assigned-last-updated-at');
- const findAuthoredCount = () => wrapper.findByTestId('authored-count');
- const findAuthoredLastUpdatedAt = () => wrapper.findByTestId('authored-last-updated-at');
-
- afterEach(() => {
- jest.restoreAllMocks();
- });
-
- function createWrapper({
- workItemsWidgetMetadataQueryHandler = workItemsWidgetMetadataQuerySuccessHandler(withItems),
- assignedIssuesCount = 0,
- } = {}) {
- userCounts.assigned_issues = assignedIssuesCount;
-
- const mockApollo = createMockApollo([
- [workItemsWidgetMetadataQuery, workItemsWidgetMetadataQueryHandler],
- ]);
- wrapper = shallowMountExtended(WorkItemsWidget, {
- apolloProvider: mockApollo,
- propsData: {
- assignedToYouPath: MOCK_ASSIGNED_TO_YOU_PATH,
- authoredByYouPath: MOCK_AUTHORED_BY_YOU_PATH,
- },
- stubs: {
- GlSprintf,
- BaseWidget,
- },
- });
- }
-
- afterEach(() => {
- userCounts.assigned_issues = 0;
- });
-
- describe('cards', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('renders the "Issues assigned to you" card', () => {
- const card = findAssignedCard();
-
- expect(card.exists()).toBe(true);
- expect(card.text()).toMatch('Issues assigned to you');
- });
-
- it('renders the "Issues authored by you" card', () => {
- const card = findAuthoredCard();
-
- expect(card.exists()).toBe(true);
- expect(card.text()).toMatch('Issues authored by you');
- });
- });
-
- describe('metadata', () => {
- it('shows the metadata once the query has resolved', async () => {
- createWrapper();
- await waitForPromises();
-
- expect(findAssignedLastUpdatedAt().text()).toBe('1 day ago');
- expect(findAuthoredLastUpdatedAt().text()).toBe('4 days ago');
- });
-
- it('shows partial metadata when the user has no relevant items', async () => {
- createWrapper({
- workItemsWidgetMetadataQueryHandler:
- workItemsWidgetMetadataQuerySuccessHandler(withoutItems),
- });
- await waitForPromises();
-
- expect(findAssignedLastUpdatedAt().exists()).toBe(false);
- expect(findAuthoredLastUpdatedAt().exists()).toBe(false);
-
- expect(findAssignedCount().text()).toBe('0');
- expect(findAuthoredCount().text()).toBe('0');
- });
-
- it('shows error messages in both cards if the query errors out', async () => {
- createWrapper({
- workItemsWidgetMetadataQueryHandler: () => jest.fn().mockRejectedValue(),
- });
- await waitForPromises();
-
- expect(findAssignedCard().text()).toContain(
- 'The number of issues is not available. Please refresh the page to try again, or visit the issue list.',
- );
- expect(findAuthoredCard().text()).toContain(
- 'The number of issues is not available. Please refresh the page to try again, or visit the issue list.',
- );
- expect(Sentry.captureException).toHaveBeenCalled();
-
- expect(findAssignedCard().text()).not.toMatch('Issues assigned to you');
- expect(findAuthoredCard().text()).not.toMatch('Issues authored by you');
- });
-
- it('shows error icons in both cards when in error state', async () => {
- createWrapper({
- workItemsWidgetMetadataQueryHandler: () => jest.fn().mockRejectedValue(),
- });
- await waitForPromises();
- const allIcons = wrapper.findAllComponents({ name: 'GlIcon' });
-
- let errorIconCount = 0;
- for (let i = 0; i < allIcons.length; i += 1) {
- const icon = allIcons.at(i);
- if (icon.props('name') === 'error') {
- expect(icon.props('size')).toBe(16);
- expect(icon.classes('gl-text-red-500')).toBe(true);
- errorIconCount += 1;
- }
- }
-
- expect(errorIconCount).toBe(2);
- });
- });
-
- describe('BaseWidget integration', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('renders BaseWidget without styling', () => {
- const baseWidget = findBaseWidget();
-
- expect(baseWidget.exists()).toBe(true);
- expect(baseWidget.props('applyDefaultStyling')).toBe(false);
- });
-
- it('handles visible event from BaseWidget', async () => {
- const workItemsWidgetMetadataQueryHandler =
- workItemsWidgetMetadataQuerySuccessHandler(withItems);
-
- createWrapper({ workItemsWidgetMetadataQueryHandler });
- await waitForPromises();
- expect(workItemsWidgetMetadataQueryHandler).toHaveBeenCalledTimes(1);
-
- const baseWidget = findBaseWidget();
- baseWidget.vm.$emit('visible');
-
- await waitForPromises();
- expect(workItemsWidgetMetadataQueryHandler).toHaveBeenCalledTimes(2);
- });
-
- it('calls reload method directly', async () => {
- createWrapper();
- await waitForPromises();
-
- const refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.metadata, 'refetch');
-
- wrapper.vm.reload();
-
- expect(wrapper.vm.hasError).toBe(false);
- expect(refetchSpy).toHaveBeenCalled();
- });
-
- it('handles visibility change when document is not hidden', async () => {
- let mockTime = Date.now();
- jest.spyOn(Date, 'now').mockImplementation(() => mockTime);
-
- const workItemsWidgetMetadataQueryHandler =
- workItemsWidgetMetadataQuerySuccessHandler(withItems);
-
- createWrapper({ workItemsWidgetMetadataQueryHandler });
- await waitForPromises();
-
- Object.defineProperty(document, 'hidden', {
- writable: true,
- value: false,
- });
-
- // Initial visibility change - sets timestamp but doesn't reload
- document.dispatchEvent(new Event('visibilitychange'));
- expect(workItemsWidgetMetadataQueryHandler).toHaveBeenCalledTimes(1);
-
- // Advance time and trigger another visibility change
- mockTime += 6000; // 6 seconds
- document.dispatchEvent(new Event('visibilitychange'));
-
- await waitForPromises();
- expect(workItemsWidgetMetadataQueryHandler).toHaveBeenCalledTimes(2);
-
- Date.now.mockRestore();
- });
-
- it('does not reload when document is hidden', async () => {
- const workItemsWidgetMetadataQueryHandler =
- workItemsWidgetMetadataQuerySuccessHandler(withItems);
-
- createWrapper({ workItemsWidgetMetadataQueryHandler });
- await waitForPromises();
-
- Object.defineProperty(document, 'hidden', {
- writable: true,
- value: true,
- });
-
- document.dispatchEvent(new Event('visibilitychange'));
- await waitForPromises();
-
- // Should not have queried again since document is hidden
- expect(workItemsWidgetMetadataQueryHandler).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('number formatting', () => {
- it('formats large counts using formatNumberWithScale', async () => {
- const mockData = {
- data: {
- currentUser: {
- id: 'gid://gitlab/User/1',
- assigned: {
- count: 15000,
- nodes: [
- {
- id: 'gid://gitlab/WorkItem/1',
- updatedAt: '2025-06-28T18:13:25Z',
- __typename: 'WorkItem',
- },
- ],
- __typename: 'WorkItemConnection',
- },
- authored: {
- count: 1500000,
- nodes: [
- {
- id: 'gid://gitlab/WorkItem/2',
- updatedAt: '2025-06-25T18:13:25Z',
- __typename: 'WorkItem',
- },
- ],
- __typename: 'WorkItemConnection',
- },
- __typename: 'CurrentUser',
- },
- },
- };
-
- createWrapper({
- workItemsWidgetMetadataQueryHandler: workItemsWidgetMetadataQuerySuccessHandler(mockData),
- assignedIssuesCount: 15000,
- });
- await waitForPromises();
-
- expect(findAssignedCount().text()).toBe('15K');
- expect(findAuthoredCount().text()).toBe('1.5M');
- });
-
- it('formats small counts with grouping', async () => {
- const mockData = {
- data: {
- currentUser: {
- id: 'gid://gitlab/User/1',
- assigned: {
- count: 1234,
- nodes: [
- {
- id: 'gid://gitlab/WorkItem/1',
- updatedAt: '2025-06-28T18:13:25Z',
- __typename: 'WorkItem',
- },
- ],
- __typename: 'WorkItemConnection',
- },
- authored: {
- count: 5678,
- nodes: [
- {
- id: 'gid://gitlab/WorkItem/2',
- updatedAt: '2025-06-25T18:13:25Z',
- __typename: 'WorkItem',
- },
- ],
- __typename: 'WorkItemConnection',
- },
- __typename: 'CurrentUser',
- },
- },
- };
-
- createWrapper({
- workItemsWidgetMetadataQueryHandler: workItemsWidgetMetadataQuerySuccessHandler(mockData),
- assignedIssuesCount: 1234,
- });
- await waitForPromises();
-
- expect(findAssignedCount().text()).toBe('1,234');
- expect(findAuthoredCount().text()).toBe('5,678');
- });
-
- it('formats negative counts correctly', async () => {
- const mockData = {
- data: {
- currentUser: {
- id: 'gid://gitlab/User/1',
- assigned: {
- count: -1234,
- nodes: [],
- __typename: 'WorkItemConnection',
- },
- authored: {
- count: -25000,
- nodes: [],
- __typename: 'WorkItemConnection',
- },
- __typename: 'CurrentUser',
- },
- },
- };
-
- createWrapper({
- workItemsWidgetMetadataQueryHandler: workItemsWidgetMetadataQuerySuccessHandler(mockData),
- assignedIssuesCount: -1234,
- });
- await waitForPromises();
-
- expect(findAssignedCount().text()).toBe('-1,234');
- expect(findAuthoredCount().text()).toBe('-25K');
- });
- });
-
- describe('tracking', () => {
- const { bindInternalEventDocument } = useMockInternalEventsTracking();
-
- beforeEach(async () => {
- createWrapper();
- await waitForPromises();
- });
-
- it('tracks click on "Issues assigned to you" card', () => {
- const { trackEventSpy } = bindInternalEventDocument(wrapper.element);
- const assignedCard = findAssignedCard();
-
- assignedCard.vm.$emit('click');
-
- expect(trackEventSpy).toHaveBeenCalledWith(
- EVENT_USER_FOLLOWS_LINK_ON_HOMEPAGE,
- {
- label: TRACKING_LABEL_ISSUES,
- property: TRACKING_PROPERTY_ASSIGNED_TO_YOU,
- },
- undefined,
- );
- });
-
- it('tracks click on "Issues authored by you" card', () => {
- const { trackEventSpy } = bindInternalEventDocument(wrapper.element);
- const authoredCard = findAuthoredCard();
-
- authoredCard.vm.$emit('click');
-
- expect(trackEventSpy).toHaveBeenCalledWith(
- EVENT_USER_FOLLOWS_LINK_ON_HOMEPAGE,
- {
- label: TRACKING_LABEL_ISSUES,
- property: TRACKING_PROPERTY_AUTHORED_BY_YOU,
- },
- undefined,
- );
- });
- });
-});