Repository: gitify-app/gitify
Pull request #814 (2024-02-22 22:59:50)

feat: support icons/colors for checksuite / workflow notifications
Closes: #767 

setchy: I have implemented 3 of the specific icon types. 

For the others, we can add them as follow up PRs once we have our hands on an example / recreation 

Patch:
diff --git a/src/components/NotificationRow.tsx b/src/components/NotificationRow.tsx
index 360df284..44c049c0 100644
--- a/src/components/NotificationRow.tsx
+++ b/src/components/NotificationRow.tsx
@@ -53,11 +53,8 @@ export const NotificationRow: React.FC<IProps> = ({
   };
 
   const reason = formatReason(notification.reason);
-  const NotificationIcon = getNotificationTypeIcon(
-    notification.subject.type,
-    notification.subject.state,
-  );
-  const iconColor = getNotificationTypeIconColor(notification.subject.state);
+  const NotificationIcon = getNotificationTypeIcon(notification.subject);
+  const iconColor = getNotificationTypeIconColor(notification.subject);
   const realIconColor = settings
     ? (settings.colors && iconColor) || ''
     : iconColor;
diff --git a/src/typesGithub.ts b/src/typesGithub.ts
index bbe02f13..0f6a087e 100644
--- a/src/typesGithub.ts
+++ b/src/typesGithub.ts
@@ -37,6 +37,21 @@ export type StateType = IssueStateType | PullRequestStateType;
 
 export type ViewerSubscription = 'IGNORED' | 'SUBSCRIBED' | 'UNSUBSCRIBED';
 
+export type CheckSuiteStatus =
+  | 'action_required'
+  | 'cancelled'
+  | 'completed'
+  | 'failure'
+  | 'in_progress'
+  | 'pending'
+  | 'queued'
+  | 'requested'
+  | 'skipped'
+  | 'stale'
+  | 'success'
+  | 'timed_out'
+  | 'waiting';
+
 export interface Notification {
   id: string;
   unread: boolean;
diff --git a/src/utils/github-api.ts b/src/utils/github-api.ts
index 10811d5b..eff85454 100644
--- a/src/utils/github-api.ts
+++ b/src/utils/github-api.ts
@@ -1,5 +1,6 @@
 import {
   AlertIcon,
+  CheckIcon,
   CommentDiscussionIcon,
   GitCommitIcon,
   GitMergeIcon,
@@ -13,10 +14,13 @@ import {
   MailIcon,
   OcticonProps,
   QuestionIcon,
+  SkipIcon,
+  StopIcon,
   SyncIcon,
   TagIcon,
+  XIcon,
 } from '@primer/octicons-react';
-import { Reason, StateType, SubjectType } from '../typesGithub';
+import { CheckSuiteStatus, Reason, Subject } from '../typesGithub';
 
 // prettier-ignore
 const DESCRIPTIONS = {
@@ -77,18 +81,30 @@ export function formatReason(reason: Reason): {
 }
 
 export function getNotificationTypeIcon(
-  type: SubjectType,
-  state?: StateType,
+  subject: Subject,
 ): React.FC<OcticonProps> {
-  switch (type) {
+  switch (subject.type) {
     case 'CheckSuite':
-      return SyncIcon;
+      const checkSuiteState = inferCheckSuiteStatus(subject.title);
+
+      switch (checkSuiteState) {
+        case 'cancelled':
+          return StopIcon;
+        case 'failure':
+          return XIcon;
+        case 'skipped':
+          return SkipIcon;
+        case 'success':
+          return CheckIcon;
+        default:
+          return SyncIcon;
+      }
     case 'Commit':
       return GitCommitIcon;
     case 'Discussion':
       return CommentDiscussionIcon;
     case 'Issue':
-      switch (state) {
+      switch (subject.state) {
         case 'draft':
           return IssueDraftIcon;
         case 'closed':
@@ -100,7 +116,7 @@ export function getNotificationTypeIcon(
           return IssueOpenedIcon;
       }
     case 'PullRequest':
-      switch (state) {
+      switch (subject.state) {
         case 'draft':
           return GitPullRequestDraftIcon;
         case 'closed':
@@ -121,8 +137,25 @@ export function getNotificationTypeIcon(
   }
 }
 
-export function getNotificationTypeIconColor(state: StateType): string {
-  switch (state) {
+export function getNotificationTypeIconColor(subject: Subject): string {
+  if (subject.type === 'CheckSuite') {
+    const checkSuiteState = inferCheckSuiteStatus(subject.title);
+
+    switch (checkSuiteState) {
+      case 'cancelled':
+        return 'text-gray-500';
+      case 'failure':
+        return 'text-red-500';
+      case 'skipped':
+        return 'text-gray-500';
+      case 'success':
+        return 'text-green-500';
+      default:
+        return 'text-gray-300';
+    }
+  }
+
+  switch (subject.state) {
     case 'closed':
       return 'text-red-500';
     case 'completed':
@@ -141,3 +174,27 @@ export function getNotificationTypeIconColor(state: StateType): string {
       return 'text-gray-300';
   }
 }
+
+export function inferCheckSuiteStatus(title: string): CheckSuiteStatus {
+  if (title) {
+    const lowerTitle = title.toLowerCase();
+
+    if (lowerTitle.includes('cancelled for')) {
+      return 'cancelled';
+    }
+
+    if (lowerTitle.includes('failed for')) {
+      return 'failure';
+    }
+
+    if (lowerTitle.includes('skipped for')) {
+      return 'skipped';
+    }
+
+    if (lowerTitle.includes('succeeded for')) {
+      return 'success';
+    }
+  }
+
+  return null;
+}
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index a90e4b21..48cd1347 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -63,7 +63,7 @@ export function formatSearchQueryString(
   return `${title} in:title repo:${repo} updated:>${addHours(lastUpdated, -2)}`;
 }
 
-export async function getHtmlUrl(url: string, token: string) {
+export async function getHtmlUrl(url: string, token: string): Promise<string> {
   const response = await apiRequestAuth(url, 'GET', token);
 
   return response.data.html_url;
@@ -152,7 +152,7 @@ export async function generateGitHubWebUrl(
   notification: Notification,
   accounts: AuthState,
 ): Promise<string> {
-  let url;
+  let url: string;
 
   if (notification.subject.latest_comment_url) {
     url = await getHtmlUrl(
@@ -167,6 +167,9 @@ export async function generateGitHubWebUrl(
       case 'Discussion':
         url = await getDiscussionUrl(notification, accounts.token);
         break;
+      case 'RepositoryInvitation':
+        url = `${notification.repository.html_url}/invitations`;
+        break;
       default:
         url = notification.repository.html_url;
         break;


Test patch:
diff --git a/src/utils/__snapshots__/github-api.test.ts.snap b/src/utils/__snapshots__/github-api.test.ts.snap
index 758f59fc..67b2749b 100644
--- a/src/utils/__snapshots__/github-api.test.ts.snap
+++ b/src/utils/__snapshots__/github-api.test.ts.snap
@@ -1,122 +1,132 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`./utils/github-api.ts should format the notification color 1`] = `"text-red-500"`;
-
-exports[`./utils/github-api.ts should format the notification color 2`] = `"text-purple-500"`;
-
-exports[`./utils/github-api.ts should format the notification color 3`] = `"text-gray-600"`;
-
-exports[`./utils/github-api.ts should format the notification color 4`] = `"text-purple-500"`;
-
-exports[`./utils/github-api.ts should format the notification color 5`] = `"text-gray-300"`;
-
-exports[`./utils/github-api.ts should format the notification color 6`] = `"text-green-500"`;
-
-exports[`./utils/github-api.ts should format the notification color 7`] = `"text-green-500"`;
-
-exports[`./utils/github-api.ts should format the notification color 8`] = `"text-gray-300"`;
-
-exports[`./utils/github-api.ts should format the notification reason 1`] = `
+exports[`formatReason should format the notification reason 1`] = `
 {
   "description": "You were assigned to the issue.",
   "type": "Assign",
 }
 `;
 
-exports[`./utils/github-api.ts should format the notification reason 2`] = `
+exports[`formatReason should format the notification reason 2`] = `
 {
   "description": "You created the thread.",
   "type": "Author",
 }
 `;
 
-exports[`./utils/github-api.ts should format the notification reason 3`] = `
+exports[`formatReason should format the notification reason 3`] = `
 {
   "description": "A GitHub Actions workflow run was triggered for your repository",
   "type": "Workflow Run",
 }
 `;
 
-exports[`./utils/github-api.ts should format the notification reason 4`] = `
+exports[`formatReason should format the notification reason 4`] = `
 {
   "description": "You commented on the thread.",
   "type": "Comment",
 }
 `;
 
-exports[`./utils/github-api.ts should format the notification reason 5`] = `
+exports[`formatReason should format the notification reason 5`] = `
 {
   "description": "You accepted an invitation to contribute to the repository.",
   "type": "Invitation",
 }
 `;
 
-exports[`./utils/github-api.ts should format the notification reason 6`] = `
+exports[`formatReason should format the notification reason 6`] = `
 {
   "description": "You subscribed to the thread (via an issue or pull request).",
   "type": "Manual",
 }
 `;
 
-exports[`./utils/github-api.ts should format the notification reason 7`] = `
+exports[`formatReason should format the notification reason 7`] = `
 {
   "description": "Organization members have requested to enable a feature such as Draft Pull Requests or CoPilot.",
   "type": "Member Feature Requested",
 }
 `;
 
-exports[`./utils/github-api.ts should format the notification reason 8`] = `
+exports[`formatReason should format the notification reason 8`] = `
 {
   "description": "You were specifically @mentioned in the content.",
   "type": "Mention",
 }
 `;
 
-exports[`./utils/github-api.ts should format the notification reason 9`] = `
+exports[`formatReason should format the notification reason 9`] = `
 {
   "description": "You, or a team you're a member of, were requested to review a pull request.",
   "type": "Review Requested",
 }
 `;
 
-exports[`./utils/github-api.ts should format the notification reason 10`] = `
+exports[`formatReason should format the notification reason 10`] = `
 {
   "description": "You were credited for contributing to a security advisory.",
   "type": "Security Advisory Credit",
 }
 `;
 
-exports[`./utils/github-api.ts should format the notification reason 11`] = `
+exports[`formatReason should format the notification reason 11`] = `
 {
   "description": "GitHub discovered a security vulnerability in your repository.",
   "type": "Security Alert",
 }
 `;
 
-exports[`./utils/github-api.ts should format the notification reason 12`] = `
+exports[`formatReason should format the notification reason 12`] = `
 {
   "description": "You changed the thread state (for example, closing an issue or merging a pull request).",
   "type": "State Change",
 }
 `;
 
-exports[`./utils/github-api.ts should format the notification reason 13`] = `
+exports[`formatReason should format the notification reason 13`] = `
 {
   "description": "You're watching the repository.",
   "type": "Subscribed",
 }
 `;
 
-exports[`./utils/github-api.ts should format the notification reason 14`] = `
+exports[`formatReason should format the notification reason 14`] = `
 {
   "description": "You were on a team that was mentioned.",
   "type": "Team Mention",
 }
 `;
 
-exports[`./utils/github-api.ts should format the notification reason 15`] = `
+exports[`formatReason should format the notification reason 15`] = `
 {
   "description": "The reason for this notification is not supported by the app.",
   "type": "Unknown",
 }
 `;
+
+exports[`getNotificationTypeIconColor should format the notification color for check suite 1`] = `"text-gray-500"`;
+
+exports[`getNotificationTypeIconColor should format the notification color for check suite 2`] = `"text-red-500"`;
+
+exports[`getNotificationTypeIconColor should format the notification color for check suite 3`] = `"text-gray-500"`;
+
+exports[`getNotificationTypeIconColor should format the notification color for check suite 4`] = `"text-green-500"`;
+
+exports[`getNotificationTypeIconColor should format the notification color for check suite 5`] = `"text-gray-300"`;
+
+exports[`getNotificationTypeIconColor should format the notification color for state 1`] = `"text-red-500"`;
+
+exports[`getNotificationTypeIconColor should format the notification color for state 2`] = `"text-purple-500"`;
+
+exports[`getNotificationTypeIconColor should format the notification color for state 3`] = `"text-gray-600"`;
+
+exports[`getNotificationTypeIconColor should format the notification color for state 4`] = `"text-purple-500"`;
+
+exports[`getNotificationTypeIconColor should format the notification color for state 5`] = `"text-gray-300"`;
+
+exports[`getNotificationTypeIconColor should format the notification color for state 6`] = `"text-green-500"`;
+
+exports[`getNotificationTypeIconColor should format the notification color for state 7`] = `"text-green-500"`;
+
+exports[`getNotificationTypeIconColor should format the notification color for state 8`] = `"text-gray-300"`;
diff --git a/src/utils/github-api.test.ts b/src/utils/github-api.test.ts
index 5ce25403..e9cc4d75 100644
--- a/src/utils/github-api.test.ts
+++ b/src/utils/github-api.test.ts
@@ -3,9 +3,9 @@ import {
   getNotificationTypeIcon,
   getNotificationTypeIconColor,
 } from './github-api';
-import { Reason, StateType, SubjectType } from '../typesGithub';
+import { Reason, StateType, Subject, SubjectType } from '../typesGithub';
 
-describe('./utils/github-api.ts', () => {
+describe('formatReason', () => {
   it('should format the notification reason', () => {
     expect(formatReason('assign')).toMatchSnapshot();
     expect(formatReason('author')).toMatchSnapshot();
@@ -23,62 +23,228 @@ describe('./utils/github-api.ts', () => {
     expect(formatReason('team_mention')).toMatchSnapshot();
     expect(formatReason('something_else_unknown' as Reason)).toMatchSnapshot();
   });
+});
 
+describe('getNotificationTypeIcon', () => {
   it('should get the notification type icon', () => {
-    expect(getNotificationTypeIcon('CheckSuite').displayName).toBe('SyncIcon');
-    expect(getNotificationTypeIcon('Commit').displayName).toBe('GitCommitIcon');
-    expect(getNotificationTypeIcon('Discussion').displayName).toBe(
-      'CommentDiscussionIcon',
-    );
-    expect(getNotificationTypeIcon('Issue').displayName).toBe(
-      'IssueOpenedIcon',
-    );
-    expect(getNotificationTypeIcon('Issue', 'draft').displayName).toBe(
-      'IssueDraftIcon',
-    );
-    expect(getNotificationTypeIcon('Issue', 'closed').displayName).toBe(
-      'IssueClosedIcon',
-    );
-    expect(getNotificationTypeIcon('Issue', 'completed').displayName).toBe(
-      'IssueClosedIcon',
-    );
-    expect(getNotificationTypeIcon('Issue', 'reopened').displayName).toBe(
-      'IssueReopenedIcon',
-    );
-    expect(getNotificationTypeIcon('PullRequest').displayName).toBe(
-      'GitPullRequestIcon',
-    );
-    expect(getNotificationTypeIcon('PullRequest', 'draft').displayName).toBe(
-      'GitPullRequestDraftIcon',
-    );
-    expect(getNotificationTypeIcon('PullRequest', 'closed').displayName).toBe(
-      'GitPullRequestClosedIcon',
-    );
-    expect(getNotificationTypeIcon('PullRequest', 'merged').displayName).toBe(
-      'GitMergeIcon',
-    );
-    expect(getNotificationTypeIcon('Release').displayName).toBe('TagIcon');
-    expect(getNotificationTypeIcon('RepositoryInvitation').displayName).toBe(
-      'MailIcon',
-    );
     expect(
-      getNotificationTypeIcon('RepositoryVulnerabilityAlert').displayName,
+      getNotificationTypeIcon(createSubjectMock({ type: 'CheckSuite' }))
+        .displayName,
+    ).toBe('SyncIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({
+          type: 'CheckSuite',
+          title: 'Workflow cancelled for main branch',
+        }),
+      ).displayName,
+    ).toBe('StopIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({
+          type: 'CheckSuite',
+          title: 'Workflow failed for main branch',
+        }),
+      ).displayName,
+    ).toBe('XIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({
+          type: 'CheckSuite',
+          title: 'Workflow skipped for main branch',
+        }),
+      ).displayName,
+    ).toBe('SkipIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({
+          type: 'CheckSuite',
+          title: 'Workflow succeeded for main branch',
+        }),
+      ).displayName,
+    ).toBe('CheckIcon');
+    expect(
+      getNotificationTypeIcon(createSubjectMock({ type: 'Commit' }))
+        .displayName,
+    ).toBe('GitCommitIcon');
+    expect(
+      getNotificationTypeIcon(createSubjectMock({ type: 'Discussion' }))
+        .displayName,
+    ).toBe('CommentDiscussionIcon');
+    expect(
+      getNotificationTypeIcon(createSubjectMock({ type: 'Issue' })).displayName,
+    ).toBe('IssueOpenedIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({ type: 'Issue', state: 'draft' }),
+      ).displayName,
+    ).toBe('IssueDraftIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({
+          type: 'Issue',
+          state: 'closed',
+        }),
+      ).displayName,
+    ).toBe('IssueClosedIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({
+          type: 'Issue',
+          state: 'completed',
+        }),
+      ).displayName,
+    ).toBe('IssueClosedIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({
+          type: 'Issue',
+          state: 'reopened',
+        }),
+      ).displayName,
+    ).toBe('IssueReopenedIcon');
+    expect(
+      getNotificationTypeIcon(createSubjectMock({ type: 'PullRequest' }))
+        .displayName,
+    ).toBe('GitPullRequestIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({
+          type: 'PullRequest',
+          state: 'draft',
+        }),
+      ).displayName,
+    ).toBe('GitPullRequestDraftIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({
+          type: 'PullRequest',
+          state: 'closed',
+        }),
+      ).displayName,
+    ).toBe('GitPullRequestClosedIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({
+          type: 'PullRequest',
+          state: 'merged',
+        }),
+      ).displayName,
+    ).toBe('GitMergeIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({
+          type: 'Release',
+        }),
+      ).displayName,
+    ).toBe('TagIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({
+          type: 'RepositoryInvitation',
+        }),
+      ).displayName,
+    ).toBe('MailIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({
+          type: 'RepositoryVulnerabilityAlert',
+        }),
+      ).displayName,
     ).toBe('AlertIcon');
-    expect(getNotificationTypeIcon('Unknown' as SubjectType).displayName).toBe(
+    expect(getNotificationTypeIcon(createSubjectMock({})).displayName).toBe(
       'QuestionIcon',
     );
   });
+});
+
+describe('getNotificationTypeIconColor', () => {
+  it('should format the notification color for check suite', () => {
+    expect(
+      getNotificationTypeIconColor(
+        createSubjectMock({
+          type: 'CheckSuite',
+          title: 'Workflow cancelled for main branch',
+        }),
+      ),
+    ).toMatchSnapshot();
+    expect(
+      getNotificationTypeIconColor(
+        createSubjectMock({
+          type: 'CheckSuite',
+          title: 'Workflow failed for main branch',
+        }),
+      ),
+    ).toMatchSnapshot();
 
-  it('should format the notification color', () => {
-    expect(getNotificationTypeIconColor('closed')).toMatchSnapshot();
-    expect(getNotificationTypeIconColor('completed')).toMatchSnapshot();
-    expect(getNotificationTypeIconColor('draft')).toMatchSnapshot();
-    expect(getNotificationTypeIconColor('merged')).toMatchSnapshot();
-    expect(getNotificationTypeIconColor('not_planned')).toMatchSnapshot();
-    expect(getNotificationTypeIconColor('open')).toMatchSnapshot();
-    expect(getNotificationTypeIconColor('reopened')).toMatchSnapshot();
-    expect(
-      getNotificationTypeIconColor('something_else_unknown' as StateType),
+    expect(
+      getNotificationTypeIconColor(
+        createSubjectMock({
+          type: 'CheckSuite',
+          title: 'Workflow skipped for main branch',
+        }),
+      ),
+    ).toMatchSnapshot();
+    expect(
+      getNotificationTypeIconColor(
+        createSubjectMock({
+          type: 'CheckSuite',
+          title: 'Workflow succeeded for main branch',
+        }),
+      ),
+    ).toMatchSnapshot();
+    expect(
+      getNotificationTypeIconColor(
+        createSubjectMock({
+          type: 'CheckSuite',
+          title: 'unknown state',
+        }),
+      ),
+    ).toMatchSnapshot();
+  });
+
+  it('should format the notification color for state', () => {
+    expect(
+      getNotificationTypeIconColor(createSubjectMock({ state: 'closed' })),
+    ).toMatchSnapshot();
+    expect(
+      getNotificationTypeIconColor(createSubjectMock({ state: 'completed' })),
+    ).toMatchSnapshot();
+    expect(
+      getNotificationTypeIconColor(createSubjectMock({ state: 'draft' })),
+    ).toMatchSnapshot();
+    expect(
+      getNotificationTypeIconColor(createSubjectMock({ state: 'merged' })),
+    ).toMatchSnapshot();
+    expect(
+      getNotificationTypeIconColor(createSubjectMock({ state: 'not_planned' })),
+    ).toMatchSnapshot();
+    expect(
+      getNotificationTypeIconColor(createSubjectMock({ state: 'open' })),
+    ).toMatchSnapshot();
+    expect(
+      getNotificationTypeIconColor(createSubjectMock({ state: 'reopened' })),
+    ).toMatchSnapshot();
+    expect(
+      getNotificationTypeIconColor(
+        createSubjectMock({
+          state: 'something_else_unknown' as StateType,
+        }),
+      ),
     ).toMatchSnapshot();
   });
 });
+
+function createSubjectMock(mocks: {
+  title?: string;
+  type?: SubjectType;
+  state?: StateType;
+}): Subject {
+  return {
+    title: mocks.title ?? 'Mock Subject',
+    type: mocks.type ?? ('Unknown' as SubjectType),
+    state: mocks.state ?? ('Unknown' as StateType),
+    url: null,
+    latest_comment_url: null,
+  };
+}
diff --git a/src/utils/helpers.test.ts b/src/utils/helpers.test.ts
index bb0e6645..8cef1c3c 100644
--- a/src/utils/helpers.test.ts
+++ b/src/utils/helpers.test.ts
@@ -152,7 +152,7 @@ describe('utils/helpers.ts', () => {
   describe('generateGitHubWebUrl', () => {
     const mockedHtmlUrl = 'https://github.com/gitify-app/gitify/issues/785';
     const mockedNotificationReferrer =
-      '?notification_referrer_id=MDE4Ok5vdGlmaWNhdGlvblRocmVhZDEzODY2MTA5NjoxMjM0NTY3ODk%3D';
+      'notification_referrer_id=MDE4Ok5vdGlmaWNhdGlvblRocmVhZDEzODY2MTA5NjoxMjM0NTY3ODk%3D';
     const apiRequestAuthMock = jest.spyOn(apiRequests, 'apiRequestAuth');
 
     afterEach(() => {
@@ -192,7 +192,7 @@ describe('utils/helpers.ts', () => {
         'GET',
         mockAccounts.token,
       );
-      expect(result).toBe(`${mockedHtmlUrl}${mockedNotificationReferrer}`);
+      expect(result).toBe(`${mockedHtmlUrl}?${mockedNotificationReferrer}`);
     });
 
     it('Subject Url: when no latest comment url available, fetch subject html url', async () => {
@@ -227,7 +227,7 @@ describe('utils/helpers.ts', () => {
         'GET',
         mockAccounts.token,
       );
-      expect(result).toBe(`${mockedHtmlUrl}${mockedNotificationReferrer}`);
+      expect(result).toBe(`${mockedHtmlUrl}?${mockedNotificationReferrer}`);
     });
 
     it('Discussions: when no subject urls and no discussions found via query, default to linking to repository discussions', async () => {
@@ -256,7 +256,7 @@ describe('utils/helpers.ts', () => {
 
       expect(apiRequestAuthMock).toHaveBeenCalledTimes(1);
       expect(result).toBe(
-        `${mockedSingleNotification.repository.html_url}/discussions${mockedNotificationReferrer}`,
+        `${mockedSingleNotification.repository.html_url}/discussions?${mockedNotificationReferrer}`,
       );
     });
 
@@ -268,8 +268,6 @@ describe('utils/helpers.ts', () => {
         type: 'Discussion' as SubjectType,
       };
 
-      // const latestDiscussionCommentId = 12345;
-
       const requestPromise = new Promise((resolve) =>
         resolve(mockedGraphQLResponse as AxiosResponse),
       ) as AxiosPromise;
@@ -286,7 +284,29 @@ describe('utils/helpers.ts', () => {
 
       expect(apiRequestAuthMock).toHaveBeenCalledTimes(1);
       expect(result).toBe(
-        'https://github.com/manosim/notifications-test/discussions/612?notification_referrer_id=MDE4Ok5vdGlmaWNhdGlvblRocmVhZDEzODY2MTA5NjoxMjM0NTY3ODk%3D#discussioncomment-2300902',
+        `https://github.com/manosim/notifications-test/discussions/612?${mockedNotificationReferrer}#discussioncomment-2300902`,
+      );
+    });
+
+    it('Repository Invitation url', async () => {
+      const subject = {
+        title: 'Invitation to join manosim/notifications-test from unit-tests',
+        url: null,
+        latest_comment_url: null,
+        type: 'RepositoryInvitation' as SubjectType,
+      };
+
+      const result = await generateGitHubWebUrl(
+        {
+          ...mockedSingleNotification,
+          subject: subject,
+        },
+        mockAccounts,
+      );
+
+      expect(apiRequestAuthMock).toHaveBeenCalledTimes(0);
+      expect(result).toBe(
+        `https://github.com/manosim/notifications-test/invitations?${mockedNotificationReferrer}`,
       );
     });
 
@@ -308,7 +328,7 @@ describe('utils/helpers.ts', () => {
 
       expect(apiRequestAuthMock).toHaveBeenCalledTimes(0);
       expect(result).toBe(
-        `${mockedSingleNotification.repository.html_url}${mockedNotificationReferrer}`,
+        `${mockedSingleNotification.repository.html_url}?${mockedNotificationReferrer}`,
       );
     });
   });