diff --git a/src/hooks/useNotifications.test.ts b/src/hooks/useNotifications.test.ts index 42c07b6ba..30fb6dcf0 100644 --- a/src/hooks/useNotifications.test.ts +++ b/src/hooks/useNotifications.test.ts @@ -192,8 +192,8 @@ describe('hooks/useNotifications.ts', () => { { id: 1, subject: { - title: 'This is a Discussion.', - type: 'Discussion', + title: 'This is a check suite workflow.', + type: 'CheckSuite', url: 'https://api.github.com/1', }, repository: { @@ -204,25 +204,37 @@ describe('hooks/useNotifications.ts', () => { { id: 2, subject: { - title: 'This is an Issue.', - type: 'Issue', + title: 'This is a Discussion.', + type: 'Discussion', url: 'https://api.github.com/2', }, + repository: { + full_name: 'some/repo', + }, + updated_at: '2024-02-26T00:00:00Z', }, { id: 3, subject: { - title: 'This is a Pull Request.', - type: 'PullRequest', + title: 'This is an Issue.', + type: 'Issue', url: 'https://api.github.com/3', }, }, { id: 4, + subject: { + title: 'This is a Pull Request.', + type: 'PullRequest', + url: 'https://api.github.com/4', + }, + }, + { + id: 5, subject: { title: 'This is an invitation.', type: 'RepositoryInvitation', - url: 'https://api.github.com/4', + url: 'https://api.github.com/5', }, }, ]; @@ -249,18 +261,15 @@ describe('hooks/useNotifications.ts', () => { }, }, }); - nock('https://api.github.com') - .get('/2') - .reply(200, { state: 'closed', merged: true }); nock('https://api.github.com') .get('/3') - .reply(200, { state: 'closed', merged: false }); + .reply(200, { state: 'closed', merged: true }); nock('https://api.github.com') .get('/4') - .reply(200, { state: 'open', draft: false }); + .reply(200, { state: 'closed', merged: false }); nock('https://api.github.com') .get('/5') - .reply(200, { state: 'open', draft: true }); + .reply(200, { state: 'open', draft: false }); const { result } = renderHook(() => useNotifications(true)); @@ -277,7 +286,7 @@ describe('hooks/useNotifications.ts', () => { expect(result.current.notifications[0].hostname).toBe('github.com'); }); - expect(result.current.notifications[0].notifications.length).toBe(4); + expect(result.current.notifications[0].notifications.length).toBe(5); }); }); }); diff --git a/src/typesGithub.ts b/src/typesGithub.ts index 68548f205..34fd51c53 100644 --- a/src/typesGithub.ts +++ b/src/typesGithub.ts @@ -46,6 +46,7 @@ export type IssueStateReasonType = 'completed' | 'not_planned' | 'reopened'; export type PullRequestStateType = 'closed' | 'draft' | 'merged' | 'open'; export type StateType = + | CheckSuiteStatus | DiscussionStateType | IssueStateType | IssueStateReasonType diff --git a/src/utils/github-api.test.ts b/src/utils/github-api.test.ts index 6e000f22b..dbb5fe929 100644 --- a/src/utils/github-api.test.ts +++ b/src/utils/github-api.test.ts @@ -29,14 +29,15 @@ describe('formatReason', () => { describe('getNotificationTypeIcon', () => { it('should get the notification type icon', () => { expect( - getNotificationTypeIcon(createSubjectMock({ type: 'CheckSuite' })) - .displayName, + getNotificationTypeIcon( + createSubjectMock({ type: 'CheckSuite', state: null }), + ).displayName, ).toBe('SyncIcon'); expect( getNotificationTypeIcon( createSubjectMock({ type: 'CheckSuite', - title: 'Workflow cancelled for main branch', + state: 'cancelled', }), ).displayName, ).toBe('StopIcon'); @@ -44,7 +45,7 @@ describe('getNotificationTypeIcon', () => { getNotificationTypeIcon( createSubjectMock({ type: 'CheckSuite', - title: 'Workflow failed for main branch', + state: 'failure', }), ).displayName, ).toBe('XIcon'); @@ -52,7 +53,7 @@ describe('getNotificationTypeIcon', () => { getNotificationTypeIcon( createSubjectMock({ type: 'CheckSuite', - title: 'Workflow skipped for main branch', + state: 'skipped', }), ).displayName, ).toBe('SkipIcon'); @@ -60,7 +61,7 @@ describe('getNotificationTypeIcon', () => { getNotificationTypeIcon( createSubjectMock({ type: 'CheckSuite', - title: 'Workflow succeeded for main branch', + state: 'success', }), ).displayName, ).toBe('CheckIcon'); @@ -195,7 +196,7 @@ describe('getNotificationTypeIconColor', () => { getNotificationTypeIconColor( createSubjectMock({ type: 'CheckSuite', - title: 'Workflow cancelled for main branch', + state: 'cancelled', }), ), ).toMatchSnapshot(); @@ -203,7 +204,7 @@ describe('getNotificationTypeIconColor', () => { getNotificationTypeIconColor( createSubjectMock({ type: 'CheckSuite', - title: 'Workflow failed for main branch', + state: 'failure', }), ), ).toMatchSnapshot(); @@ -212,7 +213,7 @@ describe('getNotificationTypeIconColor', () => { getNotificationTypeIconColor( createSubjectMock({ type: 'CheckSuite', - title: 'Workflow skipped for main branch', + state: 'skipped', }), ), ).toMatchSnapshot(); @@ -220,7 +221,7 @@ describe('getNotificationTypeIconColor', () => { getNotificationTypeIconColor( createSubjectMock({ type: 'CheckSuite', - title: 'Workflow succeeded for main branch', + state: 'success', }), ), ).toMatchSnapshot(); @@ -228,7 +229,7 @@ describe('getNotificationTypeIconColor', () => { getNotificationTypeIconColor( createSubjectMock({ type: 'CheckSuite', - title: 'unknown state', + state: null, }), ), ).toMatchSnapshot(); diff --git a/src/utils/github-api.ts b/src/utils/github-api.ts index eb2a64440..8370352f6 100644 --- a/src/utils/github-api.ts +++ b/src/utils/github-api.ts @@ -24,7 +24,7 @@ import { TagIcon, XIcon, } from '@primer/octicons-react'; -import { CheckSuiteStatus, Reason, Subject } from '../typesGithub'; +import { Reason, Subject } from '../typesGithub'; // prettier-ignore const DESCRIPTIONS = { @@ -92,9 +92,7 @@ export function getNotificationTypeIcon( ): React.FC { switch (subject.type) { case 'CheckSuite': - const checkSuiteState = inferCheckSuiteStatus(subject.title); - - switch (checkSuiteState) { + switch (subject.state) { case 'cancelled': return StopIcon; case 'failure': @@ -158,32 +156,19 @@ export function getNotificationTypeIcon( } 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 'ANSWERED': return 'text-green-500'; + case 'cancelled': + return 'text-gray-500'; case 'closed': return 'text-red-500'; case 'completed': return 'text-purple-500'; case 'draft': return 'text-gray-600'; + case 'failure': + return 'text-red-500'; case 'merged': return 'text-purple-500'; case 'not_planned': @@ -194,31 +179,11 @@ export function getNotificationTypeIconColor(subject: Subject): string { return 'text-green-500'; case 'RESOLVED': return 'text-purple-500'; + case 'skipped': + return 'text-gray-500'; + case 'success': + return 'text-green-500'; default: 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/state.test.ts b/src/utils/state.test.ts index 8cc7c06b0..b0fc2e90b 100644 --- a/src/utils/state.test.ts +++ b/src/utils/state.test.ts @@ -4,6 +4,7 @@ import nock from 'nock'; import { mockAccounts } from '../__mocks__/mock-state'; import { mockedSingleNotification } from '../__mocks__/mockedData'; import { + getCheckSuiteState, getDiscussionState, getIssueState, getPullRequestState, @@ -15,6 +16,78 @@ describe('utils/state.ts', () => { axios.defaults.adapter = 'http'; }); + describe('getCheckSuiteState', () => { + it('cancelled check suite state', async () => { + const mockNotification = { + ...mockedSingleNotification, + subject: { + ...mockedSingleNotification.subject, + title: 'Demo workflow run cancelled for main branch', + }, + }; + + const result = getCheckSuiteState(mockNotification); + + expect(result).toBe('cancelled'); + }); + + it('failed check suite state', async () => { + const mockNotification = { + ...mockedSingleNotification, + subject: { + ...mockedSingleNotification.subject, + title: 'Demo workflow run failed for main branch', + }, + }; + + const result = getCheckSuiteState(mockNotification); + + expect(result).toBe('failure'); + }); + + it('skipped check suite state', async () => { + const mockNotification = { + ...mockedSingleNotification, + subject: { + ...mockedSingleNotification.subject, + title: 'Demo workflow run skipped for main branch', + }, + }; + + const result = getCheckSuiteState(mockNotification); + + expect(result).toBe('skipped'); + }); + + it('successful check suite state', async () => { + const mockNotification = { + ...mockedSingleNotification, + subject: { + ...mockedSingleNotification.subject, + title: 'Demo workflow run succeeded for main branch', + }, + }; + + const result = getCheckSuiteState(mockNotification); + + expect(result).toBe('success'); + }); + + it('unknown check suite state', async () => { + const mockNotification = { + ...mockedSingleNotification, + subject: { + ...mockedSingleNotification.subject, + title: 'Demo workflow run for main branch', + }, + }; + + const result = getCheckSuiteState(mockNotification); + + expect(result).toBeNull(); + }); + }); + describe('getDiscussionState', () => { it('answered discussion state', async () => { const mockNotification = { diff --git a/src/utils/state.ts b/src/utils/state.ts index fed46baa9..60b7bf981 100644 --- a/src/utils/state.ts +++ b/src/utils/state.ts @@ -1,5 +1,6 @@ import { formatSearchQueryString } from './helpers'; import { + CheckSuiteStatus, DiscussionStateSearchResultEdge, DiscussionStateType, GraphQLSearch, @@ -15,6 +16,8 @@ export async function getNotificationState( token: string, ): Promise { switch (notification.subject.type) { + case 'CheckSuite': + return getCheckSuiteState(notification); case 'Discussion': return await getDiscussionState(notification, token); case 'Issue': @@ -26,6 +29,34 @@ export async function getNotificationState( } } +/** + * Ideally we would be using a GitHub API to fetch the CheckSuite / WorkflowRun state, + * but there isn't an obvious/clean way to do this currently. + */ +export function getCheckSuiteState( + notification: Notification, +): CheckSuiteStatus | null { + const lowerTitle = notification.subject.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; +} + export async function getDiscussionState( notification: Notification, token: string,