From 5d767f32d9c51ffd0c34ddc2b4f08ae70ff16c50 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Sat, 20 Dec 2025 15:53:23 +1000 Subject: [PATCH 01/25] refactor(api): use graphql api for issue and pull request enrichment Signed-off-by: Adam Setch --- .../components/metrics/MetricGroup.test.tsx | 13 +- .../components/metrics/MetricGroup.tsx | 3 +- src/renderer/typesGitHub.ts | 21 +- .../utils/api/__mocks__/response-mocks.ts | 6 +- src/renderer/utils/api/client.ts | 103 ++++---- src/renderer/utils/api/graphql/common.graphql | 6 + ...discussions.graphql => discussion.graphql} | 7 - .../utils/api/graphql/generated/gql.ts | 24 +- .../utils/api/graphql/generated/graphql.ts | 234 +++++++++++++++--- src/renderer/utils/api/graphql/issue.graphql | 41 +++ src/renderer/utils/api/graphql/pull.graphql | 57 +++++ .../utils/notifications/handlers/default.ts | 12 + .../notifications/handlers/discussion.test.ts | 16 +- .../notifications/handlers/discussion.ts | 4 +- .../utils/notifications/handlers/issue.ts | 41 ++- .../notifications/handlers/pullRequest.ts | 59 ++--- .../utils/notifications/handlers/utils.ts | 25 ++ 17 files changed, 476 insertions(+), 196 deletions(-) create mode 100644 src/renderer/utils/api/graphql/common.graphql rename src/renderer/utils/api/graphql/{discussions.graphql => discussion.graphql} (90%) create mode 100644 src/renderer/utils/api/graphql/issue.graphql create mode 100644 src/renderer/utils/api/graphql/pull.graphql diff --git a/src/renderer/components/metrics/MetricGroup.test.tsx b/src/renderer/components/metrics/MetricGroup.test.tsx index 030c171c4..7c738570e 100644 --- a/src/renderer/components/metrics/MetricGroup.test.tsx +++ b/src/renderer/components/metrics/MetricGroup.test.tsx @@ -1,7 +1,10 @@ import { renderWithAppContext } from '../../__helpers__/test-utils'; import { mockSettings } from '../../__mocks__/state-mocks'; -import type { Milestone } from '../../typesGitHub'; import { mockSingleNotification } from '../../utils/api/__mocks__/response-mocks'; +import { + type MilestoneFieldsFragment, + MilestoneState, +} from '../../utils/api/graphql/generated/graphql'; import { MetricGroup } from './MetricGroup'; describe('renderer/components/metrics/MetricGroup.tsx', () => { @@ -103,8 +106,8 @@ describe('renderer/components/metrics/MetricGroup.tsx', () => { const mockNotification = mockSingleNotification; mockNotification.subject.milestone = { title: 'Milestone 1', - state: 'open', - } as Milestone; + state: MilestoneState.Open, + } as MilestoneFieldsFragment; const props = { notification: mockNotification, @@ -118,8 +121,8 @@ describe('renderer/components/metrics/MetricGroup.tsx', () => { const mockNotification = mockSingleNotification; mockNotification.subject.milestone = { title: 'Milestone 1', - state: 'closed', - } as Milestone; + state: MilestoneState.Closed, + } as MilestoneFieldsFragment; const props = { notification: mockNotification, diff --git a/src/renderer/components/metrics/MetricGroup.tsx b/src/renderer/components/metrics/MetricGroup.tsx index cdb1bd3d4..6afc50d7a 100644 --- a/src/renderer/components/metrics/MetricGroup.tsx +++ b/src/renderer/components/metrics/MetricGroup.tsx @@ -10,6 +10,7 @@ import { import { AppContext } from '../../context/App'; import { IconColor } from '../../types'; import type { Notification } from '../../typesGitHub'; +import { MilestoneState } from '../../utils/api/graphql/generated/graphql'; import { getPullRequestReviewIcon } from '../../utils/icons'; import { MetricPill } from './MetricPill'; @@ -84,7 +85,7 @@ export const MetricGroup: FC = ({ {notification.subject.milestone && ( { - return apiRequestAuth(url, 'GET', token); -} - -/** - * Get comments on issues and pull requests. - * Every pull request is an issue, but not every issue is a pull request. - * - * Endpoint documentation: https://docs.github.com/en/rest/issues/comments#get-an-issue-comment - */ -export function getIssueOrPullRequestComment( - url: Link, - token: Token, -): AxiosPromise { - return apiRequestAuth(url, 'GET', token); -} - -/** - * Get details of a pull request. - * - * Endpoint documentation: https://docs.github.com/en/rest/pulls/pulls#get-a-pull-request - */ -export function getPullRequest( - url: Link, - token: Token, -): AxiosPromise { - return apiRequestAuth(url, 'GET', token); -} - -/** - * Lists all reviews for a specified pull request. The list of reviews returns in chronological order. - * - * Endpoint documentation: https://docs.github.com/en/rest/pulls/reviews#list-reviews-for-a-pull-request - */ -export function getPullRequestReviews( - url: Link, - token: Token, -): AxiosPromise { - return apiRequestAuth(url, 'GET', token); -} - /** * Gets a public release with the specified release ID. * @@ -234,10 +188,51 @@ export async function getHtmlUrl(url: Link, token: Token): Promise { } /** - * Search for Discussions that match notification title and repository. - * - * Returns the latest discussion and their latest comments / replies - * + * Fetch GitHub Issue by Issue Number. + */ +export async function fetchIssueByNumber( + notification: Notification, +): Promise> { + const url = getGitHubGraphQLUrl(notification.account.hostname); + const number = getNumberFromUrl(notification.subject.url); + + return performGraphQLRequest( + url.toString() as Link, + notification.account.token, + FetchIssueByNumberDocument, + { + owner: notification.repository.owner.login, + name: notification.repository.name, + number: number, + firstLabels: 100, + }, + ); +} + +/** + * Fetch GitHub Pull Request by PR Number. + */ +export async function fetchPullByNumber( + notification: Notification, +): Promise> { + const url = getGitHubGraphQLUrl(notification.account.hostname); + const number = getNumberFromUrl(notification.subject.url); + + return performGraphQLRequest( + url.toString() as Link, + notification.account.token, + FetchPullByNumberDocument, + { + owner: notification.repository.owner.login, + name: notification.repository.name, + number: number, + firstLabels: 100, + }, + ); +} + +/** + * Fetch GitHub Discussion by Discussion Number. */ export async function fetchDiscussionByNumber( notification: Notification, diff --git a/src/renderer/utils/api/graphql/common.graphql b/src/renderer/utils/api/graphql/common.graphql new file mode 100644 index 000000000..afb3c4151 --- /dev/null +++ b/src/renderer/utils/api/graphql/common.graphql @@ -0,0 +1,6 @@ +fragment AuthorFields on Actor { + login + html_url: url + avatar_url: avatarUrl + type: __typename +} diff --git a/src/renderer/utils/api/graphql/discussions.graphql b/src/renderer/utils/api/graphql/discussion.graphql similarity index 90% rename from src/renderer/utils/api/graphql/discussions.graphql rename to src/renderer/utils/api/graphql/discussion.graphql index 92c3c7f06..3620434e7 100644 --- a/src/renderer/utils/api/graphql/discussions.graphql +++ b/src/renderer/utils/api/graphql/discussion.graphql @@ -38,13 +38,6 @@ query FetchDiscussionByNumber( } } -fragment AuthorFields on Actor { - login - url - avatar_url: avatarUrl - type: __typename -} - fragment CommentFields on DiscussionComment { databaseId createdAt diff --git a/src/renderer/utils/api/graphql/generated/gql.ts b/src/renderer/utils/api/graphql/generated/gql.ts index 1a24e20d9..cd7a5ddc7 100644 --- a/src/renderer/utils/api/graphql/generated/gql.ts +++ b/src/renderer/utils/api/graphql/generated/gql.ts @@ -15,16 +15,34 @@ import * as types from './graphql'; * Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size */ type Documents = { - "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment AuthorFields on Actor {\n login\n url\n avatar_url: avatarUrl\n type: __typename\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n}": typeof types.FetchDiscussionByNumberDocument, + "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}": typeof types.AuthorFieldsFragmentDoc, + "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n}": typeof types.FetchDiscussionByNumberDocument, + "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchIssueByNumberDocument, + "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 1) {\n totalCount\n nodes {\n createdAt\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchPullByNumberDocument, }; const documents: Documents = { - "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment AuthorFields on Actor {\n login\n url\n avatar_url: avatarUrl\n type: __typename\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n}": types.FetchDiscussionByNumberDocument, + "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}": types.AuthorFieldsFragmentDoc, + "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n}": types.FetchDiscussionByNumberDocument, + "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchIssueByNumberDocument, + "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 1) {\n totalCount\n nodes {\n createdAt\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchPullByNumberDocument, }; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment AuthorFields on Actor {\n login\n url\n avatar_url: avatarUrl\n type: __typename\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n}"): typeof import('./graphql').FetchDiscussionByNumberDocument; +export function graphql(source: "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}"): typeof import('./graphql').AuthorFieldsFragmentDoc; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n}"): typeof import('./graphql').FetchDiscussionByNumberDocument; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').FetchIssueByNumberDocument; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 1) {\n totalCount\n nodes {\n createdAt\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').FetchPullByNumberDocument; export function graphql(source: string) { diff --git a/src/renderer/utils/api/graphql/generated/graphql.ts b/src/renderer/utils/api/graphql/generated/graphql.ts index 4ac16af25..a202f2d94 100644 --- a/src/renderer/utils/api/graphql/generated/graphql.ts +++ b/src/renderer/utils/api/graphql/generated/graphql.ts @@ -36168,6 +36168,24 @@ export type WorkflowsParametersInput = { export type _Entity = Issue; +type AuthorFields_Bot_Fragment = { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' }; + +type AuthorFields_EnterpriseUserAccount_Fragment = { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' }; + +type AuthorFields_Mannequin_Fragment = { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' }; + +type AuthorFields_Organization_Fragment = { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' }; + +type AuthorFields_User_Fragment = { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' }; + +export type AuthorFieldsFragment = + | AuthorFields_Bot_Fragment + | AuthorFields_EnterpriseUserAccount_Fragment + | AuthorFields_Mannequin_Fragment + | AuthorFields_Organization_Fragment + | AuthorFields_User_Fragment +; + export type FetchDiscussionByNumberQueryVariables = Exact<{ owner: Scalars['String']['input']; name: Scalars['String']['input']; @@ -36180,50 +36198,84 @@ export type FetchDiscussionByNumberQueryVariables = Exact<{ export type FetchDiscussionByNumberQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', discussion?: { __typename: 'Discussion', number: number, title: string, stateReason?: DiscussionStateReason | null, isAnswered?: boolean | null, url: any, author?: - | { __typename?: 'Bot', login: string, url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } + | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } | null, comments: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, replies: { __typename?: 'DiscussionCommentConnection', nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, author?: - | { __typename?: 'Bot', login: string, url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } + | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } | null } | null> | null }, author?: - | { __typename?: 'Bot', login: string, url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, url: any, avatar_url: any, type: 'User' } + | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } + | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null } | null } | null }; -type AuthorFields_Bot_Fragment = { __typename?: 'Bot', login: string, url: any, avatar_url: any, type: 'Bot' }; +export type CommentFieldsFragment = { __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, author?: + | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } + | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | null }; -type AuthorFields_EnterpriseUserAccount_Fragment = { __typename?: 'EnterpriseUserAccount', login: string, url: any, avatar_url: any, type: 'EnterpriseUserAccount' }; +export type FetchIssueByNumberQueryVariables = Exact<{ + owner: Scalars['String']['input']; + name: Scalars['String']['input']; + number: Scalars['Int']['input']; + firstLabels?: InputMaybe; +}>; -type AuthorFields_Mannequin_Fragment = { __typename?: 'Mannequin', login: string, url: any, avatar_url: any, type: 'Mannequin' }; -type AuthorFields_Organization_Fragment = { __typename?: 'Organization', login: string, url: any, avatar_url: any, type: 'Organization' }; +export type FetchIssueByNumberQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', issue?: { __typename: 'Issue', number: number, title: string, url: any, state: IssueState, stateReason?: IssueStateReason | null, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: + | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } + | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', author?: + | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } + | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null } | null } | null }; -type AuthorFields_User_Fragment = { __typename?: 'User', login: string, url: any, avatar_url: any, type: 'User' }; +export type MilestoneFieldsFragment = { __typename?: 'Milestone', state: MilestoneState, title: string }; -export type AuthorFieldsFragment = - | AuthorFields_Bot_Fragment - | AuthorFields_EnterpriseUserAccount_Fragment - | AuthorFields_Mannequin_Fragment - | AuthorFields_Organization_Fragment - | AuthorFields_User_Fragment -; +export type FetchPullByNumberQueryVariables = Exact<{ + owner: Scalars['String']['input']; + name: Scalars['String']['input']; + number: Scalars['Int']['input']; + firstLabels?: InputMaybe; +}>; -export type CommentFieldsFragment = { __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, author?: - | { __typename?: 'Bot', login: string, url: any, avatar_url: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, url: any, avatar_url: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, url: any, avatar_url: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, url: any, avatar_url: any, type: 'Organization' } - | { __typename?: 'User', login: string, url: any, avatar_url: any, type: 'User' } - | null }; + +export type FetchPullByNumberQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', pullRequest?: { __typename: 'PullRequest', number: number, title: string, url: any, state: PullRequestState, merged: boolean, isDraft: boolean, isInMergeQueue: boolean, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: + | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } + | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', author?: + | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } + | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } + | null } | null> | null }, reviews?: { __typename?: 'PullRequestReviewConnection', totalCount: number, nodes?: Array<{ __typename?: 'PullRequestReview', createdAt: any, author?: + | { __typename?: 'Bot', login: string } + | { __typename?: 'EnterpriseUserAccount', login: string } + | { __typename?: 'Mannequin', login: string } + | { __typename?: 'Organization', login: string } + | { __typename?: 'User', login: string } + | null } | null> | null } | null, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null, closingIssuesReferences?: { __typename?: 'IssueConnection', nodes?: Array<{ __typename?: 'Issue', number: number } | null> | null } | null } | null } | null }; export class TypedDocumentString extends String @@ -36246,7 +36298,7 @@ export class TypedDocumentString export const AuthorFieldsFragmentDoc = new TypedDocumentString(` fragment AuthorFields on Actor { login - url + html_url: url avatar_url: avatarUrl type: __typename } @@ -36261,10 +36313,16 @@ export const CommentFieldsFragmentDoc = new TypedDocumentString(` } fragment AuthorFields on Actor { login - url + html_url: url avatar_url: avatarUrl type: __typename }`, {"fragmentName":"CommentFields"}) as unknown as TypedDocumentString; +export const MilestoneFieldsFragmentDoc = new TypedDocumentString(` + fragment MilestoneFields on Milestone { + state + title +} + `, {"fragmentName":"MilestoneFields"}) as unknown as TypedDocumentString; export const FetchDiscussionByNumberDocument = new TypedDocumentString(` query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) { repository(owner: $owner, name: $name) { @@ -36299,7 +36357,7 @@ export const FetchDiscussionByNumberDocument = new TypedDocumentString(` } fragment AuthorFields on Actor { login - url + html_url: url avatar_url: avatarUrl type: __typename } @@ -36309,4 +36367,104 @@ fragment CommentFields on DiscussionComment { author { ...AuthorFields } -}`) as unknown as TypedDocumentString; \ No newline at end of file +}`) as unknown as TypedDocumentString; +export const FetchIssueByNumberDocument = new TypedDocumentString(` + query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) { + repository(owner: $owner, name: $name) { + issue(number: $number) { + __typename + number + title + url + state + stateReason + milestone { + ...MilestoneFields + } + author { + ...AuthorFields + } + comments(last: 1) { + totalCount + nodes { + author { + ...AuthorFields + } + } + } + labels(first: $firstLabels) { + nodes { + name + } + } + } + } +} + fragment AuthorFields on Actor { + login + html_url: url + avatar_url: avatarUrl + type: __typename +} +fragment MilestoneFields on Milestone { + state + title +}`) as unknown as TypedDocumentString; +export const FetchPullByNumberDocument = new TypedDocumentString(` + query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) { + repository(owner: $owner, name: $name) { + pullRequest(number: $number) { + __typename + number + title + url + state + merged + isDraft + isInMergeQueue + milestone { + ...MilestoneFields + } + author { + ...AuthorFields + } + comments(last: 1) { + totalCount + nodes { + author { + ...AuthorFields + } + } + } + reviews(last: 1) { + totalCount + nodes { + createdAt + author { + login + } + } + } + labels(first: $firstLabels) { + nodes { + name + } + } + closingIssuesReferences(first: 50) { + nodes { + number + } + } + } + } +} + fragment AuthorFields on Actor { + login + html_url: url + avatar_url: avatarUrl + type: __typename +} +fragment MilestoneFields on Milestone { + state + title +}`) as unknown as TypedDocumentString; \ No newline at end of file diff --git a/src/renderer/utils/api/graphql/issue.graphql b/src/renderer/utils/api/graphql/issue.graphql new file mode 100644 index 000000000..4a26921a4 --- /dev/null +++ b/src/renderer/utils/api/graphql/issue.graphql @@ -0,0 +1,41 @@ +query FetchIssueByNumber( + $owner: String! + $name: String! + $number: Int! + $firstLabels: Int +) { + repository(owner: $owner, name: $name) { + issue(number: $number) { + __typename + number + title + url + state + stateReason + milestone { + ...MilestoneFields + } + author { + ...AuthorFields + } + comments(last: 1) { + totalCount + nodes { + author { + ...AuthorFields + } + } + } + labels(first: $firstLabels) { + nodes { + name + } + } + } + } +} + +fragment MilestoneFields on Milestone { + state + title +} diff --git a/src/renderer/utils/api/graphql/pull.graphql b/src/renderer/utils/api/graphql/pull.graphql new file mode 100644 index 000000000..dc4894d7f --- /dev/null +++ b/src/renderer/utils/api/graphql/pull.graphql @@ -0,0 +1,57 @@ +query FetchPullByNumber( + $owner: String! + $name: String! + $number: Int! + $firstLabels: Int +) { + repository(owner: $owner, name: $name) { + pullRequest(number: $number) { + __typename + number + title + url + state + merged + isDraft + isInMergeQueue + milestone { + ...MilestoneFields + } + author { + ...AuthorFields + } + comments(last: 1) { + totalCount + nodes { + author { + ...AuthorFields + } + } + } + reviews(last: 1) { + totalCount + nodes { + createdAt + author { + login + } + } + } + labels(first: $firstLabels) { + nodes { + name + } + } + closingIssuesReferences(first: 50) { + nodes { + number + } + } + } + } +} + +fragment MilestoneFields on Milestone { + state + title +} diff --git a/src/renderer/utils/notifications/handlers/default.ts b/src/renderer/utils/notifications/handlers/default.ts index 99f66b474..aa76824d0 100644 --- a/src/renderer/utils/notifications/handlers/default.ts +++ b/src/renderer/utils/notifications/handlers/default.ts @@ -11,6 +11,11 @@ import type { Subject, SubjectType, } from '../../../typesGitHub'; +import { + IssueState, + IssueStateReason, + PullRequestState, +} from '../../api/graphql/generated/graphql'; import type { NotificationTypeHandler } from './types'; import { formatForDisplay } from './utils'; @@ -34,13 +39,20 @@ export class DefaultHandler implements NotificationTypeHandler { case 'reopened': case 'ANSWERED': case 'success': + case IssueState.Open: + case IssueStateReason.Reopened: + case PullRequestState.Open: return IconColor.GREEN; case 'closed': case 'failure': + case IssueState.Closed: + case PullRequestState.Closed: return IconColor.RED; case 'completed': case 'RESOLVED': case 'merged': + case IssueStateReason.Completed: + case PullRequestState.Merged: return IconColor.PURPLE; default: return IconColor.GRAY; diff --git a/src/renderer/utils/notifications/handlers/discussion.test.ts b/src/renderer/utils/notifications/handlers/discussion.test.ts index 5f28fb321..1c6230f26 100644 --- a/src/renderer/utils/notifications/handlers/discussion.test.ts +++ b/src/renderer/utils/notifications/handlers/discussion.test.ts @@ -17,7 +17,7 @@ import { discussionHandler } from './discussion'; const mockDiscussionAuthor: AuthorFieldsFragment = { login: 'discussion-author', - url: 'https://github.com/discussion-author' as Link, + html_url: 'https://github.com/discussion-author' as Link, avatar_url: 'https://avatars.githubusercontent.com/u/123456789?v=4' as Link, type: 'User', }; @@ -70,7 +70,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'ANSWERED', user: { login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.url, + html_url: mockDiscussionAuthor.html_url, avatar_url: mockDiscussionAuthor.avatar_url, type: mockDiscussionAuthor.type, }, @@ -103,7 +103,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'DUPLICATE', user: { login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.url, + html_url: mockDiscussionAuthor.html_url, avatar_url: mockDiscussionAuthor.avatar_url, type: mockDiscussionAuthor.type, }, @@ -133,7 +133,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'OPEN', user: { login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.url, + html_url: mockDiscussionAuthor.html_url, avatar_url: mockDiscussionAuthor.avatar_url, type: mockDiscussionAuthor.type, }, @@ -166,7 +166,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'OUTDATED', user: { login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.url, + html_url: mockDiscussionAuthor.html_url, avatar_url: mockDiscussionAuthor.avatar_url, type: mockDiscussionAuthor.type, }, @@ -199,7 +199,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'REOPENED', user: { login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.url, + html_url: mockDiscussionAuthor.html_url, avatar_url: mockDiscussionAuthor.avatar_url, type: mockDiscussionAuthor.type, }, @@ -232,7 +232,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'RESOLVED', user: { login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.url, + html_url: mockDiscussionAuthor.html_url, avatar_url: mockDiscussionAuthor.avatar_url, type: mockDiscussionAuthor.type, }, @@ -272,7 +272,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'ANSWERED', user: { login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.url, + html_url: mockDiscussionAuthor.html_url, avatar_url: mockDiscussionAuthor.avatar_url, type: mockDiscussionAuthor.type, }, diff --git a/src/renderer/utils/notifications/handlers/discussion.ts b/src/renderer/utils/notifications/handlers/discussion.ts index cf66a8de7..2f1a7ddf7 100644 --- a/src/renderer/utils/notifications/handlers/discussion.ts +++ b/src/renderer/utils/notifications/handlers/discussion.ts @@ -75,7 +75,7 @@ class DiscussionHandler extends DefaultHandler { let discussionUser: SubjectUser = { login: discussion.author.login, - html_url: discussion.author.url, + html_url: discussion.author.html_url, avatar_url: discussion.author.avatar_url, type: discussion.author.type, }; @@ -83,7 +83,7 @@ class DiscussionHandler extends DefaultHandler { if (latestDiscussionComment) { discussionUser = { login: latestDiscussionComment.author.login, - html_url: latestDiscussionComment.author.url, + html_url: latestDiscussionComment.author.html_url, avatar_url: latestDiscussionComment.author.avatar_url, type: latestDiscussionComment.author.type, }; diff --git a/src/renderer/utils/notifications/handlers/issue.ts b/src/renderer/utils/notifications/handlers/issue.ts index c351b7a5c..256c65dd1 100644 --- a/src/renderer/utils/notifications/handlers/issue.ts +++ b/src/renderer/utils/notifications/handlers/issue.ts @@ -14,49 +14,36 @@ import type { GitifySubject, Notification, Subject, - User, } from '../../../typesGitHub'; -import { getIssue, getIssueOrPullRequestComment } from '../../api/client'; -import { isStateFilteredOut } from '../filters/filter'; +import { fetchIssueByNumber } from '../../api/client'; import { DefaultHandler } from './default'; -import { getSubjectUser } from './utils'; class IssueHandler extends DefaultHandler { readonly type = 'Issue'; async enrich( notification: Notification, - settings: SettingsState, + _settings: SettingsState, ): Promise { - const issue = ( - await getIssue(notification.subject.url, notification.account.token) - ).data; + const response = await fetchIssueByNumber(notification); + const issue = response.data.repository?.issue; - const issueState = issue.state_reason ?? issue.state; + // const issueState = issue.stateReason ?? issue.state; // Return early if this notification would be hidden by filters - if (isStateFilteredOut(issueState, settings)) { - return null; - } - - let issueCommentUser: User; + // if (isStateFilteredOut(issueState, settings)) { + // return null; + // } - if (notification.subject.latest_comment_url) { - const issueComment = ( - await getIssueOrPullRequestComment( - notification.subject.latest_comment_url, - notification.account.token, - ) - ).data; - issueCommentUser = issueComment.user; - } + // const issueCommentUser = issue.comments.nodes[0]?.author; return { number: issue.number, - state: issueState, - user: getSubjectUser([issueCommentUser, issue.user]), - comments: issue.comments, - labels: issue.labels?.map((label) => label.name) ?? [], + // state: issueState + state: null, + user: null, //getSubjectUser([issueCommentUser, issue.author]), + comments: issue.comments.totalCount, + labels: issue.labels.nodes?.map((label) => label.name) ?? [], milestone: issue.milestone, }; } diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index f58eccd98..12160cda6 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -8,7 +8,7 @@ import { GitPullRequestIcon, } from '@primer/octicons-react'; -import type { Link, SettingsState } from '../../../types'; +import type { SettingsState } from '../../../types'; import type { GitifyPullRequestReview, GitifySubject, @@ -17,16 +17,12 @@ import type { PullRequestReview, PullRequestStateType, Subject, - User, } from '../../../typesGitHub'; -import { - getIssueOrPullRequestComment, - getPullRequest, - getPullRequestReviews, -} from '../../api/client'; +import { fetchPullByNumber } from '../../api/client'; +import type { PullRequestState } from '../../api/graphql/generated/graphql'; import { isStateFilteredOut, isUserFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; -import { getSubjectUser } from './utils'; +import { getSubjectAuthor } from './utils'; class PullRequestHandler extends DefaultHandler { readonly type = 'PullRequest' as const; @@ -35,14 +31,11 @@ class PullRequestHandler extends DefaultHandler { notification: Notification, settings: SettingsState, ): Promise { - const pr = ( - await getPullRequest(notification.subject.url, notification.account.token) - ).data; - - let prState: PullRequestStateType = pr.state; - if (pr.merged) { - prState = 'merged'; - } else if (pr.draft) { + const response = await fetchPullByNumber(notification); + const pr = response.data.repository.pullRequest; + + let prState: PullRequestStateType | PullRequestState = pr.state; + if (pr.isDraft) { prState = 'draft'; } @@ -51,39 +44,28 @@ class PullRequestHandler extends DefaultHandler { return null; } - let prCommentUser: User; - if ( - notification.subject.latest_comment_url && - notification.subject.latest_comment_url !== notification.subject.url - ) { - const prComment = ( - await getIssueOrPullRequestComment( - notification.subject.latest_comment_url, - notification.account.token, - ) - ).data; - prCommentUser = prComment.user; - } + const prCommentUser = pr.comments.nodes[0]?.author; - const prUser = getSubjectUser([prCommentUser, pr.user]); + const prUser = getSubjectAuthor([prCommentUser, pr.author]); // Return early if this notification would be hidden by user filters if (isUserFilteredOut(prUser, settings)) { return null; } - const reviews = await getLatestReviewForReviewers(notification); - const linkedIssues = parseLinkedIssuesFromPr(pr); + const reviews = null; // await getLatestReviewForReviewers(notification); return { number: pr.number, state: prState, user: prUser, reviews: reviews, - comments: pr.comments, - labels: pr.labels?.map((label) => label.name) ?? [], - linkedIssues: linkedIssues, - milestone: pr.milestone, + comments: pr.comments.totalCount, + labels: pr.labels.nodes?.map((label) => label.name) ?? [], + linkedIssues: pr.closingIssuesReferences.nodes.map( + (issue) => `#${issue.number}`, + ), + milestone: null, //pr.milestone, }; } @@ -110,10 +92,7 @@ export async function getLatestReviewForReviewers( return null; } - const prReviews = await getPullRequestReviews( - `${notification.subject.url}/reviews` as Link, - notification.account.token, - ); + const prReviews = null; if (!prReviews.data.length) { return null; diff --git a/src/renderer/utils/notifications/handlers/utils.ts b/src/renderer/utils/notifications/handlers/utils.ts index e8dc1b13b..e46b70b90 100644 --- a/src/renderer/utils/notifications/handlers/utils.ts +++ b/src/renderer/utils/notifications/handlers/utils.ts @@ -1,4 +1,5 @@ import type { SubjectUser, User } from '../../../typesGitHub'; +import type { AuthorFieldsFragment } from '../../api/graphql/generated/graphql'; /** * Construct the notification subject user based on an order prioritized list of users @@ -24,6 +25,30 @@ export function getSubjectUser(users: User[]): SubjectUser { return subjectUser; } +/** + * Construct the notification subject user based on an order prioritized list of users + * @param users array of users in order or priority + * @returns the subject user + */ +export function getSubjectAuthor(users: AuthorFieldsFragment[]): SubjectUser { + let subjectUser: SubjectUser = null; + + for (const user of users) { + if (user) { + subjectUser = { + login: user.login, + html_url: user.html_url, + avatar_url: user.avatar_url, + type: user.type, + }; + + return subjectUser; + } + } + + return subjectUser; +} + export function formatForDisplay(text: string[]): string { if (!text) { return ''; From 6932eca241784e8ea3a939962d84a2b883ac6944 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Sun, 21 Dec 2025 16:36:32 +1000 Subject: [PATCH 02/25] refactor(api): use graphql api for issue and pull request enrichment Signed-off-by: Adam Setch --- src/renderer/typesGitHub.ts | 109 +----------------- .../utils/api/__mocks__/response-mocks.ts | 2 + .../utils/api/graphql/discussion.graphql | 1 + .../utils/api/graphql/generated/gql.ts | 18 +-- .../utils/api/graphql/generated/graphql.ts | 15 ++- src/renderer/utils/api/graphql/issue.graphql | 1 + src/renderer/utils/api/graphql/pull.graphql | 2 + src/renderer/utils/helpers.ts | 93 ++------------- src/renderer/utils/links.ts | 12 +- .../notifications/handlers/checkSuite.ts | 26 ++++- .../utils/notifications/handlers/default.ts | 2 + .../notifications/handlers/discussion.ts | 1 + .../utils/notifications/handlers/issue.ts | 1 + .../handlers/pullRequest.test.ts | 44 +------ .../notifications/handlers/pullRequest.ts | 21 +--- .../notifications/handlers/workflowRun.ts | 16 ++- 16 files changed, 93 insertions(+), 271 deletions(-) diff --git a/src/renderer/typesGitHub.ts b/src/renderer/typesGitHub.ts index c6d20a68a..83e581aab 100644 --- a/src/renderer/typesGitHub.ts +++ b/src/renderer/typesGitHub.ts @@ -270,46 +270,7 @@ export interface GitifySubject { comments?: number; labels?: string[]; milestone?: MilestoneFieldsFragment; -} - -export interface PullRequest { - url: Link; - id: number; - node_id: string; - html_url: Link; - diff_url: Link; - patch_url: Link; - issue_url: Link; - number: number; - state: PullRequestStateType; - locked: boolean; - title: string; - user: User; - body: string; - created_at: string; - updated_at: string; - closed_at: string | null; - merged_at: string | null; - merge_commit_sha: string | null; - labels: Labels[]; - milestone: Milestone | null; - draft: boolean; - commits_url: Link; - review_comments_url: Link; - review_comment_url: Link; - comments_url: Link; - statuses_url: Link; - author_association: string; - merged: boolean; - mergeable: boolean; - rebaseable: boolean; - comments: number; - review_comments: number; - maintainer_can_modify: boolean; - commits: number; - additions: number; - deletions: number; - changed_files: number; + htmlUrl?: Link; } export interface GitifyPullRequestReview { @@ -317,16 +278,6 @@ export interface GitifyPullRequestReview { users: string[]; } -export interface Labels { - id: number; - node_id: string; - url: Link; - name: string; - color: string; - default: boolean; - description: string; -} - export interface PullRequestReview { id: number; node_id: string; @@ -411,64 +362,6 @@ export interface CommitComment { body: string; } -export interface Issue { - url: Link; - repository_url: Link; - labels_url: Link; - comments_url: Link; - events_url: Link; - html_url: Link; - id: number; - node_id: string; - number: number; - title: string; - user: User; - state: IssueStateType; - locked: boolean; - labels: Labels[]; - milestone: Milestone | null; - comments: number; - created_at: string; - updated_at: string; - closed_at: string | null; - author_association: string; - body: string; - state_reason: IssueStateReasonType | null; -} - -export interface IssueOrPullRequestComment { - url: Link; - html_url: Link; - issue_url: Link; - id: number; - node_id: string; - user: User; - created_at: string; - updated_at: string; - body: string; -} - -export interface Milestone { - url: Link; - html_url: Link; - labels_url: Link; - id: number; - node_id: string; - number: number; - title: string; - description: string; - creator: User; - open_issues: number; - closed_issues: number; - state: MilestoneStateType; - created_at: string; - updated_at: string; - due_on: string | null; - closed_at: string | null; -} - -type MilestoneStateType = 'open' | 'closed'; - export interface Release { url: Link; assets_url: Link; diff --git a/src/renderer/utils/api/__mocks__/response-mocks.ts b/src/renderer/utils/api/__mocks__/response-mocks.ts index 6f8a9cabf..450447b60 100644 --- a/src/renderer/utils/api/__mocks__/response-mocks.ts +++ b/src/renderer/utils/api/__mocks__/response-mocks.ts @@ -398,6 +398,7 @@ export const mockDiscussionByNumberGraphQLResponse: FetchDiscussionByNumberQuery 'https://avatars.githubusercontent.com/u/123456789?v=4' as Link, type: 'User', }, + url: 'https://github.com/gitify-app/notifications-test/discussions/612#discussioncomment-67890', replies: { nodes: [ { @@ -410,6 +411,7 @@ export const mockDiscussionByNumberGraphQLResponse: FetchDiscussionByNumberQuery 'https://avatars.githubusercontent.com/u/123456789?v=4' as Link, type: 'User', }, + url: 'https://github.com/gitify-app/notifications-test/discussions/612#discussioncomment-12345', }, ], }, diff --git a/src/renderer/utils/api/graphql/discussion.graphql b/src/renderer/utils/api/graphql/discussion.graphql index 3620434e7..1095fc98a 100644 --- a/src/renderer/utils/api/graphql/discussion.graphql +++ b/src/renderer/utils/api/graphql/discussion.graphql @@ -44,4 +44,5 @@ fragment CommentFields on DiscussionComment { author { ...AuthorFields } + url } diff --git a/src/renderer/utils/api/graphql/generated/gql.ts b/src/renderer/utils/api/graphql/generated/gql.ts index cd7a5ddc7..42467e918 100644 --- a/src/renderer/utils/api/graphql/generated/gql.ts +++ b/src/renderer/utils/api/graphql/generated/gql.ts @@ -16,15 +16,15 @@ import * as types from './graphql'; */ type Documents = { "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}": typeof types.AuthorFieldsFragmentDoc, - "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n}": typeof types.FetchDiscussionByNumberDocument, - "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchIssueByNumberDocument, - "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 1) {\n totalCount\n nodes {\n createdAt\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchPullByNumberDocument, + "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}": typeof types.FetchDiscussionByNumberDocument, + "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchIssueByNumberDocument, + "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 1) {\n totalCount\n nodes {\n url\n createdAt\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchPullByNumberDocument, }; const documents: Documents = { "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}": types.AuthorFieldsFragmentDoc, - "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n}": types.FetchDiscussionByNumberDocument, - "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchIssueByNumberDocument, - "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 1) {\n totalCount\n nodes {\n createdAt\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchPullByNumberDocument, + "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}": types.FetchDiscussionByNumberDocument, + "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchIssueByNumberDocument, + "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 1) {\n totalCount\n nodes {\n url\n createdAt\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchPullByNumberDocument, }; /** @@ -34,15 +34,15 @@ export function graphql(source: "fragment AuthorFields on Actor {\n login\n ht /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n}"): typeof import('./graphql').FetchDiscussionByNumberDocument; +export function graphql(source: "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}"): typeof import('./graphql').FetchDiscussionByNumberDocument; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').FetchIssueByNumberDocument; +export function graphql(source: "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').FetchIssueByNumberDocument; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 1) {\n totalCount\n nodes {\n createdAt\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').FetchPullByNumberDocument; +export function graphql(source: "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 1) {\n totalCount\n nodes {\n url\n createdAt\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').FetchPullByNumberDocument; export function graphql(source: string) { diff --git a/src/renderer/utils/api/graphql/generated/graphql.ts b/src/renderer/utils/api/graphql/generated/graphql.ts index a202f2d94..e65b3a522 100644 --- a/src/renderer/utils/api/graphql/generated/graphql.ts +++ b/src/renderer/utils/api/graphql/generated/graphql.ts @@ -36203,7 +36203,7 @@ export type FetchDiscussionByNumberQuery = { __typename?: 'Query', repository?: | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } - | null, comments: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, replies: { __typename?: 'DiscussionCommentConnection', nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, author?: + | null, comments: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, replies: { __typename?: 'DiscussionCommentConnection', nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, author?: | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } @@ -36217,7 +36217,7 @@ export type FetchDiscussionByNumberQuery = { __typename?: 'Query', repository?: | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null } | null } | null }; -export type CommentFieldsFragment = { __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, author?: +export type CommentFieldsFragment = { __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, author?: | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } @@ -36239,7 +36239,7 @@ export type FetchIssueByNumberQuery = { __typename?: 'Query', repository?: { __t | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } - | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', author?: + | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', url: any, author?: | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } @@ -36263,13 +36263,13 @@ export type FetchPullByNumberQuery = { __typename?: 'Query', repository?: { __ty | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } - | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', author?: + | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', url: any, author?: | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } - | null } | null> | null }, reviews?: { __typename?: 'PullRequestReviewConnection', totalCount: number, nodes?: Array<{ __typename?: 'PullRequestReview', createdAt: any, author?: + | null } | null> | null }, reviews?: { __typename?: 'PullRequestReviewConnection', totalCount: number, nodes?: Array<{ __typename?: 'PullRequestReview', url: any, createdAt: any, author?: | { __typename?: 'Bot', login: string } | { __typename?: 'EnterpriseUserAccount', login: string } | { __typename?: 'Mannequin', login: string } @@ -36310,6 +36310,7 @@ export const CommentFieldsFragmentDoc = new TypedDocumentString(` author { ...AuthorFields } + url } fragment AuthorFields on Actor { login @@ -36367,6 +36368,7 @@ fragment CommentFields on DiscussionComment { author { ...AuthorFields } + url }`) as unknown as TypedDocumentString; export const FetchIssueByNumberDocument = new TypedDocumentString(` query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) { @@ -36387,6 +36389,7 @@ export const FetchIssueByNumberDocument = new TypedDocumentString(` comments(last: 1) { totalCount nodes { + url author { ...AuthorFields } @@ -36431,6 +36434,7 @@ export const FetchPullByNumberDocument = new TypedDocumentString(` comments(last: 1) { totalCount nodes { + url author { ...AuthorFields } @@ -36439,6 +36443,7 @@ export const FetchPullByNumberDocument = new TypedDocumentString(` reviews(last: 1) { totalCount nodes { + url createdAt author { login diff --git a/src/renderer/utils/api/graphql/issue.graphql b/src/renderer/utils/api/graphql/issue.graphql index 4a26921a4..f042b0c4e 100644 --- a/src/renderer/utils/api/graphql/issue.graphql +++ b/src/renderer/utils/api/graphql/issue.graphql @@ -21,6 +21,7 @@ query FetchIssueByNumber( comments(last: 1) { totalCount nodes { + url author { ...AuthorFields } diff --git a/src/renderer/utils/api/graphql/pull.graphql b/src/renderer/utils/api/graphql/pull.graphql index dc4894d7f..e42c67980 100644 --- a/src/renderer/utils/api/graphql/pull.graphql +++ b/src/renderer/utils/api/graphql/pull.graphql @@ -23,6 +23,7 @@ query FetchPullByNumber( comments(last: 1) { totalCount nodes { + url author { ...AuthorFields } @@ -31,6 +32,7 @@ query FetchPullByNumber( reviews(last: 1) { totalCount nodes { + url createdAt author { login diff --git a/src/renderer/utils/helpers.ts b/src/renderer/utils/helpers.ts index 5a8bf5803..2fd46942e 100644 --- a/src/renderer/utils/helpers.ts +++ b/src/renderer/utils/helpers.ts @@ -7,12 +7,9 @@ import { import { Constants } from '../constants'; import type { Chevron, Hostname, Link } from '../types'; import type { Notification } from '../typesGitHub'; -import { fetchDiscussionByNumber, getHtmlUrl } from './api/client'; +import { getHtmlUrl } from './api/client'; import type { PlatformType } from './auth/types'; import { rendererLogError } from './logger'; -import { getCheckSuiteAttributes } from './notifications/handlers/checkSuite'; -import { getClosestDiscussionCommentOrReply } from './notifications/handlers/discussion'; -import { getWorkflowRunAttributes } from './notifications/handlers/workflowRun'; export function getPlatformFromHostname(hostname: string): PlatformType { return hostname.endsWith(Constants.DEFAULT_AUTH_OPTIONS.hostname) @@ -31,40 +28,6 @@ export function generateNotificationReferrerId( return btoa(raw); } -export function getCheckSuiteUrl(notification: Notification): Link { - const filters = []; - - const checkSuiteAttributes = getCheckSuiteAttributes(notification); - - if (checkSuiteAttributes?.workflowName) { - filters.push( - `workflow:"${checkSuiteAttributes.workflowName.replaceAll(' ', '+')}"`, - ); - } - - if (checkSuiteAttributes?.status) { - filters.push(`is:${checkSuiteAttributes.status}`); - } - - if (checkSuiteAttributes?.branchName) { - filters.push(`branch:${checkSuiteAttributes.branchName}`); - } - - return actionsURL(notification.repository.html_url, filters); -} - -export function getWorkflowRunUrl(notification: Notification): Link { - const filters = []; - - const workflowRunAttributes = getWorkflowRunAttributes(notification); - - if (workflowRunAttributes?.status) { - filters.push(`is:${workflowRunAttributes.status}`); - } - - return actionsURL(notification.repository.html_url, filters); -} - /** * Construct a GitHub Actions URL for a repository with optional filters. */ @@ -80,56 +43,22 @@ export function actionsURL(repositoryURL: string, filters: string[]): Link { return url.toString().replaceAll('%2B', '+') as Link; } -async function getDiscussionUrl(notification: Notification): Promise { - const url = new URL(notification.repository.html_url); - url.pathname += '/discussions'; - - const response = await fetchDiscussionByNumber(notification); - const discussion = response.data.repository.discussion; - - if (discussion) { - url.href = discussion.url; - - const closestComment = getClosestDiscussionCommentOrReply( - notification, - discussion.comments.nodes, - ); - if (closestComment) { - url.hash = `#discussioncomment-${closestComment.databaseId}`; - } - } - - return url.toString() as Link; -} - export async function generateGitHubWebUrl( notification: Notification, ): Promise { const url = new URL(getDefaultURLForType(notification)); try { - switch (notification.subject.type) { - case 'CheckSuite': - url.href = getCheckSuiteUrl(notification); - break; - case 'Discussion': - url.href = await getDiscussionUrl(notification); - break; - case 'WorkflowRun': - url.href = getWorkflowRunUrl(notification); - break; - default: - if (notification.subject.latest_comment_url) { - url.href = await getHtmlUrl( - notification.subject.latest_comment_url, - notification.account.token, - ); - } else if (notification.subject.url) { - url.href = await getHtmlUrl( - notification.subject.url, - notification.account.token, - ); - } + if (notification.subject.latest_comment_url) { + url.href = await getHtmlUrl( + notification.subject.latest_comment_url, + notification.account.token, + ); + } else if (notification.subject.url) { + url.href = await getHtmlUrl( + notification.subject.url, + notification.account.token, + ); } } catch (err) { rendererLogError( diff --git a/src/renderer/utils/links.ts b/src/renderer/utils/links.ts index 88ce44109..d5d0a43fc 100644 --- a/src/renderer/utils/links.ts +++ b/src/renderer/utils/links.ts @@ -5,7 +5,7 @@ import type { Account, Hostname, Link } from '../types'; import type { Notification, Repository, SubjectUser } from '../typesGitHub'; import { getDeveloperSettingsURL } from './auth/utils'; import { openExternalLink } from './comms'; -import { generateGitHubWebUrl } from './helpers'; +import { generateNotificationReferrerId } from './helpers'; export function openGitifyReleaseNotes(version: string) { openExternalLink( @@ -55,8 +55,14 @@ export function openRepository(repository: Repository) { } export async function openNotification(notification: Notification) { - const url = await generateGitHubWebUrl(notification); - openExternalLink(url); + const url = new URL(notification.subject.htmlUrl); + + url.searchParams.set( + 'notification_referrer_id', + generateNotificationReferrerId(notification), + ); + + openExternalLink(url.toString() as Link); } export function openGitHubParticipatingDocs() { diff --git a/src/renderer/utils/notifications/handlers/checkSuite.ts b/src/renderer/utils/notifications/handlers/checkSuite.ts index e4a6fcf36..0c285dbd9 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.ts @@ -9,7 +9,7 @@ import { XIcon, } from '@primer/octicons-react'; -import type { SettingsState } from '../../../types'; +import type { Link, SettingsState } from '../../../types'; import type { CheckSuiteAttributes, CheckSuiteStatus, @@ -17,6 +17,7 @@ import type { Notification, Subject, } from '../../../typesGitHub'; +import { actionsURL } from '../../helpers'; import { DefaultHandler } from './default'; class CheckSuiteHandler extends DefaultHandler { @@ -32,6 +33,7 @@ class CheckSuiteHandler extends DefaultHandler { return { state: state, user: null, + htmlUrl: getCheckSuiteUrl(notification), }; } @@ -99,3 +101,25 @@ function getCheckSuiteStatus(statusDisplayName: string): CheckSuiteStatus { return null; } } + +export function getCheckSuiteUrl(notification: Notification): Link { + const filters = []; + + const checkSuiteAttributes = getCheckSuiteAttributes(notification); + + if (checkSuiteAttributes?.workflowName) { + filters.push( + `workflow:"${checkSuiteAttributes.workflowName.replaceAll(' ', '+')}"`, + ); + } + + if (checkSuiteAttributes?.status) { + filters.push(`is:${checkSuiteAttributes.status}`); + } + + if (checkSuiteAttributes?.branchName) { + filters.push(`branch:${checkSuiteAttributes.branchName}`); + } + + return actionsURL(notification.repository.html_url, filters); +} diff --git a/src/renderer/utils/notifications/handlers/default.ts b/src/renderer/utils/notifications/handlers/default.ts index aa76824d0..6624f7543 100644 --- a/src/renderer/utils/notifications/handlers/default.ts +++ b/src/renderer/utils/notifications/handlers/default.ts @@ -65,11 +65,13 @@ export class DefaultHandler implements NotificationTypeHandler { notification.subject.type, ]); } + formattedNotificationNumber(notification: Notification): string { return notification.subject?.number ? `#${notification.subject.number}` : ''; } + formattedNotificationTitle(notification: Notification): string { let title = notification.subject.title; diff --git a/src/renderer/utils/notifications/handlers/discussion.ts b/src/renderer/utils/notifications/handlers/discussion.ts index 2f1a7ddf7..aa269876f 100644 --- a/src/renderer/utils/notifications/handlers/discussion.ts +++ b/src/renderer/utils/notifications/handlers/discussion.ts @@ -95,6 +95,7 @@ class DiscussionHandler extends DefaultHandler { user: discussionUser, comments: discussion.comments.totalCount, labels: discussion.labels?.nodes.map((label) => label.name) ?? [], + htmlUrl: latestDiscussionComment.url ?? discussion.url, }; } diff --git a/src/renderer/utils/notifications/handlers/issue.ts b/src/renderer/utils/notifications/handlers/issue.ts index 256c65dd1..ce2eda76f 100644 --- a/src/renderer/utils/notifications/handlers/issue.ts +++ b/src/renderer/utils/notifications/handlers/issue.ts @@ -45,6 +45,7 @@ class IssueHandler extends DefaultHandler { comments: issue.comments.totalCount, labels: issue.labels.nodes?.map((label) => label.name) ?? [], milestone: issue.milestone, + htmlUrl: issue.comments.nodes[0]?.url ?? issue.url, }; } diff --git a/src/renderer/utils/notifications/handlers/pullRequest.test.ts b/src/renderer/utils/notifications/handlers/pullRequest.test.ts index c251d6fe2..9f5d8ce91 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.test.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.test.ts @@ -8,12 +8,8 @@ import { import { mockSettings } from '../../../__mocks__/state-mocks'; import { createPartialMockUser } from '../../../__mocks__/user-mocks'; import type { Link } from '../../../types'; -import type { Notification, PullRequest } from '../../../typesGitHub'; -import { - getLatestReviewForReviewers, - parseLinkedIssuesFromPr, - pullRequestHandler, -} from './pullRequest'; +import type { Notification } from '../../../typesGitHub'; +import { getLatestReviewForReviewers, pullRequestHandler } from './pullRequest'; describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { let mockNotification: Notification; @@ -357,42 +353,6 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { }); }); - describe('Pull Request With Linked Issues', () => { - it('returns empty if no pr body', () => { - const mockPr = { - user: { - type: 'User', - }, - body: null, - } as PullRequest; - - const result = parseLinkedIssuesFromPr(mockPr); - expect(result).toEqual([]); - }); - - it('returns empty if pr from non-user', () => { - const mockPr = { - user: { - type: 'Bot', - }, - body: 'This PR is linked to #1, #2, and #3', - } as PullRequest; - const result = parseLinkedIssuesFromPr(mockPr); - expect(result).toEqual([]); - }); - - it('returns linked issues', () => { - const mockPr = { - user: { - type: 'User', - }, - body: 'This PR is linked to #1, #2, and #3', - } as PullRequest; - const result = parseLinkedIssuesFromPr(mockPr); - expect(result).toEqual(['#1', '#2', '#3']); - }); - }); - it('early return if pull request state filtered', async () => { nock('https://api.github.com') .get('/repos/gitify-app/notifications-test/pulls/1') diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index 12160cda6..b98b4699d 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -13,7 +13,6 @@ import type { GitifyPullRequestReview, GitifySubject, Notification, - PullRequest, PullRequestReview, PullRequestStateType, Subject, @@ -66,6 +65,7 @@ class PullRequestHandler extends DefaultHandler { (issue) => `#${issue.number}`, ), milestone: null, //pr.milestone, + htmlUrl: pr.comments.nodes[0]?.url ?? pr.url, }; } @@ -133,22 +133,3 @@ export async function getLatestReviewForReviewers( return a.state.localeCompare(b.state); }); } - -export function parseLinkedIssuesFromPr(pr: PullRequest): string[] { - const linkedIssues: string[] = []; - - if (!pr.body || pr.user.type !== 'User') { - return linkedIssues; - } - - const regexPattern = /\s?#(\d+)\s?/gi; - const matches = pr.body.matchAll(regexPattern); - - for (const match of matches) { - if (match[0]) { - linkedIssues.push(match[0].trim()); - } - } - - return linkedIssues; -} diff --git a/src/renderer/utils/notifications/handlers/workflowRun.ts b/src/renderer/utils/notifications/handlers/workflowRun.ts index 9dfd7df2e..3bfe40742 100644 --- a/src/renderer/utils/notifications/handlers/workflowRun.ts +++ b/src/renderer/utils/notifications/handlers/workflowRun.ts @@ -3,7 +3,7 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; import { RocketIcon } from '@primer/octicons-react'; -import type { SettingsState } from '../../../types'; +import type { Link, SettingsState } from '../../../types'; import type { CheckSuiteStatus, GitifySubject, @@ -11,6 +11,7 @@ import type { Subject, WorkflowRunAttributes, } from '../../../typesGitHub'; +import { actionsURL } from '../../helpers'; import { DefaultHandler } from './default'; class WorkflowRunHandler extends DefaultHandler { @@ -26,6 +27,7 @@ class WorkflowRunHandler extends DefaultHandler { return { state: state, user: null, + htmlUrl: getWorkflowRunUrl(notification), }; } @@ -72,3 +74,15 @@ function getWorkflowRunStatus(statusDisplayName: string): CheckSuiteStatus { return null; } } + +export function getWorkflowRunUrl(notification: Notification): Link { + const filters = []; + + const workflowRunAttributes = getWorkflowRunAttributes(notification); + + if (workflowRunAttributes?.status) { + filters.push(`is:${workflowRunAttributes.status}`); + } + + return actionsURL(notification.repository.html_url, filters); +} From 356b06ca2dc40677ed062b60cb4032940de89c25 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Sun, 21 Dec 2025 16:46:47 +1000 Subject: [PATCH 03/25] refactor(api): use graphql api for issue and pull request enrichment Signed-off-by: Adam Setch --- src/renderer/utils/helpers.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/renderer/utils/helpers.ts b/src/renderer/utils/helpers.ts index 2fd46942e..be990d817 100644 --- a/src/renderer/utils/helpers.ts +++ b/src/renderer/utils/helpers.ts @@ -81,6 +81,9 @@ export function getDefaultURLForType(notification: Notification) { const url = new URL(notification.repository.html_url); switch (notification.subject.type) { + case 'CheckSuite': + url.pathname += '/actions'; + break; case 'Discussion': url.pathname += '/discussions'; break; @@ -90,12 +93,18 @@ export function getDefaultURLForType(notification: Notification) { case 'PullRequest': url.pathname += '/pulls'; break; + case 'Release': + url.pathname += '/releases'; + break; case 'RepositoryInvitation': url.pathname += '/invitations'; break; case 'RepositoryDependabotAlertsThread': url.pathname += '/security/dependabot'; break; + case 'WorkflowRun': + url.pathname += '/actions'; + break; default: break; } From cb2b01b432762026b598d33d7ccace70e37287eb Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Sun, 21 Dec 2025 17:10:57 +1000 Subject: [PATCH 04/25] refactor(api): use graphql api for issue and pull request enrichment Signed-off-by: Adam Setch --- src/renderer/utils/helpers.test.ts | 72 ------------------- src/renderer/utils/helpers.ts | 45 +++--------- src/renderer/utils/links.ts | 7 +- .../notifications/handlers/checkSuite.ts | 6 ++ .../utils/notifications/handlers/default.ts | 6 +- .../notifications/handlers/discussion.ts | 8 ++- .../utils/notifications/handlers/issue.ts | 8 ++- .../notifications/handlers/pullRequest.ts | 8 ++- .../utils/notifications/handlers/release.ts | 8 ++- .../repositoryDependabotAlertsThread.ts | 9 ++- .../handlers/repositoryInvitation.ts | 9 ++- .../utils/notifications/handlers/types.ts | 7 +- .../notifications/handlers/workflowRun.ts | 6 ++ 13 files changed, 80 insertions(+), 119 deletions(-) diff --git a/src/renderer/utils/helpers.test.ts b/src/renderer/utils/helpers.test.ts index d29937918..59200dc55 100644 --- a/src/renderer/utils/helpers.test.ts +++ b/src/renderer/utils/helpers.test.ts @@ -6,7 +6,6 @@ import { import type { ExecutionResult } from 'graphql'; -import { createPartialMockNotification } from '../__mocks__/notifications-mocks'; import type { Hostname, Link } from '../types'; import type { SubjectType } from '../typesGitHub'; import * as logger from '../utils/logger'; @@ -20,7 +19,6 @@ import { generateGitHubWebUrl, generateNotificationReferrerId, getChevronDetails, - getDefaultURLForType, getPlatformFromHostname, isEnterpriseServerHost, } from './helpers'; @@ -556,76 +554,6 @@ describe('renderer/utils/helpers.ts', () => { }); }); - describe('getDefaultURLForType', () => { - const mockUrl = 'https://github.com/gitify-app/notifications-test' as Link; - - it('discussions', () => { - const mockNotification = createPartialMockNotification( - { type: 'Discussion' }, - { html_url: mockUrl }, - ); - - expect(getDefaultURLForType(mockNotification)).toEqual( - 'https://github.com/gitify-app/notifications-test/discussions', - ); - }); - - it('issues', () => { - const mockNotification = createPartialMockNotification( - { type: 'Issue' }, - { html_url: mockUrl }, - ); - - expect(getDefaultURLForType(mockNotification)).toEqual( - 'https://github.com/gitify-app/notifications-test/issues', - ); - }); - - it('pull requests', () => { - const mockNotification = createPartialMockNotification( - { type: 'PullRequest' }, - { html_url: mockUrl }, - ); - - expect(getDefaultURLForType(mockNotification)).toEqual( - 'https://github.com/gitify-app/notifications-test/pulls', - ); - }); - - it('repository invitation', () => { - const mockNotification = createPartialMockNotification( - { type: 'RepositoryInvitation' }, - { html_url: mockUrl }, - ); - - expect(getDefaultURLForType(mockNotification)).toEqual( - 'https://github.com/gitify-app/notifications-test/invitations', - ); - }); - - it('repository dependabot alert thread', () => { - const mockNotification = createPartialMockNotification( - { type: 'RepositoryDependabotAlertsThread' }, - { html_url: mockUrl }, - ); - - expect(getDefaultURLForType(mockNotification)).toEqual( - 'https://github.com/gitify-app/notifications-test/security/dependabot', - ); - }); - - it('default web urls', () => { - const mockNotification = createPartialMockNotification( - { type: 'Commit' }, - { html_url: mockUrl }, - ); - - expect(getDefaultURLForType(mockNotification)).toEqual( - 'https://github.com/gitify-app/notifications-test', - ); - }); - }); - describe('getChevronDetails', () => { it('should return correct chevron details', () => { expect(getChevronDetails(true, true, 'account')).toEqual({ diff --git a/src/renderer/utils/helpers.ts b/src/renderer/utils/helpers.ts index be990d817..6c980fcf4 100644 --- a/src/renderer/utils/helpers.ts +++ b/src/renderer/utils/helpers.ts @@ -10,6 +10,7 @@ import type { Notification } from '../typesGitHub'; import { getHtmlUrl } from './api/client'; import type { PlatformType } from './auth/types'; import { rendererLogError } from './logger'; +import { createNotificationHandler } from './notifications/handlers'; export function getPlatformFromHostname(hostname: string): PlatformType { return hostname.endsWith(Constants.DEFAULT_AUTH_OPTIONS.hostname) @@ -46,10 +47,12 @@ export function actionsURL(repositoryURL: string, filters: string[]): Link { export async function generateGitHubWebUrl( notification: Notification, ): Promise { - const url = new URL(getDefaultURLForType(notification)); + const url = new URL(notification.repository.html_url); try { - if (notification.subject.latest_comment_url) { + if (notification.subject.htmlUrl) { + url.href = notification.subject.htmlUrl; + } else if (notification.subject.latest_comment_url) { url.href = await getHtmlUrl( notification.subject.latest_comment_url, notification.account.token, @@ -59,6 +62,9 @@ export async function generateGitHubWebUrl( notification.subject.url, notification.account.token, ); + } else { + const handler = createNotificationHandler(notification); + handler.defaultUrl(notification); } } catch (err) { rendererLogError( @@ -77,41 +83,6 @@ export async function generateGitHubWebUrl( return url.toString() as Link; } -export function getDefaultURLForType(notification: Notification) { - const url = new URL(notification.repository.html_url); - - switch (notification.subject.type) { - case 'CheckSuite': - url.pathname += '/actions'; - break; - case 'Discussion': - url.pathname += '/discussions'; - break; - case 'Issue': - url.pathname += '/issues'; - break; - case 'PullRequest': - url.pathname += '/pulls'; - break; - case 'Release': - url.pathname += '/releases'; - break; - case 'RepositoryInvitation': - url.pathname += '/invitations'; - break; - case 'RepositoryDependabotAlertsThread': - url.pathname += '/security/dependabot'; - break; - case 'WorkflowRun': - url.pathname += '/actions'; - break; - default: - break; - } - - return url.href; -} - export function getChevronDetails( hasNotifications: boolean, isVisible: boolean, diff --git a/src/renderer/utils/links.ts b/src/renderer/utils/links.ts index d5d0a43fc..32803f4c6 100644 --- a/src/renderer/utils/links.ts +++ b/src/renderer/utils/links.ts @@ -5,7 +5,10 @@ import type { Account, Hostname, Link } from '../types'; import type { Notification, Repository, SubjectUser } from '../typesGitHub'; import { getDeveloperSettingsURL } from './auth/utils'; import { openExternalLink } from './comms'; -import { generateNotificationReferrerId } from './helpers'; +import { + generateGitHubWebUrl, + generateNotificationReferrerId, +} from './helpers'; export function openGitifyReleaseNotes(version: string) { openExternalLink( @@ -55,7 +58,7 @@ export function openRepository(repository: Repository) { } export async function openNotification(notification: Notification) { - const url = new URL(notification.subject.htmlUrl); + const url = new URL(await generateGitHubWebUrl(notification)); url.searchParams.set( 'notification_referrer_id', diff --git a/src/renderer/utils/notifications/handlers/checkSuite.ts b/src/renderer/utils/notifications/handlers/checkSuite.ts index 0c285dbd9..9deb6d171 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.ts @@ -54,6 +54,12 @@ class CheckSuiteHandler extends DefaultHandler { return RocketIcon; } } + + defaultUrl(notification: Notification): Link { + const url = new URL(notification.repository.html_url); + url.pathname += '/actions'; + return url.href as Link; + } } export const checkSuiteHandler = new CheckSuiteHandler(); diff --git a/src/renderer/utils/notifications/handlers/default.ts b/src/renderer/utils/notifications/handlers/default.ts index 6624f7543..e5dbfeca6 100644 --- a/src/renderer/utils/notifications/handlers/default.ts +++ b/src/renderer/utils/notifications/handlers/default.ts @@ -3,7 +3,7 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; import { QuestionIcon } from '@primer/octicons-react'; -import type { SettingsState } from '../../../types'; +import type { Link, SettingsState } from '../../../types'; import { IconColor } from '../../../types'; import type { GitifySubject, @@ -80,6 +80,10 @@ export class DefaultHandler implements NotificationTypeHandler { } return title; } + + defaultUrl(notification: Notification): Link { + return notification.repository.html_url as Link; + } } export const defaultHandler = new DefaultHandler(); diff --git a/src/renderer/utils/notifications/handlers/discussion.ts b/src/renderer/utils/notifications/handlers/discussion.ts index aa269876f..0af30b8c3 100644 --- a/src/renderer/utils/notifications/handlers/discussion.ts +++ b/src/renderer/utils/notifications/handlers/discussion.ts @@ -10,7 +10,7 @@ import { import { differenceInMilliseconds } from 'date-fns'; -import type { SettingsState } from '../../../types'; +import type { Link, SettingsState } from '../../../types'; import type { DiscussionStateType, GitifySubject, @@ -111,6 +111,12 @@ class DiscussionHandler extends DefaultHandler { return CommentDiscussionIcon; } } + + defaultUrl(notification: Notification): Link { + const url = new URL(notification.repository.html_url); + url.pathname += '/discussions'; + return url.href as Link; + } } export const discussionHandler = new DiscussionHandler(); diff --git a/src/renderer/utils/notifications/handlers/issue.ts b/src/renderer/utils/notifications/handlers/issue.ts index ce2eda76f..5791b5d8d 100644 --- a/src/renderer/utils/notifications/handlers/issue.ts +++ b/src/renderer/utils/notifications/handlers/issue.ts @@ -9,7 +9,7 @@ import { SkipIcon, } from '@primer/octicons-react'; -import type { SettingsState } from '../../../types'; +import type { Link, SettingsState } from '../../../types'; import type { GitifySubject, Notification, @@ -64,6 +64,12 @@ class IssueHandler extends DefaultHandler { return IssueOpenedIcon; } } + + defaultUrl(notification: Notification): Link { + const url = new URL(notification.repository.html_url); + url.pathname += '/issues'; + return url.href as Link; + } } export const issueHandler = new IssueHandler(); diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index b98b4699d..d5b2a0b6f 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -8,7 +8,7 @@ import { GitPullRequestIcon, } from '@primer/octicons-react'; -import type { SettingsState } from '../../../types'; +import type { Link, SettingsState } from '../../../types'; import type { GitifyPullRequestReview, GitifySubject, @@ -81,6 +81,12 @@ class PullRequestHandler extends DefaultHandler { return GitPullRequestIcon; } } + + defaultUrl(notification: Notification): Link { + const url = new URL(notification.repository.html_url); + url.pathname += '/pulls'; + return url.href as Link; + } } export const pullRequestHandler = new PullRequestHandler(); diff --git a/src/renderer/utils/notifications/handlers/release.ts b/src/renderer/utils/notifications/handlers/release.ts index 9caadc1a7..8a3190588 100644 --- a/src/renderer/utils/notifications/handlers/release.ts +++ b/src/renderer/utils/notifications/handlers/release.ts @@ -3,7 +3,7 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; import { TagIcon } from '@primer/octicons-react'; -import type { SettingsState } from '../../../types'; +import type { Link, SettingsState } from '../../../types'; import type { GitifySubject, Notification, @@ -42,6 +42,12 @@ class ReleaseHandler extends DefaultHandler { iconType(_subject: Subject): FC | null { return TagIcon; } + + defaultUrl(notification: Notification): Link { + const url = new URL(notification.repository.html_url); + url.pathname += '/releases'; + return url.href as Link; + } } export const releaseHandler = new ReleaseHandler(); diff --git a/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.ts b/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.ts index cd06771be..c71747d48 100644 --- a/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.ts +++ b/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.ts @@ -3,7 +3,8 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; import { AlertIcon } from '@primer/octicons-react'; -import type { Subject } from '../../../typesGitHub'; +import type { Link } from '../../../types'; +import type { Notification, Subject } from '../../../typesGitHub'; import { DefaultHandler } from './default'; class RepositoryDependabotAlertsThreadHandler extends DefaultHandler { @@ -12,6 +13,12 @@ class RepositoryDependabotAlertsThreadHandler extends DefaultHandler { iconType(_subject: Subject): FC | null { return AlertIcon; } + + defaultUrl(notification: Notification): Link { + const url = new URL(notification.repository.html_url); + url.pathname += '/security/dependabot'; + return url.href as Link; + } } export const repositoryDependabotAlertsThreadHandler = diff --git a/src/renderer/utils/notifications/handlers/repositoryInvitation.ts b/src/renderer/utils/notifications/handlers/repositoryInvitation.ts index 38bf30470..a5beaea28 100644 --- a/src/renderer/utils/notifications/handlers/repositoryInvitation.ts +++ b/src/renderer/utils/notifications/handlers/repositoryInvitation.ts @@ -2,7 +2,8 @@ import type { FC } from 'react'; import { MailIcon, type OcticonProps } from '@primer/octicons-react'; -import type { Subject } from '../../../typesGitHub'; +import type { Link } from '../../../types'; +import type { Notification, Subject } from '../../../typesGitHub'; import { DefaultHandler } from './default'; class RepositoryInvitationHandler extends DefaultHandler { @@ -11,6 +12,12 @@ class RepositoryInvitationHandler extends DefaultHandler { iconType(_subject: Subject): FC | null { return MailIcon; } + + defaultUrl(notification: Notification): Link { + const url = new URL(notification.repository.html_url); + url.pathname += '/invitations'; + return url.href as Link; + } } export const repositoryInvitationHandler = new RepositoryInvitationHandler(); diff --git a/src/renderer/utils/notifications/handlers/types.ts b/src/renderer/utils/notifications/handlers/types.ts index 686e3ef50..36a52fbd6 100644 --- a/src/renderer/utils/notifications/handlers/types.ts +++ b/src/renderer/utils/notifications/handlers/types.ts @@ -2,7 +2,7 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; -import type { SettingsState } from '../../../types'; +import type { Link, SettingsState } from '../../../types'; import type { GitifySubject, Notification, @@ -45,4 +45,9 @@ export interface NotificationTypeHandler { * Return the formatted notification title for this notification. */ formattedNotificationTitle(notification: Notification): string; + + /** + * Default url for notification type. + */ + defaultUrl(notification: Notification): Link; } diff --git a/src/renderer/utils/notifications/handlers/workflowRun.ts b/src/renderer/utils/notifications/handlers/workflowRun.ts index 3bfe40742..bafd7f312 100644 --- a/src/renderer/utils/notifications/handlers/workflowRun.ts +++ b/src/renderer/utils/notifications/handlers/workflowRun.ts @@ -37,6 +37,12 @@ class WorkflowRunHandler extends DefaultHandler { iconType(_subject: Subject): FC | null { return RocketIcon; } + + defaultUrl(notification: Notification): Link { + const url = new URL(notification.repository.html_url); + url.pathname += '/actions'; + return url.href as Link; + } } export const workflowRunHandler = new WorkflowRunHandler(); From fc37b54625e98a6f243a8420179c0e2f3e29b5ff Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 07:25:36 +1000 Subject: [PATCH 05/25] refactor(api): use graphql api for issue and pull request enrichment Signed-off-by: Adam Setch --- src/renderer/typesGitHub.ts | 21 +------ .../utils/api/__mocks__/response-mocks.ts | 11 ++-- .../utils/api/graphql/generated/gql.ts | 6 +- .../utils/api/graphql/generated/graphql.ts | 7 +-- src/renderer/utils/api/graphql/pull.graphql | 5 +- src/renderer/utils/icons.test.ts | 26 ++++---- src/renderer/utils/links.ts | 15 +---- .../notifications/handlers/discussion.ts | 27 ++------ .../utils/notifications/handlers/issue.ts | 26 +++++--- .../handlers/pullRequest.test.ts | 63 ++++++++----------- .../notifications/handlers/pullRequest.ts | 33 +++++----- 11 files changed, 96 insertions(+), 144 deletions(-) diff --git a/src/renderer/typesGitHub.ts b/src/renderer/typesGitHub.ts index 83e581aab..d3a2c9c42 100644 --- a/src/renderer/typesGitHub.ts +++ b/src/renderer/typesGitHub.ts @@ -4,6 +4,7 @@ import type { IssueState, IssueStateReason, MilestoneFieldsFragment, + PullRequestReviewState, PullRequestState, } from './utils/api/graphql/generated/graphql'; @@ -87,13 +88,6 @@ export type CheckSuiteStatus = | 'timed_out' | 'waiting'; -export type PullRequestReviewState = - | 'APPROVED' - | 'CHANGES_REQUESTED' - | 'COMMENTED' - | 'DISMISSED' - | 'PENDING'; - export type PullRequestReviewAuthorAssociation = | 'COLLABORATOR' | 'CONTRIBUTOR' @@ -278,19 +272,6 @@ export interface GitifyPullRequestReview { users: string[]; } -export interface PullRequestReview { - id: number; - node_id: string; - user: User; - body: string; - state: PullRequestReviewState; - html_url: Link; - pull_request_url: Link; - author_association: PullRequestReviewAuthorAssociation; - submitted_at: string; - commit_id: string; -} - export interface Commit { sha: string; node_id: string; diff --git a/src/renderer/utils/api/__mocks__/response-mocks.ts b/src/renderer/utils/api/__mocks__/response-mocks.ts index 450447b60..49f78e954 100644 --- a/src/renderer/utils/api/__mocks__/response-mocks.ts +++ b/src/renderer/utils/api/__mocks__/response-mocks.ts @@ -4,7 +4,10 @@ import { } from '../../../__mocks__/account-mocks'; import type { Link } from '../../../types'; import type { Notification, Repository, User } from '../../../typesGitHub'; -import type { FetchDiscussionByNumberQuery } from '../graphql/generated/graphql'; +import { + type FetchDiscussionByNumberQuery, + PullRequestReviewState, +} from '../graphql/generated/graphql'; export const mockNotificationUser: User = { login: 'octocat', @@ -59,15 +62,15 @@ export const mockGitHubNotifications: Notification[] = [ }, reviews: [ { - state: 'APPROVED', + state: PullRequestReviewState.Approved, users: ['octocat'], }, { - state: 'CHANGES_REQUESTED', + state: PullRequestReviewState.ChangesRequested, users: ['gitify-app'], }, { - state: 'PENDING', + state: PullRequestReviewState.Pending, users: ['gitify-user'], }, ], diff --git a/src/renderer/utils/api/graphql/generated/gql.ts b/src/renderer/utils/api/graphql/generated/gql.ts index 42467e918..71eedbd4e 100644 --- a/src/renderer/utils/api/graphql/generated/gql.ts +++ b/src/renderer/utils/api/graphql/generated/gql.ts @@ -18,13 +18,13 @@ type Documents = { "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}": typeof types.AuthorFieldsFragmentDoc, "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}": typeof types.FetchDiscussionByNumberDocument, "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchIssueByNumberDocument, - "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 1) {\n totalCount\n nodes {\n url\n createdAt\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchPullByNumberDocument, + "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 100) {\n totalCount\n nodes {\n state\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchPullByNumberDocument, }; const documents: Documents = { "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}": types.AuthorFieldsFragmentDoc, "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}": types.FetchDiscussionByNumberDocument, "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchIssueByNumberDocument, - "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 1) {\n totalCount\n nodes {\n url\n createdAt\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchPullByNumberDocument, + "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 100) {\n totalCount\n nodes {\n state\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchPullByNumberDocument, }; /** @@ -42,7 +42,7 @@ export function graphql(source: "query FetchIssueByNumber($owner: String!, $name /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 1) {\n totalCount\n nodes {\n url\n createdAt\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').FetchPullByNumberDocument; +export function graphql(source: "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 100) {\n totalCount\n nodes {\n state\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').FetchPullByNumberDocument; export function graphql(source: string) { diff --git a/src/renderer/utils/api/graphql/generated/graphql.ts b/src/renderer/utils/api/graphql/generated/graphql.ts index e65b3a522..eef58011c 100644 --- a/src/renderer/utils/api/graphql/generated/graphql.ts +++ b/src/renderer/utils/api/graphql/generated/graphql.ts @@ -36269,7 +36269,7 @@ export type FetchPullByNumberQuery = { __typename?: 'Query', repository?: { __ty | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } - | null } | null> | null }, reviews?: { __typename?: 'PullRequestReviewConnection', totalCount: number, nodes?: Array<{ __typename?: 'PullRequestReview', url: any, createdAt: any, author?: + | null } | null> | null }, reviews?: { __typename?: 'PullRequestReviewConnection', totalCount: number, nodes?: Array<{ __typename?: 'PullRequestReview', state: PullRequestReviewState, author?: | { __typename?: 'Bot', login: string } | { __typename?: 'EnterpriseUserAccount', login: string } | { __typename?: 'Mannequin', login: string } @@ -36440,11 +36440,10 @@ export const FetchPullByNumberDocument = new TypedDocumentString(` } } } - reviews(last: 1) { + reviews(last: 100) { totalCount nodes { - url - createdAt + state author { login } diff --git a/src/renderer/utils/api/graphql/pull.graphql b/src/renderer/utils/api/graphql/pull.graphql index e42c67980..acb0042b1 100644 --- a/src/renderer/utils/api/graphql/pull.graphql +++ b/src/renderer/utils/api/graphql/pull.graphql @@ -29,11 +29,10 @@ query FetchPullByNumber( } } } - reviews(last: 1) { + reviews(last: 100) { totalCount nodes { - url - createdAt + state author { login } diff --git a/src/renderer/utils/icons.test.ts b/src/renderer/utils/icons.test.ts index 1ca83f48e..c69905c9b 100644 --- a/src/renderer/utils/icons.test.ts +++ b/src/renderer/utils/icons.test.ts @@ -9,6 +9,7 @@ import { import { IconColor } from '../types'; import type { GitifyPullRequestReview } from '../typesGitHub'; +import { PullRequestReviewState } from './api/graphql/generated/graphql'; import { getAuthMethodIcon, getDefaultUserIcon, @@ -23,18 +24,18 @@ describe('renderer/utils/icons.ts', () => { beforeEach(() => { mockReviewSingleReviewer = { - state: 'APPROVED', + state: PullRequestReviewState.Approved, users: ['user1'], }; mockReviewMultipleReviewer = { - state: 'APPROVED', + state: PullRequestReviewState.Approved, users: ['user1', 'user2'], }; }); it('approved', () => { - mockReviewSingleReviewer.state = 'APPROVED'; - mockReviewMultipleReviewer.state = 'APPROVED'; + mockReviewSingleReviewer.state = PullRequestReviewState.Approved; + mockReviewMultipleReviewer.state = PullRequestReviewState.Approved; expect(getPullRequestReviewIcon(mockReviewSingleReviewer)).toEqual({ type: CheckIcon, @@ -50,8 +51,9 @@ describe('renderer/utils/icons.ts', () => { }); it('changes requested', () => { - mockReviewSingleReviewer.state = 'CHANGES_REQUESTED'; - mockReviewMultipleReviewer.state = 'CHANGES_REQUESTED'; + mockReviewSingleReviewer.state = PullRequestReviewState.ChangesRequested; + mockReviewMultipleReviewer.state = + PullRequestReviewState.ChangesRequested; expect(getPullRequestReviewIcon(mockReviewSingleReviewer)).toEqual({ type: FileDiffIcon, @@ -67,8 +69,8 @@ describe('renderer/utils/icons.ts', () => { }); it('commented', () => { - mockReviewSingleReviewer.state = 'COMMENTED'; - mockReviewMultipleReviewer.state = 'COMMENTED'; + mockReviewSingleReviewer.state = PullRequestReviewState.Commented; + mockReviewMultipleReviewer.state = PullRequestReviewState.Commented; expect(getPullRequestReviewIcon(mockReviewSingleReviewer)).toEqual({ type: CommentIcon, @@ -84,8 +86,8 @@ describe('renderer/utils/icons.ts', () => { }); it('dismissed', () => { - mockReviewSingleReviewer.state = 'DISMISSED'; - mockReviewMultipleReviewer.state = 'DISMISSED'; + mockReviewSingleReviewer.state = PullRequestReviewState.Dismissed; + mockReviewMultipleReviewer.state = PullRequestReviewState.Dismissed; expect(getPullRequestReviewIcon(mockReviewSingleReviewer)).toEqual({ type: CommentIcon, @@ -101,8 +103,8 @@ describe('renderer/utils/icons.ts', () => { }); it('pending', () => { - mockReviewSingleReviewer.state = 'PENDING'; - mockReviewMultipleReviewer.state = 'PENDING'; + mockReviewSingleReviewer.state = PullRequestReviewState.Pending; + mockReviewMultipleReviewer.state = PullRequestReviewState.Pending; expect(getPullRequestReviewIcon(mockReviewSingleReviewer)).toBeNull(); diff --git a/src/renderer/utils/links.ts b/src/renderer/utils/links.ts index 32803f4c6..88ce44109 100644 --- a/src/renderer/utils/links.ts +++ b/src/renderer/utils/links.ts @@ -5,10 +5,7 @@ import type { Account, Hostname, Link } from '../types'; import type { Notification, Repository, SubjectUser } from '../typesGitHub'; import { getDeveloperSettingsURL } from './auth/utils'; import { openExternalLink } from './comms'; -import { - generateGitHubWebUrl, - generateNotificationReferrerId, -} from './helpers'; +import { generateGitHubWebUrl } from './helpers'; export function openGitifyReleaseNotes(version: string) { openExternalLink( @@ -58,14 +55,8 @@ export function openRepository(repository: Repository) { } export async function openNotification(notification: Notification) { - const url = new URL(await generateGitHubWebUrl(notification)); - - url.searchParams.set( - 'notification_referrer_id', - generateNotificationReferrerId(notification), - ); - - openExternalLink(url.toString() as Link); + const url = await generateGitHubWebUrl(notification); + openExternalLink(url); } export function openGitHubParticipatingDocs() { diff --git a/src/renderer/utils/notifications/handlers/discussion.ts b/src/renderer/utils/notifications/handlers/discussion.ts index 0af30b8c3..afd3d8702 100644 --- a/src/renderer/utils/notifications/handlers/discussion.ts +++ b/src/renderer/utils/notifications/handlers/discussion.ts @@ -16,7 +16,6 @@ import type { GitifySubject, Notification, Subject, - SubjectUser, } from '../../../typesGitHub'; import { fetchDiscussionByNumber } from '../../api/client'; import type { @@ -25,6 +24,7 @@ import type { } from '../../api/graphql/generated/graphql'; import { isStateFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; +import { getSubjectAuthor } from './utils'; type DiscussionComment = NonNullable< NonNullable< @@ -57,9 +57,7 @@ class DiscussionHandler extends DefaultHandler { if (discussion.isAnswered) { discussionState = 'ANSWERED'; - } - - if (discussion.stateReason) { + } else if (discussion.stateReason) { discussionState = discussion.stateReason; } @@ -73,26 +71,13 @@ class DiscussionHandler extends DefaultHandler { discussion.comments.nodes, ); - let discussionUser: SubjectUser = { - login: discussion.author.login, - html_url: discussion.author.html_url, - avatar_url: discussion.author.avatar_url, - type: discussion.author.type, - }; - - if (latestDiscussionComment) { - discussionUser = { - login: latestDiscussionComment.author.login, - html_url: latestDiscussionComment.author.html_url, - avatar_url: latestDiscussionComment.author.avatar_url, - type: latestDiscussionComment.author.type, - }; - } - return { number: discussion.number, state: discussionState, - user: discussionUser, + user: getSubjectAuthor([ + latestDiscussionComment.author, + discussion.author, + ]), comments: discussion.comments.totalCount, labels: discussion.labels?.nodes.map((label) => label.name) ?? [], htmlUrl: latestDiscussionComment.url ?? discussion.url, diff --git a/src/renderer/utils/notifications/handlers/issue.ts b/src/renderer/utils/notifications/handlers/issue.ts index 5791b5d8d..19df0b5ca 100644 --- a/src/renderer/utils/notifications/handlers/issue.ts +++ b/src/renderer/utils/notifications/handlers/issue.ts @@ -16,32 +16,40 @@ import type { Subject, } from '../../../typesGitHub'; import { fetchIssueByNumber } from '../../api/client'; +import { isStateFilteredOut, isUserFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; +import { getSubjectAuthor } from './utils'; class IssueHandler extends DefaultHandler { readonly type = 'Issue'; async enrich( notification: Notification, - _settings: SettingsState, + settings: SettingsState, ): Promise { const response = await fetchIssueByNumber(notification); const issue = response.data.repository?.issue; - // const issueState = issue.stateReason ?? issue.state; + const issueState = issue.stateReason ?? issue.state; // Return early if this notification would be hidden by filters - // if (isStateFilteredOut(issueState, settings)) { - // return null; - // } + if (isStateFilteredOut(issueState, settings)) { + return null; + } + + const issueCommentUser = issue.comments.nodes[0].author; - // const issueCommentUser = issue.comments.nodes[0]?.author; + const issueUser = getSubjectAuthor([issueCommentUser, issue.author]); + + // Return early if this notification would be hidden by user filters + if (isUserFilteredOut(issueUser, settings)) { + return null; + } return { number: issue.number, - // state: issueState - state: null, - user: null, //getSubjectUser([issueCommentUser, issue.author]), + state: issueState, + user: issueUser, comments: issue.comments.totalCount, labels: issue.labels.nodes?.map((label) => label.name) ?? [], milestone: issue.milestone, diff --git a/src/renderer/utils/notifications/handlers/pullRequest.test.ts b/src/renderer/utils/notifications/handlers/pullRequest.test.ts index 9f5d8ce91..6e22377e5 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.test.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.test.ts @@ -9,6 +9,7 @@ import { mockSettings } from '../../../__mocks__/state-mocks'; import { createPartialMockUser } from '../../../__mocks__/user-mocks'; import type { Link } from '../../../types'; import type { Notification } from '../../../typesGitHub'; +import { PullRequestReviewState } from '../../api/graphql/generated/graphql'; import { getLatestReviewForReviewers, pullRequestHandler } from './pullRequest'; describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { @@ -434,36 +435,34 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { describe('Pull Request Reviews - Latest Reviews By Reviewer', () => { it('returns latest review state per reviewer', async () => { - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1/reviews') - .reply(200, [ - { - user: { - login: 'reviewer-1', - }, - state: 'REQUESTED_CHANGES', + const mockReviews = [ + { + author: { + login: 'reviewer-1', }, - { - user: { - login: 'reviewer-2', - }, - state: 'COMMENTED', + state: PullRequestReviewState.ChangesRequested, + }, + { + author: { + login: 'reviewer-2', }, - { - user: { - login: 'reviewer-1', - }, - state: 'APPROVED', + state: PullRequestReviewState.Commented, + }, + { + author: { + login: 'reviewer-1', }, - { - user: { - login: 'reviewer-3', - }, - state: 'APPROVED', + state: PullRequestReviewState.Approved, + }, + { + author: { + login: 'reviewer-3', }, - ]); + state: PullRequestReviewState.Approved, + }, + ]; - const result = await getLatestReviewForReviewers(mockNotification); + const result = getLatestReviewForReviewers(mockReviews); expect(result).toEqual([ { state: 'APPROVED', users: ['reviewer-3', 'reviewer-1'] }, @@ -472,19 +471,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { }); it('handles no PR reviews yet', async () => { - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1/reviews') - .reply(200, []); - - const result = await getLatestReviewForReviewers(mockNotification); - - expect(result).toBeNull(); - }); - - it('returns null when not a PR notification', async () => { - mockNotification.subject.type = 'Issue'; - - const result = await getLatestReviewForReviewers(mockNotification); + const result = getLatestReviewForReviewers([]); expect(result).toBeNull(); }); diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index d5b2a0b6f..1765b8683 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -13,12 +13,14 @@ import type { GitifyPullRequestReview, GitifySubject, Notification, - PullRequestReview, PullRequestStateType, Subject, } from '../../../typesGitHub'; import { fetchPullByNumber } from '../../api/client'; -import type { PullRequestState } from '../../api/graphql/generated/graphql'; +import type { + FetchPullByNumberQuery, + PullRequestState, +} from '../../api/graphql/generated/graphql'; import { isStateFilteredOut, isUserFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; import { getSubjectAuthor } from './utils'; @@ -52,7 +54,7 @@ class PullRequestHandler extends DefaultHandler { return null; } - const reviews = null; // await getLatestReviewForReviewers(notification); + const reviews = getLatestReviewForReviewers(pr.reviews.nodes); return { number: pr.number, @@ -91,25 +93,20 @@ class PullRequestHandler extends DefaultHandler { export const pullRequestHandler = new PullRequestHandler(); -export async function getLatestReviewForReviewers( - notification: Notification, -): Promise | null { - if (notification.subject.type !== 'PullRequest') { - return null; - } - - const prReviews = null; - - if (!prReviews.data.length) { +export function getLatestReviewForReviewers( + reviews: FetchPullByNumberQuery['repository']['pullRequest']['reviews']['nodes'], +): GitifyPullRequestReview[] { + if (!reviews.length) { return null; } // Find the most recent review for each reviewer - const latestReviews: PullRequestReview[] = []; - const sortedReviews = prReviews.data.slice().reverse(); + const latestReviews = []; + const sortedReviews = reviews.reverse(); for (const prReview of sortedReviews) { const reviewerFound = latestReviews.find( - (review) => review.user.login === prReview.user.login, + (review) => review.author.login === prReview.author.login, + prReview.state, ); if (!reviewerFound) { @@ -125,11 +122,11 @@ export async function getLatestReviewForReviewers( ); if (reviewerFound) { - reviewerFound.users.push(prReview.user.login); + reviewerFound.users.push(prReview.author.login); } else { reviewers.push({ state: prReview.state, - users: [prReview.user.login], + users: [prReview.author.login], }); } } From 5a198b79fa6ea9dec3d20f0eb672e2df12c17553 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 07:38:39 +1000 Subject: [PATCH 06/25] refactor(api): use graphql api for issue and pull request enrichment Signed-off-by: Adam Setch --- src/renderer/typesGitHub.ts | 16 ---------- .../utils/notifications/filters/state.ts | 29 +++++++++++++++++-- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/renderer/typesGitHub.ts b/src/renderer/typesGitHub.ts index d3a2c9c42..bd95bd030 100644 --- a/src/renderer/typesGitHub.ts +++ b/src/renderer/typesGitHub.ts @@ -46,10 +46,6 @@ export type SubjectType = | 'RepositoryVulnerabilityAlert' | 'WorkflowRun'; -export type IssueStateType = 'closed' | 'open'; - -export type IssueStateReasonType = 'completed' | 'not_planned' | 'reopened'; - export type UserType = | 'Bot' | 'EnterpriseUserAccount' @@ -66,8 +62,6 @@ export type PullRequestStateType = 'closed' | 'draft' | 'merged' | 'open'; export type StateType = | CheckSuiteStatus | DiscussionStateType - | IssueStateType - | IssueStateReasonType | PullRequestStateType | IssueState | IssueStateReason @@ -88,16 +82,6 @@ export type CheckSuiteStatus = | 'timed_out' | 'waiting'; -export type PullRequestReviewAuthorAssociation = - | 'COLLABORATOR' - | 'CONTRIBUTOR' - | 'FIRST_TIMER' - | 'FIRST_TIME_CONTRIBUTOR' - | 'MANNEQUIN' - | 'MEMBER' - | 'NONE' - | 'OWNER'; - // TODO: #828 Add explicit types for GitHub API response vs Gitify Notifications object export type Notification = GitHubNotification & GitifyNotification; diff --git a/src/renderer/utils/notifications/filters/state.ts b/src/renderer/utils/notifications/filters/state.ts index 9ed75f98a..fbb348cbf 100644 --- a/src/renderer/utils/notifications/filters/state.ts +++ b/src/renderer/utils/notifications/filters/state.ts @@ -5,6 +5,13 @@ import type { TypeDetails, } from '../../../types'; import type { Notification } from '../../../typesGitHub'; +import { + DiscussionCloseReason, + DiscussionState, + IssueState, + IssueStateReason, + PullRequestState, +} from '../../api/graphql/generated/graphql'; import type { Filter } from './types'; const STATE_TYPE_DETAILS: Record = { @@ -63,9 +70,25 @@ export const stateFilter: Filter = { notification: Notification, stateType: FilterStateType, ): boolean { - const allOpenStates = ['open', 'reopened']; - const allClosedStates = ['closed', 'completed', 'not_planned']; - const allMergedStates = ['merged']; + const allOpenStates = [ + DiscussionState.Open, + IssueState.Open, + IssueStateReason.Reopened, + PullRequestState.Open, + 'open', + 'reopened', + ]; + const allClosedStates = [ + DiscussionState.Closed, + IssueState.Closed, + IssueStateReason.NotPlanned, + PullRequestState.Closed, + + 'closed', + 'completed', + 'not_planned', + ]; + const allMergedStates = [PullRequestState.Merged, 'merged']; const allDraftStates = ['draft']; const allFilterableStates = [ ...allOpenStates, From 8f3099d8f9ff8e0179b6ffd5f876c781c4f6b080 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 07:54:30 +1000 Subject: [PATCH 07/25] refactor(api): use graphql api for issue and pull request enrichment Signed-off-by: Adam Setch --- .../utils/notifications/handlers/issue.ts | 20 +++++++++++-------- .../notifications/handlers/pullRequest.ts | 10 +++++++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/renderer/utils/notifications/handlers/issue.ts b/src/renderer/utils/notifications/handlers/issue.ts index 19df0b5ca..f63fe5cb2 100644 --- a/src/renderer/utils/notifications/handlers/issue.ts +++ b/src/renderer/utils/notifications/handlers/issue.ts @@ -3,7 +3,6 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; import { IssueClosedIcon, - IssueDraftIcon, IssueOpenedIcon, IssueReopenedIcon, SkipIcon, @@ -16,6 +15,10 @@ import type { Subject, } from '../../../typesGitHub'; import { fetchIssueByNumber } from '../../api/client'; +import { + IssueState, + IssueStateReason, +} from '../../api/graphql/generated/graphql'; import { isStateFilteredOut, isUserFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; import { getSubjectAuthor } from './utils'; @@ -58,15 +61,16 @@ class IssueHandler extends DefaultHandler { } iconType(subject: Subject): FC | null { - switch (subject.state) { - case 'draft': - return IssueDraftIcon; - case 'closed': - case 'completed': + const state = subject.state as IssueState | IssueStateReason; + + switch (state) { + case IssueState.Closed: + case IssueStateReason.Completed: return IssueClosedIcon; - case 'not_planned': + case IssueStateReason.Duplicate: + case IssueStateReason.NotPlanned: return SkipIcon; - case 'reopened': + case IssueStateReason.Reopened: return IssueReopenedIcon; default: return IssueOpenedIcon; diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index 1765b8683..4b9d2c765 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -17,8 +17,8 @@ import type { Subject, } from '../../../typesGitHub'; import { fetchPullByNumber } from '../../api/client'; -import type { - FetchPullByNumberQuery, +import { + type FetchPullByNumberQuery, PullRequestState, } from '../../api/graphql/generated/graphql'; import { isStateFilteredOut, isUserFilteredOut } from '../filters/filter'; @@ -72,11 +72,15 @@ class PullRequestHandler extends DefaultHandler { } iconType(subject: Subject): FC | null { - switch (subject.state) { + const state = subject.state as PullRequestStateType | PullRequestState; + + switch (state) { case 'draft': return GitPullRequestDraftIcon; + case PullRequestState.Closed: case 'closed': return GitPullRequestClosedIcon; + case PullRequestState.Merged: case 'merged': return GitMergeIcon; default: From f2d61988bdaa29d12a22a238e090d2df28257d96 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 11:53:01 +1000 Subject: [PATCH 08/25] refactor(api): update types Signed-off-by: Adam Setch --- codegen.ts | 1 + .../components/metrics/MetricGroup.test.tsx | 9 +- .../components/metrics/MetricGroup.tsx | 3 +- src/renderer/typesGitHub.ts | 65 +- .../utils/api/__mocks__/response-mocks.ts | 13 +- .../utils/api/graphql/generated/graphql.ts | 3226 ++++++++--------- src/renderer/utils/icons.test.ts | 26 +- .../notifications/filters/filter.test.ts | 4 +- .../utils/notifications/filters/state.test.ts | 16 +- .../utils/notifications/filters/state.ts | 33 +- .../notifications/handlers/checkSuite.test.ts | 8 +- .../notifications/handlers/checkSuite.ts | 33 +- .../notifications/handlers/default.test.ts | 10 +- .../utils/notifications/handlers/default.ts | 26 +- .../notifications/handlers/discussion.test.ts | 26 +- .../notifications/handlers/discussion.ts | 4 +- .../notifications/handlers/issue.test.ts | 10 +- .../utils/notifications/handlers/issue.ts | 19 +- .../handlers/pullRequest.test.ts | 16 +- .../notifications/handlers/pullRequest.ts | 23 +- .../notifications/handlers/workflowRun.ts | 15 +- 21 files changed, 1621 insertions(+), 1965 deletions(-) diff --git a/codegen.ts b/codegen.ts index 40318fb4b..61874aa4d 100644 --- a/codegen.ts +++ b/codegen.ts @@ -22,6 +22,7 @@ const config: CodegenConfig = { }, config: { documentMode: 'string', + enumsAsTypes: true, useTypeImports: true, }, }, diff --git a/src/renderer/components/metrics/MetricGroup.test.tsx b/src/renderer/components/metrics/MetricGroup.test.tsx index 7c738570e..51089ca65 100644 --- a/src/renderer/components/metrics/MetricGroup.test.tsx +++ b/src/renderer/components/metrics/MetricGroup.test.tsx @@ -1,10 +1,7 @@ import { renderWithAppContext } from '../../__helpers__/test-utils'; import { mockSettings } from '../../__mocks__/state-mocks'; import { mockSingleNotification } from '../../utils/api/__mocks__/response-mocks'; -import { - type MilestoneFieldsFragment, - MilestoneState, -} from '../../utils/api/graphql/generated/graphql'; +import type { MilestoneFieldsFragment } from '../../utils/api/graphql/generated/graphql'; import { MetricGroup } from './MetricGroup'; describe('renderer/components/metrics/MetricGroup.tsx', () => { @@ -106,7 +103,7 @@ describe('renderer/components/metrics/MetricGroup.tsx', () => { const mockNotification = mockSingleNotification; mockNotification.subject.milestone = { title: 'Milestone 1', - state: MilestoneState.Open, + state: 'OPEN', } as MilestoneFieldsFragment; const props = { @@ -121,7 +118,7 @@ describe('renderer/components/metrics/MetricGroup.tsx', () => { const mockNotification = mockSingleNotification; mockNotification.subject.milestone = { title: 'Milestone 1', - state: MilestoneState.Closed, + state: 'CLOSED', } as MilestoneFieldsFragment; const props = { diff --git a/src/renderer/components/metrics/MetricGroup.tsx b/src/renderer/components/metrics/MetricGroup.tsx index 6afc50d7a..fbe14cf7b 100644 --- a/src/renderer/components/metrics/MetricGroup.tsx +++ b/src/renderer/components/metrics/MetricGroup.tsx @@ -10,7 +10,6 @@ import { import { AppContext } from '../../context/App'; import { IconColor } from '../../types'; import type { Notification } from '../../typesGitHub'; -import { MilestoneState } from '../../utils/api/graphql/generated/graphql'; import { getPullRequestReviewIcon } from '../../utils/icons'; import { MetricPill } from './MetricPill'; @@ -85,7 +84,7 @@ export const MetricGroup: FC = ({ {notification.subject.milestone && ( { beforeEach(() => { mockReviewSingleReviewer = { - state: PullRequestReviewState.Approved, + state: 'APPROVED', users: ['user1'], }; mockReviewMultipleReviewer = { - state: PullRequestReviewState.Approved, + state: 'APPROVED', users: ['user1', 'user2'], }; }); it('approved', () => { - mockReviewSingleReviewer.state = PullRequestReviewState.Approved; - mockReviewMultipleReviewer.state = PullRequestReviewState.Approved; + mockReviewSingleReviewer.state = 'APPROVED'; + mockReviewMultipleReviewer.state = 'APPROVED'; expect(getPullRequestReviewIcon(mockReviewSingleReviewer)).toEqual({ type: CheckIcon, @@ -51,9 +50,8 @@ describe('renderer/utils/icons.ts', () => { }); it('changes requested', () => { - mockReviewSingleReviewer.state = PullRequestReviewState.ChangesRequested; - mockReviewMultipleReviewer.state = - PullRequestReviewState.ChangesRequested; + mockReviewSingleReviewer.state = 'CHANGES_REQUESTED'; + mockReviewMultipleReviewer.state = 'CHANGES_REQUESTED'; expect(getPullRequestReviewIcon(mockReviewSingleReviewer)).toEqual({ type: FileDiffIcon, @@ -69,8 +67,8 @@ describe('renderer/utils/icons.ts', () => { }); it('commented', () => { - mockReviewSingleReviewer.state = PullRequestReviewState.Commented; - mockReviewMultipleReviewer.state = PullRequestReviewState.Commented; + mockReviewSingleReviewer.state = 'COMMENTED'; + mockReviewMultipleReviewer.state = 'COMMENTED'; expect(getPullRequestReviewIcon(mockReviewSingleReviewer)).toEqual({ type: CommentIcon, @@ -86,8 +84,8 @@ describe('renderer/utils/icons.ts', () => { }); it('dismissed', () => { - mockReviewSingleReviewer.state = PullRequestReviewState.Dismissed; - mockReviewMultipleReviewer.state = PullRequestReviewState.Dismissed; + mockReviewSingleReviewer.state = 'DISMISSED'; + mockReviewMultipleReviewer.state = 'DISMISSED'; expect(getPullRequestReviewIcon(mockReviewSingleReviewer)).toEqual({ type: CommentIcon, @@ -103,8 +101,8 @@ describe('renderer/utils/icons.ts', () => { }); it('pending', () => { - mockReviewSingleReviewer.state = PullRequestReviewState.Pending; - mockReviewMultipleReviewer.state = PullRequestReviewState.Pending; + mockReviewSingleReviewer.state = 'PENDING'; + mockReviewMultipleReviewer.state = 'PENDING'; expect(getPullRequestReviewIcon(mockReviewSingleReviewer)).toBeNull(); diff --git a/src/renderer/utils/notifications/filters/filter.test.ts b/src/renderer/utils/notifications/filters/filter.test.ts index 95f334869..d0610f162 100644 --- a/src/renderer/utils/notifications/filters/filter.test.ts +++ b/src/renderer/utils/notifications/filters/filter.test.ts @@ -169,8 +169,8 @@ describe('renderer/utils/notifications/filters/filter.ts', () => { }); it('should filter notifications by state when provided', async () => { - mockNotifications[0].subject.state = 'open'; - mockNotifications[1].subject.state = 'closed'; + mockNotifications[0].subject.state = 'OPEN'; + mockNotifications[1].subject.state = 'CLOSED'; const result = filterDetailedNotifications(mockNotifications, { ...mockSettings, filterStates: ['closed'], diff --git a/src/renderer/utils/notifications/filters/state.test.ts b/src/renderer/utils/notifications/filters/state.test.ts index 78fce244b..0aa631b2e 100644 --- a/src/renderer/utils/notifications/filters/state.test.ts +++ b/src/renderer/utils/notifications/filters/state.test.ts @@ -9,45 +9,45 @@ describe('renderer/utils/notifications/filters/state.ts', () => { it('can filter by notification states', () => { const mockPartialNotification = { subject: { - state: 'open', + state: 'OPEN', }, } as Partial as Notification; // Open states - mockPartialNotification.subject.state = 'open'; + mockPartialNotification.subject.state = 'OPEN'; expect( stateFilter.filterNotification(mockPartialNotification, 'open'), ).toBe(true); - mockPartialNotification.subject.state = 'reopened'; + mockPartialNotification.subject.state = 'REOPENED'; expect( stateFilter.filterNotification(mockPartialNotification, 'open'), ).toBe(true); // Closed states - mockPartialNotification.subject.state = 'closed'; + mockPartialNotification.subject.state = 'CLOSED'; expect( stateFilter.filterNotification(mockPartialNotification, 'closed'), ).toBe(true); - mockPartialNotification.subject.state = 'completed'; + mockPartialNotification.subject.state = 'COMPLETED'; expect( stateFilter.filterNotification(mockPartialNotification, 'closed'), ).toBe(true); - mockPartialNotification.subject.state = 'not_planned'; + mockPartialNotification.subject.state = 'NOT_PLANNED'; expect( stateFilter.filterNotification(mockPartialNotification, 'closed'), ).toBe(true); // Merged states - mockPartialNotification.subject.state = 'merged'; + mockPartialNotification.subject.state = 'MERGED'; expect( stateFilter.filterNotification(mockPartialNotification, 'merged'), ).toBe(true); // Draft states - mockPartialNotification.subject.state = 'draft'; + mockPartialNotification.subject.state = 'DRAFT'; expect( stateFilter.filterNotification(mockPartialNotification, 'draft'), ).toBe(true); diff --git a/src/renderer/utils/notifications/filters/state.ts b/src/renderer/utils/notifications/filters/state.ts index fbb348cbf..c49dbbd85 100644 --- a/src/renderer/utils/notifications/filters/state.ts +++ b/src/renderer/utils/notifications/filters/state.ts @@ -4,14 +4,7 @@ import type { SettingsState, TypeDetails, } from '../../../types'; -import type { Notification } from '../../../typesGitHub'; -import { - DiscussionCloseReason, - DiscussionState, - IssueState, - IssueStateReason, - PullRequestState, -} from '../../api/graphql/generated/graphql'; +import type { Notification, StateType } from '../../../typesGitHub'; import type { Filter } from './types'; const STATE_TYPE_DETAILS: Record = { @@ -70,26 +63,10 @@ export const stateFilter: Filter = { notification: Notification, stateType: FilterStateType, ): boolean { - const allOpenStates = [ - DiscussionState.Open, - IssueState.Open, - IssueStateReason.Reopened, - PullRequestState.Open, - 'open', - 'reopened', - ]; - const allClosedStates = [ - DiscussionState.Closed, - IssueState.Closed, - IssueStateReason.NotPlanned, - PullRequestState.Closed, - - 'closed', - 'completed', - 'not_planned', - ]; - const allMergedStates = [PullRequestState.Merged, 'merged']; - const allDraftStates = ['draft']; + const allOpenStates: StateType[] = ['OPEN', 'REOPENED']; + const allClosedStates: StateType[] = ['CLOSED', 'COMPLETED', 'NOT_PLANNED']; + const allMergedStates: StateType[] = ['MERGED']; + const allDraftStates: StateType[] = ['DRAFT']; const allFilterableStates = [ ...allOpenStates, ...allClosedStates, diff --git a/src/renderer/utils/notifications/handlers/checkSuite.test.ts b/src/renderer/utils/notifications/handlers/checkSuite.test.ts index 8f8da018b..e22af969a 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.test.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.test.ts @@ -149,7 +149,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { checkSuiteHandler.iconType( createMockSubject({ type: 'CheckSuite', - state: 'cancelled', + state: 'CANCELLED', }), ).displayName, ).toBe('StopIcon'); @@ -158,7 +158,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { checkSuiteHandler.iconType( createMockSubject({ type: 'CheckSuite', - state: 'failure', + state: 'FAILURE', }), ).displayName, ).toBe('XIcon'); @@ -167,7 +167,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { checkSuiteHandler.iconType( createMockSubject({ type: 'CheckSuite', - state: 'skipped', + state: 'SKIPPED', }), ).displayName, ).toBe('SkipIcon'); @@ -176,7 +176,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { checkSuiteHandler.iconType( createMockSubject({ type: 'CheckSuite', - state: 'success', + state: 'SUCCESS', }), ).displayName, ).toBe('CheckIcon'); diff --git a/src/renderer/utils/notifications/handlers/checkSuite.ts b/src/renderer/utils/notifications/handlers/checkSuite.ts index 9deb6d171..eaa06d7f3 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.ts @@ -11,8 +11,7 @@ import { import type { Link, SettingsState } from '../../../types'; import type { - CheckSuiteAttributes, - CheckSuiteStatus, + GitifyCheckSuiteStatus, GitifySubject, Notification, Subject, @@ -20,6 +19,14 @@ import type { import { actionsURL } from '../../helpers'; import { DefaultHandler } from './default'; +export interface CheckSuiteAttributes { + workflowName: string; + attemptNumber?: number; + statusDisplayName: string; + status: GitifyCheckSuiteStatus | null; + branchName: string; +} + class CheckSuiteHandler extends DefaultHandler { readonly type = 'CheckSuite'; @@ -41,14 +48,14 @@ class CheckSuiteHandler extends DefaultHandler { } iconType(subject: Subject): FC | null { - switch (subject.state) { - case 'cancelled': + switch (subject.state as GitifyCheckSuiteStatus) { + case 'CANCELLED': return StopIcon; - case 'failure': + case 'FAILURE': return XIcon; - case 'skipped': + case 'SKIPPED': return SkipIcon; - case 'success': + case 'SUCCESS': return CheckIcon; default: return RocketIcon; @@ -92,17 +99,19 @@ export function getCheckSuiteAttributes( }; } -function getCheckSuiteStatus(statusDisplayName: string): CheckSuiteStatus { +function getCheckSuiteStatus( + statusDisplayName: string, +): GitifyCheckSuiteStatus { switch (statusDisplayName) { case 'cancelled': - return 'cancelled'; + return 'CANCELLED'; case 'failed': case 'failed at startup': - return 'failure'; + return 'FAILURE'; case 'skipped': - return 'skipped'; + return 'SKIPPED'; case 'succeeded': - return 'success'; + return 'SUCCESS'; default: return null; } diff --git a/src/renderer/utils/notifications/handlers/default.test.ts b/src/renderer/utils/notifications/handlers/default.test.ts index caebed99b..5f7439541 100644 --- a/src/renderer/utils/notifications/handlers/default.test.ts +++ b/src/renderer/utils/notifications/handlers/default.test.ts @@ -62,7 +62,7 @@ describe('renderer/utils/notifications/handlers/default.ts', () => { const notification = createPartialMockNotification({ title: 'Sample', type: 'PullRequest', - state: 'open', + state: 'OPEN', }); expect(defaultHandler.formattedNotificationType(notification)).toBe( @@ -88,7 +88,7 @@ describe('renderer/utils/notifications/handlers/default.ts', () => { const notification = createPartialMockNotification({ title: 'Sample', type: 'Issue', - state: 'open', + state: 'OPEN', }); notification.subject.number = 42; expect(defaultHandler.formattedNotificationNumber(notification)).toBe( @@ -100,7 +100,7 @@ describe('renderer/utils/notifications/handlers/default.ts', () => { const notification = createPartialMockNotification({ title: 'Sample', type: 'Issue', - state: 'open', + state: 'OPEN', }); expect(defaultHandler.formattedNotificationNumber(notification)).toBe(''); }); @@ -111,7 +111,7 @@ describe('renderer/utils/notifications/handlers/default.ts', () => { const notification = createPartialMockNotification({ title: 'Fix bug', type: 'Issue', - state: 'open', + state: 'OPEN', }); notification.subject.number = 101; expect(defaultHandler.formattedNotificationTitle(notification)).toBe( @@ -123,7 +123,7 @@ describe('renderer/utils/notifications/handlers/default.ts', () => { const notification = createPartialMockNotification({ title: 'Improve docs', type: 'Issue', - state: 'open', + state: 'OPEN', }); expect(defaultHandler.formattedNotificationTitle(notification)).toBe( 'Improve docs', diff --git a/src/renderer/utils/notifications/handlers/default.ts b/src/renderer/utils/notifications/handlers/default.ts index e5dbfeca6..019e0a3fc 100644 --- a/src/renderer/utils/notifications/handlers/default.ts +++ b/src/renderer/utils/notifications/handlers/default.ts @@ -11,11 +11,6 @@ import type { Subject, SubjectType, } from '../../../typesGitHub'; -import { - IssueState, - IssueStateReason, - PullRequestState, -} from '../../api/graphql/generated/graphql'; import type { NotificationTypeHandler } from './types'; import { formatForDisplay } from './utils'; @@ -35,24 +30,17 @@ export class DefaultHandler implements NotificationTypeHandler { iconColor(subject: Subject): IconColor { switch (subject.state) { - case 'open': - case 'reopened': + case 'OPEN': + case 'REOPENED': case 'ANSWERED': - case 'success': - case IssueState.Open: - case IssueStateReason.Reopened: - case PullRequestState.Open: + case 'SUCCESS': return IconColor.GREEN; - case 'closed': - case 'failure': - case IssueState.Closed: - case PullRequestState.Closed: + case 'CLOSED': + case 'FAILURE': return IconColor.RED; - case 'completed': + case 'COMPLETED': case 'RESOLVED': - case 'merged': - case IssueStateReason.Completed: - case PullRequestState.Merged: + case 'MERGED': return IconColor.PURPLE; default: return IconColor.GRAY; diff --git a/src/renderer/utils/notifications/handlers/discussion.test.ts b/src/renderer/utils/notifications/handlers/discussion.test.ts index 1c6230f26..63125c38e 100644 --- a/src/renderer/utils/notifications/handlers/discussion.test.ts +++ b/src/renderer/utils/notifications/handlers/discussion.test.ts @@ -8,9 +8,9 @@ import { import { mockSettings } from '../../../__mocks__/state-mocks'; import type { Link } from '../../../types'; import type { Owner, Repository } from '../../../typesGitHub'; -import { - type AuthorFieldsFragment, - type Discussion, +import type { + AuthorFieldsFragment, + Discussion, DiscussionStateReason, } from '../../api/graphql/generated/graphql'; import { discussionHandler } from './discussion'; @@ -85,10 +85,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { .reply(200, { data: { repository: { - discussion: mockDiscussionNode( - DiscussionStateReason.Duplicate, - false, - ), + discussion: mockDiscussionNode('DUPLICATE', false), }, }, }); @@ -148,10 +145,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { .reply(200, { data: { repository: { - discussion: mockDiscussionNode( - DiscussionStateReason.Outdated, - false, - ), + discussion: mockDiscussionNode('OUTDATED', false), }, }, }); @@ -181,10 +175,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { .reply(200, { data: { repository: { - discussion: mockDiscussionNode( - DiscussionStateReason.Reopened, - false, - ), + discussion: mockDiscussionNode('REOPENED', false), }, }, }); @@ -214,10 +205,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { .reply(200, { data: { repository: { - discussion: mockDiscussionNode( - DiscussionStateReason.Resolved, - true, - ), + discussion: mockDiscussionNode('RESOLVED', true), }, }, }); diff --git a/src/renderer/utils/notifications/handlers/discussion.ts b/src/renderer/utils/notifications/handlers/discussion.ts index afd3d8702..1bf65ec98 100644 --- a/src/renderer/utils/notifications/handlers/discussion.ts +++ b/src/renderer/utils/notifications/handlers/discussion.ts @@ -12,7 +12,7 @@ import { differenceInMilliseconds } from 'date-fns'; import type { Link, SettingsState } from '../../../types'; import type { - DiscussionStateType, + GitifyDiscussionState, GitifySubject, Notification, Subject, @@ -53,7 +53,7 @@ class DiscussionHandler extends DefaultHandler { return null; } - let discussionState: DiscussionStateType = 'OPEN'; + let discussionState: GitifyDiscussionState = 'OPEN'; if (discussion.isAnswered) { discussionState = 'ANSWERED'; diff --git a/src/renderer/utils/notifications/handlers/issue.test.ts b/src/renderer/utils/notifications/handlers/issue.test.ts index 0a5bde114..1f9da4311 100644 --- a/src/renderer/utils/notifications/handlers/issue.test.ts +++ b/src/renderer/utils/notifications/handlers/issue.test.ts @@ -301,7 +301,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { expect( issueHandler.iconType( - createMockSubject({ type: 'Issue', state: 'draft' }), + createMockSubject({ type: 'Issue', state: 'DRAFT' }), ).displayName, ).toBe('IssueDraftIcon'); @@ -309,7 +309,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { issueHandler.iconType( createMockSubject({ type: 'Issue', - state: 'closed', + state: 'CLOSED', }), ).displayName, ).toBe('IssueClosedIcon'); @@ -318,7 +318,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { issueHandler.iconType( createMockSubject({ type: 'Issue', - state: 'completed', + state: 'COMPLETED', }), ).displayName, ).toBe('IssueClosedIcon'); @@ -327,7 +327,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { issueHandler.iconType( createMockSubject({ type: 'Issue', - state: 'not_planned', + state: 'NOT_PLANNED', }), ).displayName, ).toBe('SkipIcon'); @@ -336,7 +336,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { issueHandler.iconType( createMockSubject({ type: 'Issue', - state: 'reopened', + state: 'REOPENED', }), ).displayName, ).toBe('IssueReopenedIcon'); diff --git a/src/renderer/utils/notifications/handlers/issue.ts b/src/renderer/utils/notifications/handlers/issue.ts index f63fe5cb2..1a70e3abd 100644 --- a/src/renderer/utils/notifications/handlers/issue.ts +++ b/src/renderer/utils/notifications/handlers/issue.ts @@ -10,15 +10,12 @@ import { import type { Link, SettingsState } from '../../../types'; import type { + GitifyIssueState, GitifySubject, Notification, Subject, } from '../../../typesGitHub'; import { fetchIssueByNumber } from '../../api/client'; -import { - IssueState, - IssueStateReason, -} from '../../api/graphql/generated/graphql'; import { isStateFilteredOut, isUserFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; import { getSubjectAuthor } from './utils'; @@ -61,16 +58,14 @@ class IssueHandler extends DefaultHandler { } iconType(subject: Subject): FC | null { - const state = subject.state as IssueState | IssueStateReason; - - switch (state) { - case IssueState.Closed: - case IssueStateReason.Completed: + switch (subject.state as GitifyIssueState) { + case 'CLOSED': + case 'COMPLETED': return IssueClosedIcon; - case IssueStateReason.Duplicate: - case IssueStateReason.NotPlanned: + case 'DUPLICATE': + case 'NOT_PLANNED': return SkipIcon; - case IssueStateReason.Reopened: + case 'REOPENED': return IssueReopenedIcon; default: return IssueOpenedIcon; diff --git a/src/renderer/utils/notifications/handlers/pullRequest.test.ts b/src/renderer/utils/notifications/handlers/pullRequest.test.ts index 6e22377e5..8b6befc6b 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.test.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.test.ts @@ -9,7 +9,7 @@ import { mockSettings } from '../../../__mocks__/state-mocks'; import { createPartialMockUser } from '../../../__mocks__/user-mocks'; import type { Link } from '../../../types'; import type { Notification } from '../../../typesGitHub'; -import { PullRequestReviewState } from '../../api/graphql/generated/graphql'; +import type { PullRequestReviewState } from '../../api/graphql/generated/graphql'; import { getLatestReviewForReviewers, pullRequestHandler } from './pullRequest'; describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { @@ -409,7 +409,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { pullRequestHandler.iconType( createMockSubject({ type: 'PullRequest', - state: 'draft', + state: 'DRAFT', }), ).displayName, ).toBe('GitPullRequestDraftIcon'); @@ -418,7 +418,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { pullRequestHandler.iconType( createMockSubject({ type: 'PullRequest', - state: 'closed', + state: 'CLOSED', }), ).displayName, ).toBe('GitPullRequestClosedIcon'); @@ -427,7 +427,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { pullRequestHandler.iconType( createMockSubject({ type: 'PullRequest', - state: 'merged', + state: 'MERGED', }), ).displayName, ).toBe('GitMergeIcon'); @@ -440,25 +440,25 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { author: { login: 'reviewer-1', }, - state: PullRequestReviewState.ChangesRequested, + state: 'CHANGES_REQUESTED' as PullRequestReviewState, }, { author: { login: 'reviewer-2', }, - state: PullRequestReviewState.Commented, + state: 'COMMENTED' as PullRequestReviewState, }, { author: { login: 'reviewer-1', }, - state: PullRequestReviewState.Approved, + state: 'APPROVED' as PullRequestReviewState, }, { author: { login: 'reviewer-3', }, - state: PullRequestReviewState.Approved, + state: 'APPROVED' as PullRequestReviewState, }, ]; diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index 4b9d2c765..89aa125d7 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -11,16 +11,13 @@ import { import type { Link, SettingsState } from '../../../types'; import type { GitifyPullRequestReview, + GitifyPullRequestState, GitifySubject, Notification, - PullRequestStateType, Subject, } from '../../../typesGitHub'; import { fetchPullByNumber } from '../../api/client'; -import { - type FetchPullByNumberQuery, - PullRequestState, -} from '../../api/graphql/generated/graphql'; +import type { FetchPullByNumberQuery } from '../../api/graphql/generated/graphql'; import { isStateFilteredOut, isUserFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; import { getSubjectAuthor } from './utils'; @@ -35,9 +32,9 @@ class PullRequestHandler extends DefaultHandler { const response = await fetchPullByNumber(notification); const pr = response.data.repository.pullRequest; - let prState: PullRequestStateType | PullRequestState = pr.state; + let prState: GitifyPullRequestState = pr.state; if (pr.isDraft) { - prState = 'draft'; + prState = 'DRAFT'; } // Return early if this notification would be hidden by state filters @@ -72,16 +69,12 @@ class PullRequestHandler extends DefaultHandler { } iconType(subject: Subject): FC | null { - const state = subject.state as PullRequestStateType | PullRequestState; - - switch (state) { - case 'draft': + switch (subject.state as GitifyPullRequestState) { + case 'DRAFT': return GitPullRequestDraftIcon; - case PullRequestState.Closed: - case 'closed': + case 'CLOSED': return GitPullRequestClosedIcon; - case PullRequestState.Merged: - case 'merged': + case 'MERGED': return GitMergeIcon; default: return GitPullRequestIcon; diff --git a/src/renderer/utils/notifications/handlers/workflowRun.ts b/src/renderer/utils/notifications/handlers/workflowRun.ts index bafd7f312..6b2904fac 100644 --- a/src/renderer/utils/notifications/handlers/workflowRun.ts +++ b/src/renderer/utils/notifications/handlers/workflowRun.ts @@ -5,15 +5,20 @@ import { RocketIcon } from '@primer/octicons-react'; import type { Link, SettingsState } from '../../../types'; import type { - CheckSuiteStatus, + GitifyCheckSuiteStatus, GitifySubject, Notification, Subject, - WorkflowRunAttributes, } from '../../../typesGitHub'; import { actionsURL } from '../../helpers'; import { DefaultHandler } from './default'; +export interface WorkflowRunAttributes { + user: string; + statusDisplayName: string; + status: GitifyCheckSuiteStatus | null; +} + class WorkflowRunHandler extends DefaultHandler { readonly type = 'WorkflowRun'; @@ -72,10 +77,12 @@ export function getWorkflowRunAttributes( }; } -function getWorkflowRunStatus(statusDisplayName: string): CheckSuiteStatus { +function getWorkflowRunStatus( + statusDisplayName: string, +): GitifyCheckSuiteStatus { switch (statusDisplayName) { case 'review': - return 'waiting'; + return 'WAITING'; default: return null; } From 5f42503d8bc28d44bf8f67b608abf406a52d3296 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 12:21:37 +1000 Subject: [PATCH 09/25] refactor(api): update types Signed-off-by: Adam Setch --- src/renderer/__mocks__/notifications-mocks.ts | 11 ++- src/renderer/types.ts | 66 +++++++++++++ src/renderer/typesGitHub.ts | 94 ++++--------------- src/renderer/utils/icons.test.ts | 3 +- src/renderer/utils/icons.ts | 8 +- src/renderer/utils/links.ts | 6 +- .../utils/notifications/filters/filter.ts | 14 +-- .../utils/notifications/filters/state.ts | 15 ++- .../notifications/handlers/checkSuite.ts | 8 +- .../utils/notifications/handlers/commit.ts | 16 ++-- .../notifications/handlers/default.test.ts | 33 ++++--- .../utils/notifications/handlers/default.ts | 9 +- .../notifications/handlers/discussion.ts | 12 +-- .../utils/notifications/handlers/issue.ts | 12 +-- .../notifications/handlers/pullRequest.ts | 12 +-- .../utils/notifications/handlers/release.ts | 16 ++-- .../utils/notifications/handlers/types.ts | 9 +- .../notifications/handlers/utils.test.ts | 10 +- .../utils/notifications/handlers/utils.ts | 33 +------ .../notifications/handlers/workflowRun.ts | 8 +- .../utils/notifications/notifications.ts | 3 +- 21 files changed, 189 insertions(+), 209 deletions(-) diff --git a/src/renderer/__mocks__/notifications-mocks.ts b/src/renderer/__mocks__/notifications-mocks.ts index fe1d45fda..c0cc02e95 100644 --- a/src/renderer/__mocks__/notifications-mocks.ts +++ b/src/renderer/__mocks__/notifications-mocks.ts @@ -1,9 +1,12 @@ import { Constants } from '../constants'; -import type { AccountNotifications, Hostname } from '../types'; +import type { + AccountNotifications, + GitifyNotificationState, + Hostname, +} from '../types'; import type { Notification, Repository, - StateType, Subject, SubjectType, } from '../typesGitHub'; @@ -43,12 +46,12 @@ export const mockSingleAccountNotifications: AccountNotifications[] = [ export function createMockSubject(mocks: { title?: string; type?: SubjectType; - state?: StateType; + state?: GitifyNotificationState; }): Subject { return { title: mocks.title ?? 'Mock Subject', type: mocks.type ?? ('Unknown' as SubjectType), - state: mocks.state ?? ('Unknown' as StateType), + state: mocks.state ?? ('Unknown' as GitifyNotificationState), url: null, latest_comment_url: null, }; diff --git a/src/renderer/types.ts b/src/renderer/types.ts index 3ce20b6ea..c407c3f01 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -8,6 +8,15 @@ import type { SubjectType, UserType, } from './typesGitHub'; +import type { + AuthorFieldsFragment, + DiscussionStateReason, + IssueState, + IssueStateReason, + MilestoneFieldsFragment, + PullRequestReviewState, + PullRequestState, +} from './utils/api/graphql/generated/graphql'; import type { AuthMethod, PlatformType } from './utils/auth/types'; declare const __brand: unique symbol; @@ -251,3 +260,60 @@ export interface Chevron { } export type FilterStateType = 'open' | 'closed' | 'merged' | 'draft' | 'other'; + +/** + * + * Gitify Notification Types + * + **/ + +export interface GitifyNotification { + account: Account; + order: number; +} + +export interface GitifySubject { + number?: number; + state?: GitifyNotificationState; + user?: GitifyNotificationUser; + reviews?: GitifyPullRequestReview[]; + linkedIssues?: string[]; + comments?: number; + labels?: string[]; + milestone?: MilestoneFieldsFragment; + htmlUrl?: Link; +} + +export type GitifyNotificationUser = AuthorFieldsFragment; + +export interface GitifyPullRequestReview { + state: PullRequestReviewState; + users: string[]; +} + +export type GitifyDiscussionState = DiscussionStateReason | 'OPEN' | 'ANSWERED'; + +export type GitifyPullRequestState = PullRequestState | 'DRAFT'; + +export type GitifyIssueState = IssueState | IssueStateReason; + +export type GitifyNotificationState = + | GitifyCheckSuiteStatus + | GitifyDiscussionState + | GitifyIssueState + | GitifyPullRequestState; + +export type GitifyCheckSuiteStatus = + | 'ACTION_REQUIRED' + | 'CANCELLED' + | 'COMPLETED' + | 'FAILURE' + | 'IN_PROGRESS' + | 'PENDING' + | 'QUEUED' + | 'REQUESTED' + | 'SKIPPED' + | 'STALE' + | 'SUCCESS' + | 'TIMED_OUT' + | 'WAITING'; diff --git a/src/renderer/typesGitHub.ts b/src/renderer/typesGitHub.ts index cc74fa4d9..15f810b49 100644 --- a/src/renderer/typesGitHub.ts +++ b/src/renderer/typesGitHub.ts @@ -1,13 +1,14 @@ -import type { Account, Link } from './types'; -import type { - AuthorFieldsFragment, - DiscussionStateReason, - IssueState, - IssueStateReason, - MilestoneFieldsFragment, - PullRequestReviewState, - PullRequestState, -} from './utils/api/graphql/generated/graphql'; +import type { GitifyNotification, GitifySubject, Link } from './types'; + +// TODO: #828 Add explicit types for GitHub API response vs Gitify Notifications object +export type Notification = GitHubNotification & GitifyNotification; +export type Subject = GitHubSubject & GitifySubject; + +/** + * + * GitHub REST API Response Types + * + **/ export type Reason = | 'approval_requested' @@ -26,9 +27,6 @@ export type Reason = | 'subscribed' | 'team_mention'; -// Note: ANSWERED and OPEN are not an official discussion state type in the GitHub API -export type GitifyDiscussionState = DiscussionStateReason | 'OPEN' | 'ANSWERED'; - export type SubjectType = | 'CheckSuite' | 'Commit' @@ -48,38 +46,6 @@ export type UserType = | 'Organization' | 'User'; -/** - * Note: draft and merged are not official states in the GitHub API. - * These are derived from the pull request's `merged` and `draft` properties. - */ -export type GitifyPullRequestState = PullRequestState | 'DRAFT'; - -export type GitifyIssueState = IssueState | IssueStateReason; - -export type StateType = - | GitifyCheckSuiteStatus - | GitifyDiscussionState - | GitifyIssueState - | GitifyPullRequestState; - -export type GitifyCheckSuiteStatus = - | 'ACTION_REQUIRED' - | 'CANCELLED' - | 'COMPLETED' - | 'FAILURE' - | 'IN_PROGRESS' - | 'PENDING' - | 'QUEUED' - | 'REQUESTED' - | 'SKIPPED' - | 'STALE' - | 'SUCCESS' - | 'TIMED_OUT' - | 'WAITING'; - -// TODO: #828 Add explicit types for GitHub API response vs Gitify Notifications object -export type Notification = GitHubNotification & GitifyNotification; - export interface GitHubNotification { id: string; unread: boolean; @@ -92,10 +58,11 @@ export interface GitHubNotification { subscription_url: Link; } -// Note: This is not in the official GitHub API. We add this to make notification interactions easier. -export interface GitifyNotification { - account: Account; - order: number; +interface GitHubSubject { + title: string; + url: Link | null; + latest_comment_url: Link | null; + type: SubjectType; } export type UserDetails = User & UserProfile; @@ -152,8 +119,6 @@ export interface User { site_admin: boolean; } -export type SubjectUser = AuthorFieldsFragment; - export interface Repository { id: number; node_id: string; @@ -224,33 +189,6 @@ export interface Owner { site_admin: boolean; } -export type Subject = GitHubSubject & GitifySubject; - -interface GitHubSubject { - title: string; - url: Link | null; - latest_comment_url: Link | null; - type: SubjectType; -} - -// This is not in the GitHub API, but we add it to the type to make it easier to work with -export interface GitifySubject { - number?: number; - state?: StateType; - user?: SubjectUser; - reviews?: GitifyPullRequestReview[]; - linkedIssues?: string[]; - comments?: number; - labels?: string[]; - milestone?: MilestoneFieldsFragment; - htmlUrl?: Link; -} - -export interface GitifyPullRequestReview { - state: PullRequestReviewState; - users: string[]; -} - export interface Commit { sha: string; node_id: string; diff --git a/src/renderer/utils/icons.test.ts b/src/renderer/utils/icons.test.ts index 1ca83f48e..eae477ea1 100644 --- a/src/renderer/utils/icons.test.ts +++ b/src/renderer/utils/icons.test.ts @@ -7,8 +7,7 @@ import { OrganizationIcon, } from '@primer/octicons-react'; -import { IconColor } from '../types'; -import type { GitifyPullRequestReview } from '../typesGitHub'; +import { type GitifyPullRequestReview, IconColor } from '../types'; import { getAuthMethodIcon, getDefaultUserIcon, diff --git a/src/renderer/utils/icons.ts b/src/renderer/utils/icons.ts index 1f472ff5d..c682e8b72 100644 --- a/src/renderer/utils/icons.ts +++ b/src/renderer/utils/icons.ts @@ -14,8 +14,12 @@ import { ServerIcon, } from '@primer/octicons-react'; -import { IconColor, type PullRequestApprovalIcon } from '../types'; -import type { GitifyPullRequestReview, UserType } from '../typesGitHub'; +import { + type GitifyPullRequestReview, + IconColor, + type PullRequestApprovalIcon, +} from '../types'; +import type { UserType } from '../typesGitHub'; import type { AuthMethod, PlatformType } from './auth/types'; export function getPullRequestReviewIcon( diff --git a/src/renderer/utils/links.ts b/src/renderer/utils/links.ts index 88ce44109..4b0beb4f5 100644 --- a/src/renderer/utils/links.ts +++ b/src/renderer/utils/links.ts @@ -1,8 +1,8 @@ import { APPLICATION } from '../../shared/constants'; import { Constants } from '../constants'; -import type { Account, Hostname, Link } from '../types'; -import type { Notification, Repository, SubjectUser } from '../typesGitHub'; +import type { Account, GitifyNotificationUser, Hostname, Link } from '../types'; +import type { Notification, Repository } from '../typesGitHub'; import { getDeveloperSettingsURL } from './auth/utils'; import { openExternalLink } from './comms'; import { generateGitHubWebUrl } from './helpers'; @@ -37,7 +37,7 @@ export function openAccountProfile(account: Account) { openExternalLink(url.toString() as Link); } -export function openUserProfile(user: SubjectUser) { +export function openUserProfile(user: GitifyNotificationUser) { openExternalLink(user.html_url); } diff --git a/src/renderer/utils/notifications/filters/filter.ts b/src/renderer/utils/notifications/filters/filter.ts index 89ea66992..9ebb2812f 100644 --- a/src/renderer/utils/notifications/filters/filter.ts +++ b/src/renderer/utils/notifications/filters/filter.ts @@ -1,9 +1,9 @@ -import type { SettingsState } from '../../../types'; import type { - Notification, - StateType, - SubjectUser, -} from '../../../typesGitHub'; + GitifyNotificationState, + GitifyNotificationUser, + SettingsState, +} from '../../../types'; +import type { Notification } from '../../../typesGitHub'; import { BASE_SEARCH_QUALIFIERS, DETAILED_ONLY_SEARCH_QUALIFIERS, @@ -167,7 +167,7 @@ function passesStateFilter( } export function isStateFilteredOut( - state: StateType, + state: GitifyNotificationState, settings: SettingsState, ): boolean { const notification = { subject: { state: state } } as Notification; @@ -176,7 +176,7 @@ export function isStateFilteredOut( } export function isUserFilteredOut( - user: SubjectUser, + user: GitifyNotificationUser, settings: SettingsState, ): boolean { const notification = { subject: { user: user } } as Notification; diff --git a/src/renderer/utils/notifications/filters/state.ts b/src/renderer/utils/notifications/filters/state.ts index c49dbbd85..d7be8f486 100644 --- a/src/renderer/utils/notifications/filters/state.ts +++ b/src/renderer/utils/notifications/filters/state.ts @@ -1,10 +1,11 @@ import type { AccountNotifications, FilterStateType, + GitifyNotificationState, SettingsState, TypeDetails, } from '../../../types'; -import type { Notification, StateType } from '../../../typesGitHub'; +import type { Notification } from '../../../typesGitHub'; import type { Filter } from './types'; const STATE_TYPE_DETAILS: Record = { @@ -63,10 +64,14 @@ export const stateFilter: Filter = { notification: Notification, stateType: FilterStateType, ): boolean { - const allOpenStates: StateType[] = ['OPEN', 'REOPENED']; - const allClosedStates: StateType[] = ['CLOSED', 'COMPLETED', 'NOT_PLANNED']; - const allMergedStates: StateType[] = ['MERGED']; - const allDraftStates: StateType[] = ['DRAFT']; + const allOpenStates: GitifyNotificationState[] = ['OPEN', 'REOPENED']; + const allClosedStates: GitifyNotificationState[] = [ + 'CLOSED', + 'COMPLETED', + 'NOT_PLANNED', + ]; + const allMergedStates: GitifyNotificationState[] = ['MERGED']; + const allDraftStates: GitifyNotificationState[] = ['DRAFT']; const allFilterableStates = [ ...allOpenStates, ...allClosedStates, diff --git a/src/renderer/utils/notifications/handlers/checkSuite.ts b/src/renderer/utils/notifications/handlers/checkSuite.ts index eaa06d7f3..c6f8a5c87 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.ts @@ -9,13 +9,13 @@ import { XIcon, } from '@primer/octicons-react'; -import type { Link, SettingsState } from '../../../types'; import type { GitifyCheckSuiteStatus, GitifySubject, - Notification, - Subject, -} from '../../../typesGitHub'; + Link, + SettingsState, +} from '../../../types'; +import type { Notification, Subject } from '../../../typesGitHub'; import { actionsURL } from '../../helpers'; import { DefaultHandler } from './default'; diff --git a/src/renderer/utils/notifications/handlers/commit.ts b/src/renderer/utils/notifications/handlers/commit.ts index 882134f03..cc13f5bcd 100644 --- a/src/renderer/utils/notifications/handlers/commit.ts +++ b/src/renderer/utils/notifications/handlers/commit.ts @@ -3,18 +3,16 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; import { GitCommitIcon } from '@primer/octicons-react'; -import type { SettingsState } from '../../../types'; import type { + GitifyNotificationState, GitifySubject, - Notification, - StateType, - Subject, - User, -} from '../../../typesGitHub'; + SettingsState, +} from '../../../types'; +import type { Notification, Subject, User } from '../../../typesGitHub'; import { getCommit, getCommitComment } from '../../api/client'; import { isStateFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; -import { getSubjectUser } from './utils'; +import { getNotificationAuthor } from './utils'; class CommitHandler extends DefaultHandler { readonly type = 'Commit'; @@ -23,7 +21,7 @@ class CommitHandler extends DefaultHandler { notification: Notification, settings: SettingsState, ): Promise { - const commitState: StateType = null; // Commit notifications are stateless + const commitState: GitifyNotificationState = null; // Commit notifications are stateless // Return early if this notification would be hidden by filters if (isStateFilteredOut(commitState, settings)) { @@ -51,7 +49,7 @@ class CommitHandler extends DefaultHandler { return { state: commitState, - user: getSubjectUser([user]), + user: getNotificationAuthor([user]), }; } diff --git a/src/renderer/utils/notifications/handlers/default.test.ts b/src/renderer/utils/notifications/handlers/default.test.ts index 5f7439541..dae0a5322 100644 --- a/src/renderer/utils/notifications/handlers/default.test.ts +++ b/src/renderer/utils/notifications/handlers/default.test.ts @@ -3,8 +3,7 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; -import { IconColor } from '../../../types'; -import type { StateType } from '../../../typesGitHub'; +import { type GitifyNotificationState, IconColor } from '../../../types'; import { defaultHandler } from './default'; describe('renderer/utils/notifications/handlers/default.ts', () => { @@ -32,21 +31,21 @@ describe('renderer/utils/notifications/handlers/default.ts', () => { }); describe('iconColor', () => { - const cases: Array<[StateType | null, IconColor]> = [ - ['open' as StateType, IconColor.GREEN], - ['reopened' as StateType, IconColor.GREEN], - ['ANSWERED' as StateType, IconColor.GREEN], - ['success' as StateType, IconColor.GREEN], - ['closed' as StateType, IconColor.RED], - ['failure' as StateType, IconColor.RED], - ['completed' as StateType, IconColor.PURPLE], - ['RESOLVED' as StateType, IconColor.PURPLE], - ['merged' as StateType, IconColor.PURPLE], - ['not_planned' as StateType, IconColor.GRAY], - ['draft' as StateType, IconColor.GRAY], - ['skipped' as StateType, IconColor.GRAY], - ['cancelled' as StateType, IconColor.GRAY], - ['unknown' as StateType, IconColor.GRAY], + const cases: Array<[GitifyNotificationState | null, IconColor]> = [ + ['OPEN' as GitifyNotificationState, IconColor.GREEN], + ['REOPENED' as GitifyNotificationState, IconColor.GREEN], + ['ANSWERED' as GitifyNotificationState, IconColor.GREEN], + ['SUCCESS' as GitifyNotificationState, IconColor.GREEN], + ['CLOSED' as GitifyNotificationState, IconColor.RED], + ['FAILURE' as GitifyNotificationState, IconColor.RED], + ['COMPLETED' as GitifyNotificationState, IconColor.PURPLE], + ['RESOLVED' as GitifyNotificationState, IconColor.PURPLE], + ['MERGED' as GitifyNotificationState, IconColor.PURPLE], + ['NOT_PLANNED' as GitifyNotificationState, IconColor.GRAY], + ['DRAFT' as GitifyNotificationState, IconColor.GRAY], + ['SKIPPED' as GitifyNotificationState, IconColor.GRAY], + ['CANCELLED' as GitifyNotificationState, IconColor.GRAY], + ['unknown' as GitifyNotificationState, IconColor.GRAY], [null, IconColor.GRAY], [undefined, IconColor.GRAY], ]; diff --git a/src/renderer/utils/notifications/handlers/default.ts b/src/renderer/utils/notifications/handlers/default.ts index 019e0a3fc..123b91e45 100644 --- a/src/renderer/utils/notifications/handlers/default.ts +++ b/src/renderer/utils/notifications/handlers/default.ts @@ -3,14 +3,9 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; import { QuestionIcon } from '@primer/octicons-react'; -import type { Link, SettingsState } from '../../../types'; +import type { GitifySubject, Link, SettingsState } from '../../../types'; import { IconColor } from '../../../types'; -import type { - GitifySubject, - Notification, - Subject, - SubjectType, -} from '../../../typesGitHub'; +import type { Notification, Subject, SubjectType } from '../../../typesGitHub'; import type { NotificationTypeHandler } from './types'; import { formatForDisplay } from './utils'; diff --git a/src/renderer/utils/notifications/handlers/discussion.ts b/src/renderer/utils/notifications/handlers/discussion.ts index 1bf65ec98..ce91e7408 100644 --- a/src/renderer/utils/notifications/handlers/discussion.ts +++ b/src/renderer/utils/notifications/handlers/discussion.ts @@ -10,13 +10,13 @@ import { import { differenceInMilliseconds } from 'date-fns'; -import type { Link, SettingsState } from '../../../types'; import type { GitifyDiscussionState, GitifySubject, - Notification, - Subject, -} from '../../../typesGitHub'; + Link, + SettingsState, +} from '../../../types'; +import type { Notification, Subject } from '../../../typesGitHub'; import { fetchDiscussionByNumber } from '../../api/client'; import type { CommentFieldsFragment, @@ -24,7 +24,7 @@ import type { } from '../../api/graphql/generated/graphql'; import { isStateFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; -import { getSubjectAuthor } from './utils'; +import { getNotificationAuthor } from './utils'; type DiscussionComment = NonNullable< NonNullable< @@ -74,7 +74,7 @@ class DiscussionHandler extends DefaultHandler { return { number: discussion.number, state: discussionState, - user: getSubjectAuthor([ + user: getNotificationAuthor([ latestDiscussionComment.author, discussion.author, ]), diff --git a/src/renderer/utils/notifications/handlers/issue.ts b/src/renderer/utils/notifications/handlers/issue.ts index 1a70e3abd..59d2bb7f6 100644 --- a/src/renderer/utils/notifications/handlers/issue.ts +++ b/src/renderer/utils/notifications/handlers/issue.ts @@ -8,17 +8,17 @@ import { SkipIcon, } from '@primer/octicons-react'; -import type { Link, SettingsState } from '../../../types'; import type { GitifyIssueState, GitifySubject, - Notification, - Subject, -} from '../../../typesGitHub'; + Link, + SettingsState, +} from '../../../types'; +import type { Notification, Subject } from '../../../typesGitHub'; import { fetchIssueByNumber } from '../../api/client'; import { isStateFilteredOut, isUserFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; -import { getSubjectAuthor } from './utils'; +import { getNotificationAuthor } from './utils'; class IssueHandler extends DefaultHandler { readonly type = 'Issue'; @@ -39,7 +39,7 @@ class IssueHandler extends DefaultHandler { const issueCommentUser = issue.comments.nodes[0].author; - const issueUser = getSubjectAuthor([issueCommentUser, issue.author]); + const issueUser = getNotificationAuthor([issueCommentUser, issue.author]); // Return early if this notification would be hidden by user filters if (isUserFilteredOut(issueUser, settings)) { diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index 89aa125d7..bb0ccd0c0 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -8,19 +8,19 @@ import { GitPullRequestIcon, } from '@primer/octicons-react'; -import type { Link, SettingsState } from '../../../types'; import type { GitifyPullRequestReview, GitifyPullRequestState, GitifySubject, - Notification, - Subject, -} from '../../../typesGitHub'; + Link, + SettingsState, +} from '../../../types'; +import type { Notification, Subject } from '../../../typesGitHub'; import { fetchPullByNumber } from '../../api/client'; import type { FetchPullByNumberQuery } from '../../api/graphql/generated/graphql'; import { isStateFilteredOut, isUserFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; -import { getSubjectAuthor } from './utils'; +import { getNotificationAuthor } from './utils'; class PullRequestHandler extends DefaultHandler { readonly type = 'PullRequest' as const; @@ -44,7 +44,7 @@ class PullRequestHandler extends DefaultHandler { const prCommentUser = pr.comments.nodes[0]?.author; - const prUser = getSubjectAuthor([prCommentUser, pr.author]); + const prUser = getNotificationAuthor([prCommentUser, pr.author]); // Return early if this notification would be hidden by user filters if (isUserFilteredOut(prUser, settings)) { diff --git a/src/renderer/utils/notifications/handlers/release.ts b/src/renderer/utils/notifications/handlers/release.ts index 8a3190588..9b2fe3149 100644 --- a/src/renderer/utils/notifications/handlers/release.ts +++ b/src/renderer/utils/notifications/handlers/release.ts @@ -3,17 +3,17 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; import { TagIcon } from '@primer/octicons-react'; -import type { Link, SettingsState } from '../../../types'; import type { + GitifyNotificationState, GitifySubject, - Notification, - StateType, - Subject, -} from '../../../typesGitHub'; + Link, + SettingsState, +} from '../../../types'; +import type { Notification, Subject } from '../../../typesGitHub'; import { getRelease } from '../../api/client'; import { isStateFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; -import { getSubjectUser } from './utils'; +import { getNotificationAuthor } from './utils'; class ReleaseHandler extends DefaultHandler { readonly type = 'Release'; @@ -22,7 +22,7 @@ class ReleaseHandler extends DefaultHandler { notification: Notification, settings: SettingsState, ): Promise { - const releaseState: StateType = null; // Release notifications are stateless + const releaseState: GitifyNotificationState = null; // Release notifications are stateless // Return early if this notification would be hidden by filters if (isStateFilteredOut(releaseState, settings)) { @@ -35,7 +35,7 @@ class ReleaseHandler extends DefaultHandler { return { state: releaseState, - user: getSubjectUser([release.author]), + user: getNotificationAuthor([release.author]), }; } diff --git a/src/renderer/utils/notifications/handlers/types.ts b/src/renderer/utils/notifications/handlers/types.ts index 36a52fbd6..ced167101 100644 --- a/src/renderer/utils/notifications/handlers/types.ts +++ b/src/renderer/utils/notifications/handlers/types.ts @@ -2,13 +2,8 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; -import type { Link, SettingsState } from '../../../types'; -import type { - GitifySubject, - Notification, - Subject, - SubjectType, -} from '../../../typesGitHub'; +import type { GitifySubject, Link, SettingsState } from '../../../types'; +import type { Notification, Subject, SubjectType } from '../../../typesGitHub'; export interface NotificationTypeHandler { readonly type?: SubjectType; diff --git a/src/renderer/utils/notifications/handlers/utils.test.ts b/src/renderer/utils/notifications/handlers/utils.test.ts index b16f1d812..3b18b6a94 100644 --- a/src/renderer/utils/notifications/handlers/utils.test.ts +++ b/src/renderer/utils/notifications/handlers/utils.test.ts @@ -1,18 +1,18 @@ import { createPartialMockUser } from '../../../__mocks__/user-mocks'; -import { formatForDisplay, getSubjectUser } from './utils'; +import { formatForDisplay, getNotificationAuthor } from './utils'; describe('renderer/utils/notifications/handlers/utils.ts', () => { - describe('getSubjectUser', () => { + describe('getNotificationAuthor', () => { const mockAuthor = createPartialMockUser('some-author'); it('returns null when all users are null', () => { - const result = getSubjectUser([null, null]); + const result = getNotificationAuthor([null, null]); expect(result).toBeNull(); }); it('returns first user', () => { - const result = getSubjectUser([mockAuthor, null]); + const result = getNotificationAuthor([mockAuthor, null]); expect(result).toEqual({ login: mockAuthor.login, @@ -23,7 +23,7 @@ describe('renderer/utils/notifications/handlers/utils.ts', () => { }); it('returns second user if first is null', () => { - const result = getSubjectUser([null, mockAuthor]); + const result = getNotificationAuthor([null, mockAuthor]); expect(result).toEqual({ login: mockAuthor.login, diff --git a/src/renderer/utils/notifications/handlers/utils.ts b/src/renderer/utils/notifications/handlers/utils.ts index e46b70b90..4f9187d36 100644 --- a/src/renderer/utils/notifications/handlers/utils.ts +++ b/src/renderer/utils/notifications/handlers/utils.ts @@ -1,37 +1,14 @@ -import type { SubjectUser, User } from '../../../typesGitHub'; -import type { AuthorFieldsFragment } from '../../api/graphql/generated/graphql'; +import type { GitifyNotificationUser } from '../../../types'; /** * Construct the notification subject user based on an order prioritized list of users * @param users array of users in order or priority * @returns the subject user */ -export function getSubjectUser(users: User[]): SubjectUser { - let subjectUser: SubjectUser = null; - - for (const user of users) { - if (user) { - subjectUser = { - login: user.login, - html_url: user.html_url, - avatar_url: user.avatar_url, - type: user.type, - }; - - return subjectUser; - } - } - - return subjectUser; -} - -/** - * Construct the notification subject user based on an order prioritized list of users - * @param users array of users in order or priority - * @returns the subject user - */ -export function getSubjectAuthor(users: AuthorFieldsFragment[]): SubjectUser { - let subjectUser: SubjectUser = null; +export function getNotificationAuthor( + users: GitifyNotificationUser[], +): GitifyNotificationUser { + let subjectUser: GitifyNotificationUser = null; for (const user of users) { if (user) { diff --git a/src/renderer/utils/notifications/handlers/workflowRun.ts b/src/renderer/utils/notifications/handlers/workflowRun.ts index 6b2904fac..5b10ae2df 100644 --- a/src/renderer/utils/notifications/handlers/workflowRun.ts +++ b/src/renderer/utils/notifications/handlers/workflowRun.ts @@ -3,13 +3,13 @@ import type { FC } from 'react'; import type { OcticonProps } from '@primer/octicons-react'; import { RocketIcon } from '@primer/octicons-react'; -import type { Link, SettingsState } from '../../../types'; import type { GitifyCheckSuiteStatus, GitifySubject, - Notification, - Subject, -} from '../../../typesGitHub'; + Link, + SettingsState, +} from '../../../types'; +import type { Notification, Subject } from '../../../typesGitHub'; import { actionsURL } from '../../helpers'; import { DefaultHandler } from './default'; diff --git a/src/renderer/utils/notifications/notifications.ts b/src/renderer/utils/notifications/notifications.ts index 7780af27c..ff822c872 100644 --- a/src/renderer/utils/notifications/notifications.ts +++ b/src/renderer/utils/notifications/notifications.ts @@ -1,9 +1,10 @@ import type { AccountNotifications, GitifyState, + GitifySubject, SettingsState, } from '../../types'; -import type { GitifySubject, Notification } from '../../typesGitHub'; +import type { Notification } from '../../typesGitHub'; import { listNotificationsForAuthenticatedUser } from '../api/client'; import { determineFailureType } from '../api/errors'; import { rendererLogError, rendererLogWarn } from '../logger'; From df570ea41dcce22071ecd0f2523ff7d08c6d0ae0 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 17:39:15 +1000 Subject: [PATCH 10/25] refactor: tests Signed-off-by: Adam Setch --- src/renderer/__mocks__/notifications-mocks.ts | 10 +- src/renderer/utils/helpers.test.ts | 75 +-- src/renderer/utils/helpers.ts | 46 +- .../notifications/handlers/checkSuite.test.ts | 52 ++- .../notifications/handlers/checkSuite.ts | 4 +- .../notifications/handlers/commit.test.ts | 14 + .../notifications/handlers/default.test.ts | 20 +- .../notifications/handlers/discussion.test.ts | 29 +- .../notifications/handlers/discussion.ts | 14 +- .../notifications/handlers/issue.test.ts | 20 +- .../utils/notifications/handlers/issue.ts | 24 +- .../handlers/pullRequest.test.ts | 435 ++++++++---------- .../notifications/handlers/pullRequest.ts | 31 +- .../notifications/handlers/release.test.ts | 14 + .../repositoryDependabotAlertsThread.test.ts | 15 + .../handlers/repositoryInvitation.test.ts | 15 + .../repositoryVulnerabilityAlert.test.ts | 15 + .../handlers/workflowRun.test.ts | 24 +- .../notifications/handlers/workflowRun.ts | 4 +- .../utils/notifications/notifications.test.ts | 8 +- 20 files changed, 495 insertions(+), 374 deletions(-) diff --git a/src/renderer/__mocks__/notifications-mocks.ts b/src/renderer/__mocks__/notifications-mocks.ts index c0cc02e95..5f8fbff1c 100644 --- a/src/renderer/__mocks__/notifications-mocks.ts +++ b/src/renderer/__mocks__/notifications-mocks.ts @@ -71,7 +71,15 @@ export function createPartialMockNotification( hasRequiredScopes: true, }, subject: subject as Subject, - repository: repository as Repository, + repository: { + name: 'notifications-test', + full_name: 'gitify-app/notifications-test', + html_url: 'https://github.com/gitify-app/notifications-test', + owner: { + login: 'gitify-app', + }, + ...repository, + } as Repository, }; return mockNotification as Notification; diff --git a/src/renderer/utils/helpers.test.ts b/src/renderer/utils/helpers.test.ts index 59200dc55..07878688f 100644 --- a/src/renderer/utils/helpers.test.ts +++ b/src/renderer/utils/helpers.test.ts @@ -7,7 +7,7 @@ import { import type { ExecutionResult } from 'graphql'; import type { Hostname, Link } from '../types'; -import type { SubjectType } from '../typesGitHub'; +import type { Subject, SubjectType } from '../typesGitHub'; import * as logger from '../utils/logger'; import { mockDiscussionByNumberGraphQLResponse, @@ -81,6 +81,27 @@ describe('renderer/utils/helpers.ts', () => { jest.clearAllMocks(); }); + it('Subject HTML URL: prefer if available from enrichment stage', async () => { + const mockHtmlUrl = 'https://gitify.io/' as Link; + + const subject = { + title: 'generate github web url unit tests', + url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link, + latest_comment_url: + 'https://api.github.com/repos/gitify-app/notifications-test/issues/comments/302888448' as Link, + type: 'Issue' as SubjectType, + htmlUrl: mockHtmlUrl, + } as Subject; + + const result = await generateGitHubWebUrl({ + ...mockSingleNotification, + subject: subject, + }); + + expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); + expect(result).toBe(`${mockHtmlUrl}?${mockNotificationReferrer}`); + }); + it('Subject Latest Comment Url: when not null, fetch latest comment html url', async () => { const subject = { title: 'generate github web url unit tests', @@ -88,7 +109,7 @@ describe('renderer/utils/helpers.ts', () => { latest_comment_url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/comments/302888448' as Link, type: 'Issue' as SubjectType, - }; + } as Subject; getHtmlUrlSpy.mockResolvedValue(mockHtmlUrl); @@ -107,7 +128,7 @@ describe('renderer/utils/helpers.ts', () => { url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link, latest_comment_url: null, type: 'Issue' as SubjectType, - }; + } as Subject; getHtmlUrlSpy.mockResolvedValue(mockHtmlUrl); @@ -127,7 +148,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'CheckSuite' as SubjectType, - }; + } as Subject; const result = await generateGitHubWebUrl({ ...mockSingleNotification, @@ -136,7 +157,7 @@ describe('renderer/utils/helpers.ts', () => { expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); expect(result).toBe( - `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3Asuccess+branch%3Amain&${mockNotificationReferrer}`, + `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3ASUCCESS+branch%3Amain&${mockNotificationReferrer}`, ); }); @@ -146,7 +167,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'CheckSuite' as SubjectType, - }; + } as Subject; const result = await generateGitHubWebUrl({ ...mockSingleNotification, @@ -155,7 +176,7 @@ describe('renderer/utils/helpers.ts', () => { expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); expect(result).toBe( - `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3Afailure+branch%3Amain&${mockNotificationReferrer}`, + `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3AFAILURE+branch%3Amain&${mockNotificationReferrer}`, ); }); @@ -165,7 +186,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'CheckSuite' as SubjectType, - }; + } as Subject; const result = await generateGitHubWebUrl({ ...mockSingleNotification, @@ -174,7 +195,7 @@ describe('renderer/utils/helpers.ts', () => { expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); expect(result).toBe( - `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3Afailure+branch%3Amain&${mockNotificationReferrer}`, + `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3AFAILURE+branch%3Amain&${mockNotificationReferrer}`, ); }); @@ -184,7 +205,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'CheckSuite' as SubjectType, - }; + } as Subject; const result = await generateGitHubWebUrl({ ...mockSingleNotification, @@ -193,7 +214,7 @@ describe('renderer/utils/helpers.ts', () => { expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); expect(result).toBe( - `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3Askipped+branch%3Amain&${mockNotificationReferrer}`, + `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3ASKIPPED+branch%3Amain&${mockNotificationReferrer}`, ); }); @@ -203,7 +224,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'CheckSuite' as SubjectType, - }; + } as Subject; const result = await generateGitHubWebUrl({ ...mockSingleNotification, @@ -222,7 +243,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'CheckSuite' as SubjectType, - }; + } as Subject; const result = await generateGitHubWebUrl({ ...mockSingleNotification, @@ -241,7 +262,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'CheckSuite' as SubjectType, - }; + } as Subject; const result = await generateGitHubWebUrl({ ...mockSingleNotification, @@ -267,7 +288,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'Discussion' as SubjectType, - }; + } as Subject; fetchDiscussionByNumberSpy.mockResolvedValue({ data: { repository: { discussion: null } }, @@ -294,7 +315,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'Discussion' as SubjectType, - }; + } as Subject; fetchDiscussionByNumberSpy.mockResolvedValue({ data: null, @@ -323,7 +344,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'Discussion' as SubjectType, - }; + } as Subject; fetchDiscussionByNumberSpy.mockResolvedValue({ data: mockDiscussionByNumberGraphQLResponse, @@ -350,7 +371,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'Discussion' as SubjectType, - }; + } as Subject; fetchDiscussionByNumberSpy.mockRejectedValue( new Error('Something failed'), @@ -376,7 +397,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'RepositoryInvitation' as SubjectType, - }; + } as Subject; const result = await generateGitHubWebUrl({ ...mockSingleNotification, @@ -415,7 +436,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'WorkflowRun' as SubjectType, - }; + } as Subject; const result = await generateGitHubWebUrl({ ...mockSingleNotification, @@ -424,7 +445,7 @@ describe('renderer/utils/helpers.ts', () => { expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); expect(result).toBe( - `https://github.com/gitify-app/notifications-test/actions?query=is%3Awaiting&${mockNotificationReferrer}`, + `https://github.com/gitify-app/notifications-test/actions?query=is%3AWAITING&${mockNotificationReferrer}`, ); }); @@ -435,7 +456,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'WorkflowRun' as SubjectType, - }; + } as Subject; const result = await generateGitHubWebUrl({ ...mockSingleNotification, @@ -454,7 +475,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'WorkflowRun' as SubjectType, - }; + } as Subject; const result = await generateGitHubWebUrl({ ...mockSingleNotification, @@ -475,7 +496,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'Issue' as SubjectType, - }; + } as Subject; const result = await generateGitHubWebUrl({ ...mockSingleNotification, @@ -494,7 +515,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'PullRequest' as SubjectType, - }; + } as Subject; const result = await generateGitHubWebUrl({ ...mockSingleNotification, @@ -513,7 +534,7 @@ describe('renderer/utils/helpers.ts', () => { url: null, latest_comment_url: null, type: 'Commit' as SubjectType, - }; + } as Subject; const result = await generateGitHubWebUrl({ ...mockSingleNotification, @@ -536,7 +557,7 @@ describe('renderer/utils/helpers.ts', () => { url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link, latest_comment_url: null as Link, type: 'Issue' as SubjectType, - }; + } as Subject; getHtmlUrlSpy.mockRejectedValue(new Error('Test error')); diff --git a/src/renderer/utils/helpers.ts b/src/renderer/utils/helpers.ts index 6c980fcf4..9580fc116 100644 --- a/src/renderer/utils/helpers.ts +++ b/src/renderer/utils/helpers.ts @@ -47,32 +47,32 @@ export function actionsURL(repositoryURL: string, filters: string[]): Link { export async function generateGitHubWebUrl( notification: Notification, ): Promise { - const url = new URL(notification.repository.html_url); + const handler = createNotificationHandler(notification); + const url = new URL(handler.defaultUrl(notification)); - try { - if (notification.subject.htmlUrl) { - url.href = notification.subject.htmlUrl; - } else if (notification.subject.latest_comment_url) { - url.href = await getHtmlUrl( - notification.subject.latest_comment_url, - notification.account.token, + if (notification.subject.htmlUrl) { + url.href = notification.subject.htmlUrl; + } else { + try { + if (notification.subject.latest_comment_url) { + url.href = await getHtmlUrl( + notification.subject.latest_comment_url, + notification.account.token, + ); + } else if (notification.subject.url) { + url.href = await getHtmlUrl( + notification.subject.url, + notification.account.token, + ); + } + } catch (err) { + rendererLogError( + 'generateGitHubWebUrl', + 'Failed to resolve specific notification html url for', + err, + notification, ); - } else if (notification.subject.url) { - url.href = await getHtmlUrl( - notification.subject.url, - notification.account.token, - ); - } else { - const handler = createNotificationHandler(notification); - handler.defaultUrl(notification); } - } catch (err) { - rendererLogError( - 'generateGitHubWebUrl', - 'Failed to resolve specific notification html url for', - err, - notification, - ); } url.searchParams.set( diff --git a/src/renderer/utils/notifications/handlers/checkSuite.test.ts b/src/renderer/utils/notifications/handlers/checkSuite.test.ts index e22af969a..6c52b5c07 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.test.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.test.ts @@ -3,6 +3,8 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; +import type { Link } from '../../../types'; +import type { Notification } from '../../../typesGitHub'; import { checkSuiteHandler, getCheckSuiteAttributes } from './checkSuite'; describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { @@ -19,8 +21,10 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { ); expect(result).toEqual({ - state: 'cancelled', + state: 'CANCELLED', user: null, + htmlUrl: + 'https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3ACANCELLED+branch%3Amain', }); }); @@ -36,8 +40,10 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { ); expect(result).toEqual({ - state: 'failure', + state: 'FAILURE', user: null, + htmlUrl: + 'https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3AFAILURE+branch%3Amain', }); }); @@ -53,8 +59,10 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { ); expect(result).toEqual({ - state: 'failure', + state: 'FAILURE', user: null, + htmlUrl: + 'https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3AFAILURE+branch%3Amain', }); }); @@ -70,8 +78,10 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { ); expect(result).toEqual({ - state: 'failure', + state: 'FAILURE', user: null, + htmlUrl: + 'https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3AFAILURE+branch%3Amain', }); }); @@ -87,8 +97,10 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { ); expect(result).toEqual({ - state: 'skipped', + state: 'SKIPPED', user: null, + htmlUrl: + 'https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3ASKIPPED+branch%3Amain', }); }); @@ -104,8 +116,10 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { ); expect(result).toEqual({ - state: 'success', + state: 'SUCCESS', user: null, + htmlUrl: + 'https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3ASUCCESS+branch%3Amain', }); }); @@ -182,6 +196,22 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { ).toBe('CheckIcon'); }); + it('defaultUrl', () => { + const mockHtmlUrl = + 'https://github.com/gitify-app/notifications-test' as Link; + + expect( + checkSuiteHandler.defaultUrl({ + subject: { + title: 'Some notification', + }, + repository: { + html_url: mockHtmlUrl, + }, + } as Notification), + ).toEqual(`${mockHtmlUrl}/actions`); + }); + describe('getCheckSuiteState', () => { it('cancelled check suite state', async () => { const mockNotification = createPartialMockNotification({ @@ -194,7 +224,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { expect(result).toEqual({ workflowName: 'Demo', attemptNumber: null, - status: 'cancelled', + status: 'CANCELLED', statusDisplayName: 'cancelled', branchName: 'feature/foo', }); @@ -211,7 +241,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { expect(result).toEqual({ workflowName: 'Demo', attemptNumber: null, - status: 'failure', + status: 'FAILURE', statusDisplayName: 'failed', branchName: 'main', }); @@ -228,7 +258,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { expect(result).toEqual({ workflowName: 'Demo', attemptNumber: 3, - status: 'failure', + status: 'FAILURE', statusDisplayName: 'failed', branchName: 'main', }); @@ -245,7 +275,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { expect(result).toEqual({ workflowName: 'Demo', attemptNumber: null, - status: 'skipped', + status: 'SKIPPED', statusDisplayName: 'skipped', branchName: 'main', }); @@ -262,7 +292,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { expect(result).toEqual({ workflowName: 'Demo', attemptNumber: null, - status: 'success', + status: 'SUCCESS', statusDisplayName: 'succeeded', branchName: 'main', }); diff --git a/src/renderer/utils/notifications/handlers/checkSuite.ts b/src/renderer/utils/notifications/handlers/checkSuite.ts index c6f8a5c87..1bb659406 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.ts @@ -63,9 +63,7 @@ class CheckSuiteHandler extends DefaultHandler { } defaultUrl(notification: Notification): Link { - const url = new URL(notification.repository.html_url); - url.pathname += '/actions'; - return url.href as Link; + return getCheckSuiteUrl(notification); } } diff --git a/src/renderer/utils/notifications/handlers/commit.test.ts b/src/renderer/utils/notifications/handlers/commit.test.ts index d1a54542e..a846bb150 100644 --- a/src/renderer/utils/notifications/handlers/commit.test.ts +++ b/src/renderer/utils/notifications/handlers/commit.test.ts @@ -8,6 +8,7 @@ import { import { mockSettings } from '../../../__mocks__/state-mocks'; import { createPartialMockUser } from '../../../__mocks__/user-mocks'; import type { Link } from '../../../types'; +import type { Notification } from '../../../typesGitHub'; import { commitHandler } from './commit'; describe('renderer/utils/notifications/handlers/commit.ts', () => { @@ -102,4 +103,17 @@ describe('renderer/utils/notifications/handlers/commit.ts', () => { commitHandler.iconType(createMockSubject({ type: 'Commit' })).displayName, ).toBe('GitCommitIcon'); }); + + it('defaultUrl', () => { + const mockHtmlUrl = + 'https://github.com/gitify-app/notifications-test' as Link; + + expect( + commitHandler.defaultUrl({ + repository: { + html_url: mockHtmlUrl, + }, + } as Notification), + ).toEqual(mockHtmlUrl); + }); }); diff --git a/src/renderer/utils/notifications/handlers/default.test.ts b/src/renderer/utils/notifications/handlers/default.test.ts index dae0a5322..39a8f15b1 100644 --- a/src/renderer/utils/notifications/handlers/default.test.ts +++ b/src/renderer/utils/notifications/handlers/default.test.ts @@ -3,7 +3,12 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; -import { type GitifyNotificationState, IconColor } from '../../../types'; +import { + type GitifyNotificationState, + IconColor, + type Link, +} from '../../../types'; +import type { Notification } from '../../../typesGitHub'; import { defaultHandler } from './default'; describe('renderer/utils/notifications/handlers/default.ts', () => { @@ -129,4 +134,17 @@ describe('renderer/utils/notifications/handlers/default.ts', () => { ); }); }); + + it('defaultUrl', () => { + const mockHtmlUrl = + 'https://github.com/gitify-app/notifications-test' as Link; + + expect( + defaultHandler.defaultUrl({ + repository: { + html_url: mockHtmlUrl, + }, + } as Notification), + ).toEqual(mockHtmlUrl); + }); }); diff --git a/src/renderer/utils/notifications/handlers/discussion.test.ts b/src/renderer/utils/notifications/handlers/discussion.test.ts index 63125c38e..58a6a2922 100644 --- a/src/renderer/utils/notifications/handlers/discussion.test.ts +++ b/src/renderer/utils/notifications/handlers/discussion.test.ts @@ -7,7 +7,7 @@ import { } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; import type { Link } from '../../../types'; -import type { Owner, Repository } from '../../../typesGitHub'; +import type { Notification, Owner, Repository } from '../../../typesGitHub'; import type { AuthorFieldsFragment, Discussion, @@ -76,6 +76,8 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { }, comments: 0, labels: [], + htmlUrl: + 'https://github.com/gitify-app/notifications-test/discussions/1', }); }); @@ -106,6 +108,8 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { }, comments: 0, labels: [], + htmlUrl: + 'https://github.com/gitify-app/notifications-test/discussions/1', }); }); @@ -136,6 +140,8 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { }, comments: 0, labels: [], + htmlUrl: + 'https://github.com/gitify-app/notifications-test/discussions/1', }); }); @@ -166,6 +172,8 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { }, comments: 0, labels: [], + htmlUrl: + 'https://github.com/gitify-app/notifications-test/discussions/1', }); }); @@ -196,6 +204,8 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { }, comments: 0, labels: [], + htmlUrl: + 'https://github.com/gitify-app/notifications-test/discussions/1', }); }); @@ -226,6 +236,8 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { }, comments: 0, labels: [], + htmlUrl: + 'https://github.com/gitify-app/notifications-test/discussions/1', }); }); @@ -266,6 +278,8 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { }, comments: 0, labels: ['enhancement'], + htmlUrl: + 'https://github.com/gitify-app/notifications-test/discussions/1', }); }); @@ -313,6 +327,19 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { ).displayName, ).toBe('DiscussionClosedIcon'); }); + + it('defaultUrl', () => { + const mockHtmlUrl = + 'https://github.com/gitify-app/notifications-test' as Link; + + expect( + discussionHandler.defaultUrl({ + repository: { + html_url: mockHtmlUrl, + }, + } as Notification), + ).toEqual(`${mockHtmlUrl}/discussions`); + }); }); function mockDiscussionNode( diff --git a/src/renderer/utils/notifications/handlers/discussion.ts b/src/renderer/utils/notifications/handlers/discussion.ts index ce91e7408..96251269a 100644 --- a/src/renderer/utils/notifications/handlers/discussion.ts +++ b/src/renderer/utils/notifications/handlers/discussion.ts @@ -22,7 +22,6 @@ import type { CommentFieldsFragment, FetchDiscussionByNumberQuery, } from '../../api/graphql/generated/graphql'; -import { isStateFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; import { getNotificationAuthor } from './utils'; @@ -44,7 +43,7 @@ class DiscussionHandler extends DefaultHandler { async enrich( notification: Notification, - settings: SettingsState, + _settings: SettingsState, ): Promise { const response = await fetchDiscussionByNumber(notification); const discussion = response.data.repository?.discussion; @@ -57,13 +56,10 @@ class DiscussionHandler extends DefaultHandler { if (discussion.isAnswered) { discussionState = 'ANSWERED'; - } else if (discussion.stateReason) { - discussionState = discussion.stateReason; } - // Return early if this notification would be hidden by filters - if (isStateFilteredOut(discussionState, settings)) { - return null; + if (discussion.stateReason) { + discussionState = discussion.stateReason; } const latestDiscussionComment = getClosestDiscussionCommentOrReply( @@ -75,12 +71,12 @@ class DiscussionHandler extends DefaultHandler { number: discussion.number, state: discussionState, user: getNotificationAuthor([ - latestDiscussionComment.author, + latestDiscussionComment?.author, discussion.author, ]), comments: discussion.comments.totalCount, labels: discussion.labels?.nodes.map((label) => label.name) ?? [], - htmlUrl: latestDiscussionComment.url ?? discussion.url, + htmlUrl: latestDiscussionComment?.url ?? discussion.url, }; } diff --git a/src/renderer/utils/notifications/handlers/issue.test.ts b/src/renderer/utils/notifications/handlers/issue.test.ts index 1f9da4311..4b20f1665 100644 --- a/src/renderer/utils/notifications/handlers/issue.test.ts +++ b/src/renderer/utils/notifications/handlers/issue.test.ts @@ -300,10 +300,9 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { ).toBe('IssueOpenedIcon'); expect( - issueHandler.iconType( - createMockSubject({ type: 'Issue', state: 'DRAFT' }), - ).displayName, - ).toBe('IssueDraftIcon'); + issueHandler.iconType(createMockSubject({ type: 'Issue', state: 'OPEN' })) + .displayName, + ).toBe('IssueOpenedIcon'); expect( issueHandler.iconType( @@ -341,4 +340,17 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { ).displayName, ).toBe('IssueReopenedIcon'); }); + + it('defaultUrl', () => { + const mockHtmlUrl = + 'https://github.com/gitify-app/notifications-test' as Link; + + expect( + issueHandler.defaultUrl({ + repository: { + html_url: mockHtmlUrl, + }, + } as Notification), + ).toEqual(`${mockHtmlUrl}/issues`); + }); }); diff --git a/src/renderer/utils/notifications/handlers/issue.ts b/src/renderer/utils/notifications/handlers/issue.ts index 59d2bb7f6..62ba9f7a6 100644 --- a/src/renderer/utils/notifications/handlers/issue.ts +++ b/src/renderer/utils/notifications/handlers/issue.ts @@ -16,7 +16,6 @@ import type { } from '../../../types'; import type { Notification, Subject } from '../../../typesGitHub'; import { fetchIssueByNumber } from '../../api/client'; -import { isStateFilteredOut, isUserFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; import { getNotificationAuthor } from './utils'; @@ -25,35 +24,28 @@ class IssueHandler extends DefaultHandler { async enrich( notification: Notification, - settings: SettingsState, + _settings: SettingsState, ): Promise { const response = await fetchIssueByNumber(notification); const issue = response.data.repository?.issue; const issueState = issue.stateReason ?? issue.state; - // Return early if this notification would be hidden by filters - if (isStateFilteredOut(issueState, settings)) { - return null; - } - - const issueCommentUser = issue.comments.nodes[0].author; + const issueComment = issue.comments.nodes[0]; - const issueUser = getNotificationAuthor([issueCommentUser, issue.author]); - - // Return early if this notification would be hidden by user filters - if (isUserFilteredOut(issueUser, settings)) { - return null; - } + const issueUser = getNotificationAuthor([ + issueComment?.author, + issue.author, + ]); return { number: issue.number, state: issueState, user: issueUser, comments: issue.comments.totalCount, - labels: issue.labels.nodes?.map((label) => label.name) ?? [], + labels: issue.labels?.nodes.map((label) => label.name) ?? [], milestone: issue.milestone, - htmlUrl: issue.comments.nodes[0]?.url ?? issue.url, + htmlUrl: issueComment?.url ?? issue.url, }; } diff --git a/src/renderer/utils/notifications/handlers/pullRequest.test.ts b/src/renderer/utils/notifications/handlers/pullRequest.test.ts index 8b6befc6b..d4e88aa74 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.test.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.test.ts @@ -9,7 +9,10 @@ import { mockSettings } from '../../../__mocks__/state-mocks'; import { createPartialMockUser } from '../../../__mocks__/user-mocks'; import type { Link } from '../../../types'; import type { Notification } from '../../../typesGitHub'; -import type { PullRequestReviewState } from '../../api/graphql/generated/graphql'; +import type { + FetchPullByNumberQuery, + PullRequestReviewState, +} from '../../api/graphql/generated/graphql'; import { getLatestReviewForReviewers, pullRequestHandler } from './pullRequest'; describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { @@ -37,24 +40,36 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { it('closed pull request state', async () => { nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1') + .post('/graphql') .reply(200, { - number: 123, - state: 'closed', - draft: false, - merged: false, - user: mockAuthor, - labels: [], + data: { + repository: { + pullRequest: { + __typename: 'PullRequest', + number: 123, + title: 'Test PR', + state: 'CLOSED', + isDraft: false, + merged: false, + isInMergeQueue: false, + url: 'https://github.com/gitify-app/noticiation-test/pulls/1', + user: mockAuthor, + labels: null, + comments: { + totalCount: 1, + nodes: [ + { + author: mockCommenter, + url: 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', + }, + ], + }, + reviews: null, + } as FetchPullByNumberQuery['repository']['pullRequest'], + }, + }, }); - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/comments/302888448') - .reply(200, { user: mockCommenter }); - - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1/reviews') - .reply(200, []); - const result = await pullRequestHandler.enrich( mockNotification, mockSettings, @@ -62,7 +77,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { expect(result).toEqual({ number: 123, - state: 'closed', + state: 'CLOSED', user: { login: mockCommenter.login, html_url: mockCommenter.html_url, @@ -72,29 +87,45 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { reviews: null, labels: [], linkedIssues: [], + comments: 1, + milestone: null, + htmlUrl: + 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', }); }); it('draft pull request state', async () => { nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1') + .post('/graphql') .reply(200, { - number: 123, - state: 'open', - draft: true, - merged: false, - user: mockAuthor, - labels: [], + data: { + repository: { + pullRequest: { + __typename: 'PullRequest', + number: 123, + title: 'Test PR', + state: 'OPEN', + isDraft: true, + merged: false, + isInMergeQueue: false, + url: 'https://github.com/gitify-app/noticiation-test/pulls/1', + user: mockAuthor, + labels: null, + comments: { + totalCount: 1, + nodes: [ + { + author: mockCommenter, + url: 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', + }, + ], + }, + reviews: null, + } as FetchPullByNumberQuery['repository']['pullRequest'], + }, + }, }); - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/comments/302888448') - .reply(200, { user: mockCommenter }); - - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1/reviews') - .reply(200, []); - const result = await pullRequestHandler.enrich( mockNotification, mockSettings, @@ -102,7 +133,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { expect(result).toEqual({ number: 123, - state: 'draft', + state: 'DRAFT', user: { login: mockCommenter.login, html_url: mockCommenter.html_url, @@ -112,29 +143,45 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { reviews: null, labels: [], linkedIssues: [], + comments: 1, + milestone: null, + htmlUrl: + 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', }); }); it('merged pull request state', async () => { nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1') + .post('/graphql') .reply(200, { - number: 123, - state: 'open', - draft: false, - merged: true, - user: mockAuthor, - labels: [], + data: { + repository: { + pullRequest: { + __typename: 'PullRequest', + number: 123, + title: 'Test PR', + state: 'MERGED', + isDraft: false, + merged: true, + isInMergeQueue: false, + url: 'https://github.com/gitify-app/noticiation-test/pulls/1', + user: mockAuthor, + labels: null, + comments: { + totalCount: 1, + nodes: [ + { + author: mockCommenter, + url: 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', + }, + ], + }, + reviews: null, + } as FetchPullByNumberQuery['repository']['pullRequest'], + }, + }, }); - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/comments/302888448') - .reply(200, { user: mockCommenter }); - - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1/reviews') - .reply(200, []); - const result = await pullRequestHandler.enrich( mockNotification, mockSettings, @@ -142,7 +189,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { expect(result).toEqual({ number: 123, - state: 'merged', + state: 'MERGED', user: { login: mockCommenter.login, html_url: mockCommenter.html_url, @@ -152,29 +199,45 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { reviews: null, labels: [], linkedIssues: [], + comments: 1, + milestone: null, + htmlUrl: + 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', }); }); it('open pull request state', async () => { nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1') + .post('/graphql') .reply(200, { - number: 123, - state: 'open', - draft: false, - merged: false, - user: mockAuthor, - labels: [], + data: { + repository: { + pullRequest: { + __typename: 'PullRequest', + number: 123, + title: 'Test PR', + state: 'OPEN', + isDraft: false, + merged: false, + isInMergeQueue: false, + url: 'https://github.com/gitify-app/noticiation-test/pulls/1', + user: mockAuthor, + labels: null, + comments: { + totalCount: 1, + nodes: [ + { + author: mockCommenter, + url: 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', + }, + ], + }, + reviews: null, + } as FetchPullByNumberQuery['repository']['pullRequest'], + }, + }, }); - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/comments/302888448') - .reply(200, { user: mockCommenter }); - - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1/reviews') - .reply(200, []); - const result = await pullRequestHandler.enrich( mockNotification, mockSettings, @@ -182,7 +245,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { expect(result).toEqual({ number: 123, - state: 'open', + state: 'OPEN', user: { login: mockCommenter.login, html_url: mockCommenter.html_url, @@ -192,66 +255,47 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { reviews: null, labels: [], linkedIssues: [], + comments: 1, + milestone: null, + htmlUrl: + 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', }); }); - it('avoid fetching comments if latest_comment_url and url are the same', async () => { - mockNotification.subject.latest_comment_url = - mockNotification.subject.url; - - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1') - .reply(200, { - number: 123, - state: 'open', - draft: false, - merged: false, - user: mockAuthor, - labels: [], - }); - - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1/reviews') - .reply(200, []); - - const result = await pullRequestHandler.enrich( - mockNotification, - mockSettings, - ); - - expect(result).toEqual({ - number: 123, - state: 'open', - user: { - login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, - type: mockAuthor.type, - }, - reviews: null, - labels: [], - linkedIssues: [], - }); - }); - - it('handle pull request without latest_comment_url', async () => { - mockNotification.subject.latest_comment_url = null; - + it('with labels', async () => { nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1') + .post('/graphql') .reply(200, { - number: 123, - state: 'open', - draft: false, - merged: false, - user: mockAuthor, - labels: [], + data: { + repository: { + pullRequest: { + __typename: 'PullRequest', + number: 123, + title: 'Test PR', + state: 'OPEN', + isDraft: false, + merged: false, + isInMergeQueue: false, + url: 'https://github.com/gitify-app/noticiation-test/pulls/1', + user: mockAuthor, + labels: { + nodes: [{ name: 'enhancement' }], + }, + comments: { + totalCount: 1, + nodes: [ + { + author: mockCommenter, + url: 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', + }, + ], + }, + reviews: null, + } as FetchPullByNumberQuery['repository']['pullRequest'], + }, + }, }); - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1/reviews') - .reply(200, []); - const result = await pullRequestHandler.enrich( mockNotification, mockSettings, @@ -259,144 +303,22 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { expect(result).toEqual({ number: 123, - state: 'open', + state: 'OPEN', user: { - login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, - type: mockAuthor.type, + login: mockCommenter.login, + html_url: mockCommenter.html_url, + avatar_url: mockCommenter.avatar_url, + type: mockCommenter.type, }, reviews: null, - labels: [], + labels: ['enhancement'], linkedIssues: [], + comments: 1, + milestone: null, + htmlUrl: + 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', }); }); - - describe('Pull Requests With Labels', () => { - it('with labels', async () => { - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1') - .reply(200, { - number: 123, - state: 'open', - draft: false, - merged: false, - user: mockAuthor, - labels: [{ name: 'enhancement' }], - }); - - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/comments/302888448') - .reply(200, { user: mockCommenter }); - - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1/reviews') - .reply(200, []); - - const result = await pullRequestHandler.enrich( - mockNotification, - mockSettings, - ); - - expect(result).toEqual({ - number: 123, - state: 'open', - user: { - login: mockCommenter.login, - html_url: mockCommenter.html_url, - avatar_url: mockCommenter.avatar_url, - type: mockCommenter.type, - }, - reviews: null, - labels: ['enhancement'], - linkedIssues: [], - }); - }); - - it('handle null labels', async () => { - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1') - .reply(200, { - number: 123, - state: 'open', - draft: false, - merged: false, - user: mockAuthor, - labels: null, - }); - - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/comments/302888448') - .reply(200, { user: mockCommenter }); - - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1/reviews') - .reply(200, []); - - const result = await pullRequestHandler.enrich( - mockNotification, - mockSettings, - ); - - expect(result).toEqual({ - number: 123, - state: 'open', - user: { - login: mockCommenter.login, - html_url: mockCommenter.html_url, - avatar_url: mockCommenter.avatar_url, - type: mockCommenter.type, - }, - reviews: null, - labels: [], - linkedIssues: [], - }); - }); - }); - - it('early return if pull request state filtered', async () => { - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1') - .reply(200, { - number: 123, - state: 'open', - draft: false, - merged: false, - user: mockAuthor, - labels: [], - }); - - const result = await pullRequestHandler.enrich(mockNotification, { - ...mockSettings, - filterStates: ['closed'], - }); - - expect(result).toEqual(null); - }); - - it('early return if pull request user filtered', async () => { - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/1') - .reply(200, { - number: 123, - state: 'open', - draft: false, - merged: false, - user: mockAuthor, - labels: [], - }); - - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/comments/302888448') - .reply(200, { user: mockCommenter }); - - const result = await pullRequestHandler.enrich(mockNotification, { - ...mockSettings, - filterUserTypes: ['Bot'], - }); - - expect(result).toEqual(null); - }); }); it('iconType', () => { @@ -433,6 +355,19 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { ).toBe('GitMergeIcon'); }); + it('defaultUrl', () => { + const mockHtmlUrl = + 'https://github.com/gitify-app/notifications-test' as Link; + + expect( + pullRequestHandler.defaultUrl({ + repository: { + html_url: mockHtmlUrl, + }, + } as Notification), + ).toEqual(`${mockHtmlUrl}/pulls`); + }); + describe('Pull Request Reviews - Latest Reviews By Reviewer', () => { it('returns latest review state per reviewer', async () => { const mockReviews = [ diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index bb0ccd0c0..ed9a61c29 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -18,7 +18,6 @@ import type { import type { Notification, Subject } from '../../../typesGitHub'; import { fetchPullByNumber } from '../../api/client'; import type { FetchPullByNumberQuery } from '../../api/graphql/generated/graphql'; -import { isStateFilteredOut, isUserFilteredOut } from '../filters/filter'; import { DefaultHandler } from './default'; import { getNotificationAuthor } from './utils'; @@ -27,7 +26,7 @@ class PullRequestHandler extends DefaultHandler { async enrich( notification: Notification, - settings: SettingsState, + _settings: SettingsState, ): Promise { const response = await fetchPullByNumber(notification); const pr = response.data.repository.pullRequest; @@ -37,21 +36,13 @@ class PullRequestHandler extends DefaultHandler { prState = 'DRAFT'; } - // Return early if this notification would be hidden by state filters - if (isStateFilteredOut(prState, settings)) { - return null; - } - - const prCommentUser = pr.comments.nodes[0]?.author; + const prComment = pr.comments?.nodes[0]; - const prUser = getNotificationAuthor([prCommentUser, pr.author]); - - // Return early if this notification would be hidden by user filters - if (isUserFilteredOut(prUser, settings)) { - return null; - } + const prUser = getNotificationAuthor([prComment?.author, pr.author]); - const reviews = getLatestReviewForReviewers(pr.reviews.nodes); + const reviews = pr.reviews + ? getLatestReviewForReviewers(pr.reviews.nodes) + : null; return { number: pr.number, @@ -59,12 +50,12 @@ class PullRequestHandler extends DefaultHandler { user: prUser, reviews: reviews, comments: pr.comments.totalCount, - labels: pr.labels.nodes?.map((label) => label.name) ?? [], - linkedIssues: pr.closingIssuesReferences.nodes.map( - (issue) => `#${issue.number}`, - ), + labels: pr.labels?.nodes.map((label) => label.name) ?? [], + linkedIssues: + pr.closingIssuesReferences?.nodes.map((issue) => `#${issue.number}`) ?? + [], milestone: null, //pr.milestone, - htmlUrl: pr.comments.nodes[0]?.url ?? pr.url, + htmlUrl: prComment?.url ?? pr.url, }; } diff --git a/src/renderer/utils/notifications/handlers/release.test.ts b/src/renderer/utils/notifications/handlers/release.test.ts index 530c8d0f9..27490a9cb 100644 --- a/src/renderer/utils/notifications/handlers/release.test.ts +++ b/src/renderer/utils/notifications/handlers/release.test.ts @@ -8,6 +8,7 @@ import { import { mockSettings } from '../../../__mocks__/state-mocks'; import { createPartialMockUser } from '../../../__mocks__/user-mocks'; import type { Link } from '../../../types'; +import type { Notification } from '../../../typesGitHub'; import { releaseHandler } from './release'; describe('renderer/utils/notifications/handlers/release.ts', () => { @@ -76,4 +77,17 @@ describe('renderer/utils/notifications/handlers/release.ts', () => { ).displayName, ).toBe('TagIcon'); }); + + it('defaultUrl', () => { + const mockHtmlUrl = + 'https://github.com/gitify-app/notifications-test' as Link; + + expect( + releaseHandler.defaultUrl({ + repository: { + html_url: mockHtmlUrl, + }, + } as Notification), + ).toEqual(`${mockHtmlUrl}/releases`); + }); }); diff --git a/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.test.ts b/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.test.ts index bd99284ab..c754eba5a 100644 --- a/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.test.ts +++ b/src/renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.test.ts @@ -1,4 +1,6 @@ import { createMockSubject } from '../../../__mocks__/notifications-mocks'; +import type { Link } from '../../../types'; +import type { Notification } from '../../../typesGitHub'; import { repositoryDependabotAlertsThreadHandler } from './repositoryDependabotAlertsThread'; describe('renderer/utils/notifications/handlers/repositoryDependabotAlertsThread.ts', () => { @@ -11,4 +13,17 @@ describe('renderer/utils/notifications/handlers/repositoryDependabotAlertsThread ).displayName, ).toBe('AlertIcon'); }); + + it('defaultUrl', () => { + const mockHtmlUrl = + 'https://github.com/gitify-app/notifications-test' as Link; + + expect( + repositoryDependabotAlertsThreadHandler.defaultUrl({ + repository: { + html_url: mockHtmlUrl, + }, + } as Notification), + ).toEqual(`${mockHtmlUrl}/security/dependabot`); + }); }); diff --git a/src/renderer/utils/notifications/handlers/repositoryInvitation.test.ts b/src/renderer/utils/notifications/handlers/repositoryInvitation.test.ts index 1ccefa670..a9b08ed5d 100644 --- a/src/renderer/utils/notifications/handlers/repositoryInvitation.test.ts +++ b/src/renderer/utils/notifications/handlers/repositoryInvitation.test.ts @@ -1,4 +1,6 @@ import { createMockSubject } from '../../../__mocks__/notifications-mocks'; +import type { Link } from '../../../types'; +import type { Notification } from '../../../typesGitHub'; import { repositoryInvitationHandler } from './repositoryInvitation'; describe('renderer/utils/notifications/handlers/repositoryInvitation.ts', () => { @@ -11,4 +13,17 @@ describe('renderer/utils/notifications/handlers/repositoryInvitation.ts', () => ).displayName, ).toBe('MailIcon'); }); + + it('defaultUrl', () => { + const mockHtmlUrl = + 'https://github.com/gitify-app/notifications-test' as Link; + + expect( + repositoryInvitationHandler.defaultUrl({ + repository: { + html_url: mockHtmlUrl, + }, + } as Notification), + ).toEqual(`${mockHtmlUrl}/invitations`); + }); }); diff --git a/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.test.ts b/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.test.ts index eea848173..73ae231ca 100644 --- a/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.test.ts +++ b/src/renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.test.ts @@ -1,4 +1,6 @@ import { createMockSubject } from '../../../__mocks__/notifications-mocks'; +import type { Link } from '../../../types'; +import type { Notification } from '../../../typesGitHub'; import { repositoryVulnerabilityAlertHandler } from './repositoryVulnerabilityAlert'; describe('renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.ts', () => { @@ -11,4 +13,17 @@ describe('renderer/utils/notifications/handlers/repositoryVulnerabilityAlert.ts' ).displayName, ).toBe('AlertIcon'); }); + + it('defaultUrl', () => { + const mockHtmlUrl = + 'https://github.com/gitify-app/notifications-test' as Link; + + expect( + repositoryVulnerabilityAlertHandler.defaultUrl({ + repository: { + html_url: mockHtmlUrl, + }, + } as Notification), + ).toEqual(mockHtmlUrl); + }); }); diff --git a/src/renderer/utils/notifications/handlers/workflowRun.test.ts b/src/renderer/utils/notifications/handlers/workflowRun.test.ts index 7d95c4b41..b5c156a9d 100644 --- a/src/renderer/utils/notifications/handlers/workflowRun.test.ts +++ b/src/renderer/utils/notifications/handlers/workflowRun.test.ts @@ -3,6 +3,8 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; +import type { Link } from '../../../types'; +import type { Notification } from '../../../typesGitHub'; import { getWorkflowRunAttributes, workflowRunHandler } from './workflowRun'; describe('renderer/utils/notifications/handlers/workflowRun.ts', () => { @@ -19,8 +21,10 @@ describe('renderer/utils/notifications/handlers/workflowRun.ts', () => { ); expect(result).toEqual({ - state: 'waiting', + state: 'WAITING', user: null, + htmlUrl: + 'https://github.com/gitify-app/notifications-test/actions?query=is%3AWAITING', }); }); @@ -64,6 +68,22 @@ describe('renderer/utils/notifications/handlers/workflowRun.ts', () => { ).toBe('RocketIcon'); }); + it('defaultUrl', () => { + const mockHtmlUrl = + 'https://github.com/gitify-app/notifications-test' as Link; + + expect( + workflowRunHandler.defaultUrl({ + subject: { + title: 'Some notification', + }, + repository: { + html_url: mockHtmlUrl, + }, + } as Notification), + ).toEqual(`${mockHtmlUrl}/actions`); + }); + describe('getWorkflowRunAttributes', () => { it('deploy review workflow run state', async () => { const mockNotification = createPartialMockNotification({ @@ -74,7 +94,7 @@ describe('renderer/utils/notifications/handlers/workflowRun.ts', () => { const result = getWorkflowRunAttributes(mockNotification); expect(result).toEqual({ - status: 'waiting', + status: 'WAITING', statusDisplayName: 'review', user: 'some-user', }); diff --git a/src/renderer/utils/notifications/handlers/workflowRun.ts b/src/renderer/utils/notifications/handlers/workflowRun.ts index 5b10ae2df..2eaa8f1b0 100644 --- a/src/renderer/utils/notifications/handlers/workflowRun.ts +++ b/src/renderer/utils/notifications/handlers/workflowRun.ts @@ -44,9 +44,7 @@ class WorkflowRunHandler extends DefaultHandler { } defaultUrl(notification: Notification): Link { - const url = new URL(notification.repository.html_url); - url.pathname += '/actions'; - return url.href as Link; + return getWorkflowRunUrl(notification); } } diff --git a/src/renderer/utils/notifications/notifications.test.ts b/src/renderer/utils/notifications/notifications.test.ts index e3205c954..7ee1e2c8c 100644 --- a/src/renderer/utils/notifications/notifications.test.ts +++ b/src/renderer/utils/notifications/notifications.test.ts @@ -64,13 +64,15 @@ describe('renderer/utils/notifications/notifications.ts', () => { url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link, }); const mockRepository = { + name: 'notifications-test', full_name: 'gitify-app/notifications-test', + owner: { + login: 'gitify-app', + }, } as Repository; mockNotification.repository = mockRepository; - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/1') - .replyWithError(mockError); + nock('https://api.github.com').post('/graphql').replyWithError(mockError); await enrichNotification(mockNotification, mockSettings); From 7f17bb06c7666d027885fc287b05f5de420ef778 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 21:41:06 +1000 Subject: [PATCH 11/25] refactor: tests Signed-off-by: Adam Setch --- .../notifications/handlers/issue.test.ts | 73 ++-- .../handlers/pullRequest.test.ts | 311 ++++++++++++++---- 2 files changed, 283 insertions(+), 101 deletions(-) diff --git a/src/renderer/utils/notifications/handlers/issue.test.ts b/src/renderer/utils/notifications/handlers/issue.test.ts index 4b20f1665..5deb80518 100644 --- a/src/renderer/utils/notifications/handlers/issue.test.ts +++ b/src/renderer/utils/notifications/handlers/issue.test.ts @@ -9,6 +9,7 @@ import { mockSettings } from '../../../__mocks__/state-mocks'; import { createPartialMockUser } from '../../../__mocks__/user-mocks'; import type { Link } from '../../../types'; import type { Notification } from '../../../typesGitHub'; +import type { FetchIssueByNumberQuery } from '../../api/graphql/generated/graphql'; import { issueHandler } from './issue'; describe('renderer/utils/notifications/handlers/issue.ts', () => { @@ -34,58 +35,74 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { it('open issue state', async () => { nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/1') + .post('/graphql') .reply(200, { - number: 123, - state: 'open', - user: mockAuthor, - labels: [], + data: { + repository: { + issue: { + __typename: 'Issue', + number: 123, + title: 'PR Title', + state: 'OPEN', + url: 'https://github.com/gitify-app/noticiation-test/issues/123', + author: mockAuthor, + labels: { nodes: [] }, + comments: { totalCount: 0, nodes: [] }, + } as FetchIssueByNumberQuery['repository']['issue'], + }, + }, }); - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/comments/302888448') - .reply(200, { user: mockCommenter }); - const result = await issueHandler.enrich(mockNotification, mockSettings); expect(result).toEqual({ number: 123, - state: 'open', + state: 'OPEN', user: { - login: mockCommenter.login, - html_url: mockCommenter.html_url, - avatar_url: mockCommenter.avatar_url, - type: mockCommenter.type, + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, }, + comments: 0, + htmlUrl: 'https://github.com/gitify-app/noticiation-test/issues/123', labels: [], }); }); it('closed issue state', async () => { nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/1') + .post('/graphql') .reply(200, { - number: 123, - state: 'closed', - user: mockAuthor, - labels: [], + data: { + repository: { + issue: { + __typename: 'Issue', + number: 123, + title: 'PR Title', + state: 'CLOSED', + url: 'https://github.com/gitify-app/noticiation-test/issues/123', + author: mockAuthor, + labels: { nodes: [] }, + comments: { totalCount: 0, nodes: [] }, + } as FetchIssueByNumberQuery['repository']['issue'], + }, + }, }); - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/comments/302888448') - .reply(200, { user: mockCommenter }); - const result = await issueHandler.enrich(mockNotification, mockSettings); expect(result).toEqual({ number: 123, - state: 'closed', + state: 'CLOSED', user: { - login: mockCommenter.login, - html_url: mockCommenter.html_url, - avatar_url: mockCommenter.avatar_url, - type: mockCommenter.type, + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, }, + comments: 0, + htmlUrl: 'https://github.com/gitify-app/noticiation-test/issues/123', labels: [], }); }); diff --git a/src/renderer/utils/notifications/handlers/pullRequest.test.ts b/src/renderer/utils/notifications/handlers/pullRequest.test.ts index d4e88aa74..73b454e43 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.test.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.test.ts @@ -38,6 +38,59 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { axios.defaults.adapter = 'http'; }); + it('open pull request state', async () => { + nock('https://api.github.com') + .post('/graphql') + .reply(200, { + data: { + repository: { + pullRequest: { + __typename: 'PullRequest', + number: 123, + title: 'Test PR', + state: 'OPEN', + isDraft: false, + merged: false, + isInMergeQueue: false, + url: 'https://github.com/gitify-app/noticiation-test/pulls/123', + author: mockAuthor, + labels: null, + comments: { + totalCount: 0, + nodes: [], + }, + reviews: { + totalCount: 0, + nodes: [], + }, + } as FetchPullByNumberQuery['repository']['pullRequest'], + }, + }, + }); + + const result = await pullRequestHandler.enrich( + mockNotification, + mockSettings, + ); + + expect(result).toEqual({ + number: 123, + state: 'OPEN', + user: { + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, + }, + reviews: null, + labels: [], + linkedIssues: [], + comments: 0, + milestone: null, + htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', + }); + }); + it('closed pull request state', async () => { nock('https://api.github.com') .post('/graphql') @@ -52,19 +105,17 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { isDraft: false, merged: false, isInMergeQueue: false, - url: 'https://github.com/gitify-app/noticiation-test/pulls/1', - user: mockAuthor, + url: 'https://github.com/gitify-app/noticiation-test/pulls/123', + author: mockAuthor, labels: null, comments: { - totalCount: 1, - nodes: [ - { - author: mockCommenter, - url: 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', - }, - ], + totalCount: 0, + nodes: [], + }, + reviews: { + totalCount: 0, + nodes: [], }, - reviews: null, } as FetchPullByNumberQuery['repository']['pullRequest'], }, }, @@ -79,18 +130,17 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { number: 123, state: 'CLOSED', user: { - login: mockCommenter.login, - html_url: mockCommenter.html_url, - avatar_url: mockCommenter.avatar_url, - type: mockCommenter.type, + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, }, reviews: null, labels: [], linkedIssues: [], - comments: 1, + comments: 0, milestone: null, - htmlUrl: - 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', + htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', }); }); @@ -108,19 +158,17 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { isDraft: true, merged: false, isInMergeQueue: false, - url: 'https://github.com/gitify-app/noticiation-test/pulls/1', - user: mockAuthor, + url: 'https://github.com/gitify-app/noticiation-test/pulls/123', + author: mockAuthor, labels: null, comments: { - totalCount: 1, - nodes: [ - { - author: mockCommenter, - url: 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', - }, - ], + totalCount: 0, + nodes: [], + }, + reviews: { + totalCount: 0, + nodes: [], }, - reviews: null, } as FetchPullByNumberQuery['repository']['pullRequest'], }, }, @@ -135,18 +183,17 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { number: 123, state: 'DRAFT', user: { - login: mockCommenter.login, - html_url: mockCommenter.html_url, - avatar_url: mockCommenter.avatar_url, - type: mockCommenter.type, + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, }, reviews: null, labels: [], linkedIssues: [], - comments: 1, + comments: 0, milestone: null, - htmlUrl: - 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', + htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', }); }); @@ -164,19 +211,17 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { isDraft: false, merged: true, isInMergeQueue: false, - url: 'https://github.com/gitify-app/noticiation-test/pulls/1', - user: mockAuthor, + url: 'https://github.com/gitify-app/noticiation-test/pulls/123', + author: mockAuthor, labels: null, comments: { - totalCount: 1, - nodes: [ - { - author: mockCommenter, - url: 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', - }, - ], + totalCount: 0, + nodes: [], + }, + reviews: { + totalCount: 0, + nodes: [], }, - reviews: null, } as FetchPullByNumberQuery['repository']['pullRequest'], }, }, @@ -191,22 +236,21 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { number: 123, state: 'MERGED', user: { - login: mockCommenter.login, - html_url: mockCommenter.html_url, - avatar_url: mockCommenter.avatar_url, - type: mockCommenter.type, + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, }, reviews: null, labels: [], linkedIssues: [], - comments: 1, + comments: 0, milestone: null, - htmlUrl: - 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', + htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', }); }); - it('open pull request state', async () => { + it('with comments', async () => { nock('https://api.github.com') .post('/graphql') .reply(200, { @@ -220,19 +264,24 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { isDraft: false, merged: false, isInMergeQueue: false, - url: 'https://github.com/gitify-app/noticiation-test/pulls/1', - user: mockAuthor, - labels: null, + url: 'https://github.com/gitify-app/noticiation-test/pulls/123', + author: mockAuthor, + labels: { + nodes: [{ name: 'enhancement' }], + }, comments: { totalCount: 1, nodes: [ { author: mockCommenter, - url: 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', + url: 'https://github.com/gitify-app/noticiation-test/pulls/123#issuecomment-1234', }, ], }, - reviews: null, + reviews: { + totalCount: 0, + nodes: [], + }, } as FetchPullByNumberQuery['repository']['pullRequest'], }, }, @@ -253,12 +302,12 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { type: mockCommenter.type, }, reviews: null, - labels: [], + labels: ['enhancement'], linkedIssues: [], comments: 1, milestone: null, htmlUrl: - 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', + 'https://github.com/gitify-app/noticiation-test/pulls/123#issuecomment-1234', }); }); @@ -276,21 +325,79 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { isDraft: false, merged: false, isInMergeQueue: false, - url: 'https://github.com/gitify-app/noticiation-test/pulls/1', - user: mockAuthor, + url: 'https://github.com/gitify-app/noticiation-test/pulls/123', + author: mockAuthor, labels: { nodes: [{ name: 'enhancement' }], }, comments: { - totalCount: 1, + totalCount: 0, + nodes: [], + }, + reviews: { + totalCount: 0, + nodes: [], + }, + } as FetchPullByNumberQuery['repository']['pullRequest'], + }, + }, + }); + + const result = await pullRequestHandler.enrich( + mockNotification, + mockSettings, + ); + + expect(result).toEqual({ + number: 123, + state: 'OPEN', + user: { + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, + }, + reviews: null, + labels: ['enhancement'], + linkedIssues: [], + comments: 0, + milestone: null, + htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', + }); + }); + + it('with linked issues', async () => { + nock('https://api.github.com') + .post('/graphql') + .reply(200, { + data: { + repository: { + pullRequest: { + __typename: 'PullRequest', + number: 123, + title: 'Test PR', + state: 'OPEN', + isDraft: false, + merged: false, + isInMergeQueue: false, + url: 'https://github.com/gitify-app/noticiation-test/pulls/123', + author: mockAuthor, + labels: null, + comments: { + totalCount: 0, + nodes: [], + }, + reviews: { + totalCount: 0, + nodes: [], + }, + closingIssuesReferences: { nodes: [ { - author: mockCommenter, - url: 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', + number: 789, }, ], }, - reviews: null, } as FetchPullByNumberQuery['repository']['pullRequest'], }, }, @@ -305,18 +412,76 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { number: 123, state: 'OPEN', user: { - login: mockCommenter.login, - html_url: mockCommenter.html_url, - avatar_url: mockCommenter.avatar_url, - type: mockCommenter.type, + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, }, reviews: null, - labels: ['enhancement'], + labels: [], + linkedIssues: ['#789'], + comments: 0, + milestone: null, + htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', + }); + }); + + it('with milestone', async () => { + nock('https://api.github.com') + .post('/graphql') + .reply(200, { + data: { + repository: { + pullRequest: { + __typename: 'PullRequest', + number: 123, + title: 'Test PR', + state: 'OPEN', + isDraft: false, + merged: false, + isInMergeQueue: false, + url: 'https://github.com/gitify-app/noticiation-test/pulls/123', + author: mockAuthor, + labels: { + nodes: [], + }, + comments: { + totalCount: 0, + nodes: [], + }, + reviews: { + totalCount: 0, + nodes: [], + }, + milestone: { + state: 'OPEN', + title: 'Open Milestone', + }, + } as FetchPullByNumberQuery['repository']['pullRequest'], + }, + }, + }); + + const result = await pullRequestHandler.enrich( + mockNotification, + mockSettings, + ); + + expect(result).toEqual({ + number: 123, + state: 'OPEN', + user: { + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, + }, + reviews: null, + labels: [], linkedIssues: [], - comments: 1, + comments: 0, milestone: null, - htmlUrl: - 'https://github.com/gitify-app/noticiation-test/pulls/1#issuecomment-1234', + htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', }); }); }); From 6edeb5cdb671096e4ebebf0c59fe202dc81cc4b3 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 22:16:44 +1000 Subject: [PATCH 12/25] refactor: tests Signed-off-by: Adam Setch --- src/renderer/utils/api/client.ts | 12 +- .../utils/api/graphql/generated/gql.ts | 12 +- .../utils/api/graphql/generated/graphql.ts | 24 +- src/renderer/utils/api/graphql/issue.graphql | 3 +- src/renderer/utils/api/graphql/pull.graphql | 11 +- src/renderer/utils/helpers.test.ts | 916 +++++++++--------- .../notifications/handlers/discussion.test.ts | 43 +- .../notifications/handlers/issue.test.ts | 339 ++++--- .../handlers/pullRequest.test.ts | 72 +- .../notifications/handlers/pullRequest.ts | 6 +- 10 files changed, 774 insertions(+), 664 deletions(-) diff --git a/src/renderer/utils/api/client.ts b/src/renderer/utils/api/client.ts index b8fc343d3..46761fed4 100644 --- a/src/renderer/utils/api/client.ts +++ b/src/renderer/utils/api/client.ts @@ -23,8 +23,8 @@ import { type FetchDiscussionByNumberQuery, FetchIssueByNumberDocument, type FetchIssueByNumberQuery, - FetchPullByNumberDocument, - type FetchPullByNumberQuery, + FetchPullRequestByNumberDocument, + type FetchPullRequestByNumberQuery, } from './graphql/generated/graphql'; import { apiRequestAuth, performGraphQLRequest } from './request'; import { @@ -205,6 +205,7 @@ export async function fetchIssueByNumber( name: notification.repository.name, number: number, firstLabels: 100, + lastComments: 1, }, ); } @@ -214,19 +215,22 @@ export async function fetchIssueByNumber( */ export async function fetchPullByNumber( notification: Notification, -): Promise> { +): Promise> { const url = getGitHubGraphQLUrl(notification.account.hostname); const number = getNumberFromUrl(notification.subject.url); return performGraphQLRequest( url.toString() as Link, notification.account.token, - FetchPullByNumberDocument, + FetchPullRequestByNumberDocument, { owner: notification.repository.owner.login, name: notification.repository.name, number: number, firstLabels: 100, + firstClosingIssues: 100, + lastComments: 1, + lastReviews: 100, }, ); } diff --git a/src/renderer/utils/api/graphql/generated/gql.ts b/src/renderer/utils/api/graphql/generated/gql.ts index 71eedbd4e..a3a5c4b74 100644 --- a/src/renderer/utils/api/graphql/generated/gql.ts +++ b/src/renderer/utils/api/graphql/generated/gql.ts @@ -17,14 +17,14 @@ import * as types from './graphql'; type Documents = { "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}": typeof types.AuthorFieldsFragmentDoc, "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}": typeof types.FetchDiscussionByNumberDocument, - "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchIssueByNumberDocument, - "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 100) {\n totalCount\n nodes {\n state\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchPullByNumberDocument, + "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchIssueByNumberDocument, + "query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: $lastReviews) {\n totalCount\n nodes {\n state\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: $firstClosingIssues) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchPullRequestByNumberDocument, }; const documents: Documents = { "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}": types.AuthorFieldsFragmentDoc, "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}": types.FetchDiscussionByNumberDocument, - "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchIssueByNumberDocument, - "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 100) {\n totalCount\n nodes {\n state\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchPullByNumberDocument, + "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchIssueByNumberDocument, + "query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: $lastReviews) {\n totalCount\n nodes {\n state\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: $firstClosingIssues) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchPullRequestByNumberDocument, }; /** @@ -38,11 +38,11 @@ export function graphql(source: "query FetchDiscussionByNumber($owner: String!, /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').FetchIssueByNumberDocument; +export function graphql(source: "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').FetchIssueByNumberDocument; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: 1) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: 100) {\n totalCount\n nodes {\n state\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: 50) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').FetchPullByNumberDocument; +export function graphql(source: "query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: $lastReviews) {\n totalCount\n nodes {\n state\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: $firstClosingIssues) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').FetchPullRequestByNumberDocument; export function graphql(source: string) { diff --git a/src/renderer/utils/api/graphql/generated/graphql.ts b/src/renderer/utils/api/graphql/generated/graphql.ts index e95f72825..679ea040b 100644 --- a/src/renderer/utils/api/graphql/generated/graphql.ts +++ b/src/renderer/utils/api/graphql/generated/graphql.ts @@ -35955,6 +35955,7 @@ export type FetchIssueByNumberQueryVariables = Exact<{ owner: Scalars['String']['input']; name: Scalars['String']['input']; number: Scalars['Int']['input']; + lastComments?: InputMaybe; firstLabels?: InputMaybe; }>; @@ -35975,15 +35976,18 @@ export type FetchIssueByNumberQuery = { __typename?: 'Query', repository?: { __t export type MilestoneFieldsFragment = { __typename?: 'Milestone', state: MilestoneState, title: string }; -export type FetchPullByNumberQueryVariables = Exact<{ +export type FetchPullRequestByNumberQueryVariables = Exact<{ owner: Scalars['String']['input']; name: Scalars['String']['input']; number: Scalars['Int']['input']; firstLabels?: InputMaybe; + lastComments?: InputMaybe; + lastReviews?: InputMaybe; + firstClosingIssues?: InputMaybe; }>; -export type FetchPullByNumberQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', pullRequest?: { __typename: 'PullRequest', number: number, title: string, url: any, state: PullRequestState, merged: boolean, isDraft: boolean, isInMergeQueue: boolean, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: +export type FetchPullRequestByNumberQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', pullRequest?: { __typename: 'PullRequest', number: number, title: string, url: any, state: PullRequestState, merged: boolean, isDraft: boolean, isInMergeQueue: boolean, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } @@ -36097,7 +36101,7 @@ fragment CommentFields on DiscussionComment { url }`) as unknown as TypedDocumentString; export const FetchIssueByNumberDocument = new TypedDocumentString(` - query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) { + query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) { repository(owner: $owner, name: $name) { issue(number: $number) { __typename @@ -36112,7 +36116,7 @@ export const FetchIssueByNumberDocument = new TypedDocumentString(` author { ...AuthorFields } - comments(last: 1) { + comments(last: $lastComments) { totalCount nodes { url @@ -36139,8 +36143,8 @@ fragment MilestoneFields on Milestone { state title }`) as unknown as TypedDocumentString; -export const FetchPullByNumberDocument = new TypedDocumentString(` - query FetchPullByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int) { +export const FetchPullRequestByNumberDocument = new TypedDocumentString(` + query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) { repository(owner: $owner, name: $name) { pullRequest(number: $number) { __typename @@ -36157,7 +36161,7 @@ export const FetchPullByNumberDocument = new TypedDocumentString(` author { ...AuthorFields } - comments(last: 1) { + comments(last: $lastComments) { totalCount nodes { url @@ -36166,7 +36170,7 @@ export const FetchPullByNumberDocument = new TypedDocumentString(` } } } - reviews(last: 100) { + reviews(last: $lastReviews) { totalCount nodes { state @@ -36180,7 +36184,7 @@ export const FetchPullByNumberDocument = new TypedDocumentString(` name } } - closingIssuesReferences(first: 50) { + closingIssuesReferences(first: $firstClosingIssues) { nodes { number } @@ -36197,4 +36201,4 @@ export const FetchPullByNumberDocument = new TypedDocumentString(` fragment MilestoneFields on Milestone { state title -}`) as unknown as TypedDocumentString; \ No newline at end of file +}`) as unknown as TypedDocumentString; \ No newline at end of file diff --git a/src/renderer/utils/api/graphql/issue.graphql b/src/renderer/utils/api/graphql/issue.graphql index f042b0c4e..63af6ec18 100644 --- a/src/renderer/utils/api/graphql/issue.graphql +++ b/src/renderer/utils/api/graphql/issue.graphql @@ -2,6 +2,7 @@ query FetchIssueByNumber( $owner: String! $name: String! $number: Int! + $lastComments: Int $firstLabels: Int ) { repository(owner: $owner, name: $name) { @@ -18,7 +19,7 @@ query FetchIssueByNumber( author { ...AuthorFields } - comments(last: 1) { + comments(last: $lastComments) { totalCount nodes { url diff --git a/src/renderer/utils/api/graphql/pull.graphql b/src/renderer/utils/api/graphql/pull.graphql index acb0042b1..fa33c9f0c 100644 --- a/src/renderer/utils/api/graphql/pull.graphql +++ b/src/renderer/utils/api/graphql/pull.graphql @@ -1,8 +1,11 @@ -query FetchPullByNumber( +query FetchPullRequestByNumber( $owner: String! $name: String! $number: Int! $firstLabels: Int + $lastComments: Int + $lastReviews: Int + $firstClosingIssues: Int ) { repository(owner: $owner, name: $name) { pullRequest(number: $number) { @@ -20,7 +23,7 @@ query FetchPullByNumber( author { ...AuthorFields } - comments(last: 1) { + comments(last: $lastComments) { totalCount nodes { url @@ -29,7 +32,7 @@ query FetchPullByNumber( } } } - reviews(last: 100) { + reviews(last: $lastReviews) { totalCount nodes { state @@ -43,7 +46,7 @@ query FetchPullByNumber( name } } - closingIssuesReferences(first: 50) { + closingIssuesReferences(first: $firstClosingIssues) { nodes { number } diff --git a/src/renderer/utils/helpers.test.ts b/src/renderer/utils/helpers.test.ts index 07878688f..5a01bff82 100644 --- a/src/renderer/utils/helpers.test.ts +++ b/src/renderer/utils/helpers.test.ts @@ -4,17 +4,11 @@ import { ChevronRightIcon, } from '@primer/octicons-react'; -import type { ExecutionResult } from 'graphql'; - +import { mockToken } from '../__mocks__/state-mocks'; import type { Hostname, Link } from '../types'; import type { Subject, SubjectType } from '../typesGitHub'; -import * as logger from '../utils/logger'; -import { - mockDiscussionByNumberGraphQLResponse, - mockSingleNotification, -} from './api/__mocks__/response-mocks'; +import { mockSingleNotification } from './api/__mocks__/response-mocks'; import * as apiClient from './api/client'; -import type { FetchDiscussionByNumberQuery } from './api/graphql/generated/graphql'; import { generateGitHubWebUrl, generateNotificationReferrerId, @@ -82,15 +76,18 @@ describe('renderer/utils/helpers.ts', () => { }); it('Subject HTML URL: prefer if available from enrichment stage', async () => { - const mockHtmlUrl = 'https://gitify.io/' as Link; + const mockSubjectHtmlUrl = 'https://gitify.io/' as Link; + const mockSubjectUrl = + 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link; + const mockLatestCommentUrl = + 'https://api.github.com/repos/gitify-app/notifications-test/issues/comments/302888448' as Link; const subject = { title: 'generate github web url unit tests', - url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link, - latest_comment_url: - 'https://api.github.com/repos/gitify-app/notifications-test/issues/comments/302888448' as Link, + url: mockSubjectUrl, + latest_comment_url: mockLatestCommentUrl, type: 'Issue' as SubjectType, - htmlUrl: mockHtmlUrl, + htmlUrl: mockSubjectHtmlUrl, } as Subject; const result = await generateGitHubWebUrl({ @@ -99,16 +96,22 @@ describe('renderer/utils/helpers.ts', () => { }); expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - expect(result).toBe(`${mockHtmlUrl}?${mockNotificationReferrer}`); + expect(result).toBe(`${mockSubjectHtmlUrl}?${mockNotificationReferrer}`); }); it('Subject Latest Comment Url: when not null, fetch latest comment html url', async () => { + const mockSubjectHtmlUrl = null; + const mockSubjectUrl = + 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link; + const mockLatestCommentUrl = + 'https://api.github.com/repos/gitify-app/notifications-test/issues/comments/302888448' as Link; + const subject = { title: 'generate github web url unit tests', - url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link, - latest_comment_url: - 'https://api.github.com/repos/gitify-app/notifications-test/issues/comments/302888448' as Link, + url: mockSubjectUrl, + latest_comment_url: mockLatestCommentUrl, type: 'Issue' as SubjectType, + htmlUrl: mockSubjectHtmlUrl, } as Subject; getHtmlUrlSpy.mockResolvedValue(mockHtmlUrl); @@ -119,15 +122,25 @@ describe('renderer/utils/helpers.ts', () => { }); expect(getHtmlUrlSpy).toHaveBeenCalledTimes(1); + expect(getHtmlUrlSpy).toHaveBeenCalledWith( + mockLatestCommentUrl, + mockToken, + ); expect(result).toBe(`${mockHtmlUrl}?${mockNotificationReferrer}`); }); it('Subject Url: when no latest comment url available, fetch subject html url', async () => { + const mockSubjectHtmlUrl = null; + const mockSubjectUrl = + 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link; + const mockLatestCommentUrl = null; + const subject = { title: 'generate github web url unit tests', - url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link, - latest_comment_url: null, + url: mockSubjectUrl, + latest_comment_url: mockLatestCommentUrl, type: 'Issue' as SubjectType, + htmlUrl: mockSubjectHtmlUrl, } as Subject; getHtmlUrlSpy.mockResolvedValue(mockHtmlUrl); @@ -138,441 +151,442 @@ describe('renderer/utils/helpers.ts', () => { }); expect(getHtmlUrlSpy).toHaveBeenCalledTimes(1); + expect(getHtmlUrlSpy).toHaveBeenCalledWith(mockSubjectUrl, mockToken); expect(result).toBe(`${mockHtmlUrl}?${mockNotificationReferrer}`); }); - describe('Check Suite URLs', () => { - it('successful workflow', async () => { - const subject = { - title: 'Demo workflow run succeeded for main branch', - url: null, - latest_comment_url: null, - type: 'CheckSuite' as SubjectType, - } as Subject; - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3ASUCCESS+branch%3Amain&${mockNotificationReferrer}`, - ); - }); - - it('failed workflow', async () => { - const subject = { - title: 'Demo workflow run failed for main branch', - url: null, - latest_comment_url: null, - type: 'CheckSuite' as SubjectType, - } as Subject; - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3AFAILURE+branch%3Amain&${mockNotificationReferrer}`, - ); - }); - - it('failed workflow multiple attempts', async () => { - const subject = { - title: 'Demo workflow run, Attempt #3 failed for main branch', - url: null, - latest_comment_url: null, - type: 'CheckSuite' as SubjectType, - } as Subject; - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3AFAILURE+branch%3Amain&${mockNotificationReferrer}`, - ); - }); - - it('skipped workflow', async () => { - const subject = { - title: 'Demo workflow run skipped for main branch', - url: null, - latest_comment_url: null, - type: 'CheckSuite' as SubjectType, - } as Subject; - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3ASKIPPED+branch%3Amain&${mockNotificationReferrer}`, - ); - }); - - it('unhandled workflow scenario', async () => { - const subject = { - title: 'unhandled workflow scenario', - url: null, - latest_comment_url: null, - type: 'CheckSuite' as SubjectType, - } as Subject; - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/actions?${mockNotificationReferrer}`, - ); - }); - - it('unhandled status scenario', async () => { - const subject = { - title: 'Demo workflow run unhandled-status for main branch', - url: null, - latest_comment_url: null, - type: 'CheckSuite' as SubjectType, - } as Subject; - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+branch%3Amain&${mockNotificationReferrer}`, - ); - }); - - it('unhandled check suite scenario', async () => { - const subject = { - title: 'Unhandled scenario', - url: null, - latest_comment_url: null, - type: 'CheckSuite' as SubjectType, - } as Subject; - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/actions?${mockNotificationReferrer}`, - ); - }); - }); - - describe('Discussions URLs', () => { - const fetchDiscussionByNumberSpy = jest.spyOn( - apiClient, - 'fetchDiscussionByNumber', - ); - - it('when no discussion found via graphql api, default to base repository discussion url', async () => { - const subject = { - title: 'generate github web url unit tests', - url: null, - latest_comment_url: null, - type: 'Discussion' as SubjectType, - } as Subject; - - fetchDiscussionByNumberSpy.mockResolvedValue({ - data: { repository: { discussion: null } }, - } as any); - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(fetchDiscussionByNumberSpy).toHaveBeenCalledTimes(1); - expect(result).toBe( - `${mockSingleNotification.repository.html_url}/discussions?${mockNotificationReferrer}`, - ); - }); - - it('when error fetching discussion via graphql api, default to base repository discussion url', async () => { - const rendererLogErrorSpy = jest - .spyOn(logger, 'rendererLogError') - .mockImplementation(); - - const subject = { - title: '1.16.0', - url: null, - latest_comment_url: null, - type: 'Discussion' as SubjectType, - } as Subject; - - fetchDiscussionByNumberSpy.mockResolvedValue({ - data: null, - errors: [ - { - message: 'Something failed', - }, - ], - } as unknown as ExecutionResult); - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(fetchDiscussionByNumberSpy).toHaveBeenCalledTimes(1); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/discussions?${mockNotificationReferrer}`, - ); - expect(rendererLogErrorSpy).toHaveBeenCalledTimes(1); - }); - - it('when discussion found via graphql api, link to matching discussion and comment hash', async () => { - const subject = { - title: '1.16.0', - url: null, - latest_comment_url: null, - type: 'Discussion' as SubjectType, - } as Subject; - - fetchDiscussionByNumberSpy.mockResolvedValue({ - data: mockDiscussionByNumberGraphQLResponse, - } as ExecutionResult); - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(fetchDiscussionByNumberSpy).toHaveBeenCalledTimes(1); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/discussions/612?${mockNotificationReferrer}#discussioncomment-2300902`, - ); - }); - - it('when api throws error, default to base repository discussion url', async () => { - const rendererLogErrorSpy = jest - .spyOn(logger, 'rendererLogError') - .mockImplementation(); - - const subject = { - title: '1.16.0', - url: null, - latest_comment_url: null, - type: 'Discussion' as SubjectType, - } as Subject; - - fetchDiscussionByNumberSpy.mockRejectedValue( - new Error('Something failed'), - ); - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(fetchDiscussionByNumberSpy).toHaveBeenCalledTimes(1); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/discussions?${mockNotificationReferrer}`, - ); - expect(rendererLogErrorSpy).toHaveBeenCalledTimes(1); - }); - }); - - it('Repository Invitation url', async () => { - const subject = { - title: - 'Invitation to join gitify-app/notifications-test from unit-tests', - url: null, - latest_comment_url: null, - type: 'RepositoryInvitation' as SubjectType, - } as Subject; - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/invitations?${mockNotificationReferrer}`, - ); - }); - - it('Repository Dependabot Alerts Thread url', async () => { - const subject = { - title: 'Your repository has dependencies with security vulnerabilities', - url: null, - latest_comment_url: null, - type: 'RepositoryDependabotAlertsThread' as SubjectType, - }; - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/security/dependabot?${mockNotificationReferrer}`, - ); - }); - - describe('Workflow Run URLs', () => { - it('approval requested', async () => { - const subject = { - title: 'some-user requested your review to deploy to an environment', - url: null, - latest_comment_url: null, - type: 'WorkflowRun' as SubjectType, - } as Subject; - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/actions?query=is%3AWAITING&${mockNotificationReferrer}`, - ); - }); - - it('unhandled status/action scenario', async () => { - const subject = { - title: - 'some-user requested your unhandled-action to deploy to an environment', - url: null, - latest_comment_url: null, - type: 'WorkflowRun' as SubjectType, - } as Subject; - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/actions?${mockNotificationReferrer}`, - ); - }); - - it('unhandled workflow scenario', async () => { - const subject = { - title: 'some unhandled scenario', - url: null, - latest_comment_url: null, - type: 'WorkflowRun' as SubjectType, - } as Subject; - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/actions?${mockNotificationReferrer}`, - ); - }); - }); - - describe('defaults web urls', () => { - it('issues - defaults when no urls present in notification', async () => { - const subject = { - title: 'generate github web url unit tests', - url: null, - latest_comment_url: null, - type: 'Issue' as SubjectType, - } as Subject; - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/issues?${mockNotificationReferrer}`, - ); - }); - - it('pull requests - defaults when no urls present in notification', async () => { - const subject = { - title: 'generate github web url unit tests', - url: null, - latest_comment_url: null, - type: 'PullRequest' as SubjectType, - } as Subject; - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/pulls?${mockNotificationReferrer}`, - ); - }); - - it('other - defaults when no urls present in notification', async () => { - const subject = { - title: 'generate github web url unit tests', - url: null, - latest_comment_url: null, - type: 'Commit' as SubjectType, - } as Subject; - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test?${mockNotificationReferrer}`, - ); - }); - - it('defaults when exception handled during specialized html enrichment process', async () => { - const rendererLogErrorSpy = jest - .spyOn(logger, 'rendererLogError') - .mockImplementation(); - - const subject = { - title: 'generate github web url unit tests', - url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link, - latest_comment_url: null as Link, - type: 'Issue' as SubjectType, - } as Subject; - - getHtmlUrlSpy.mockRejectedValue(new Error('Test error')); - - const result = await generateGitHubWebUrl({ - ...mockSingleNotification, - subject: subject, - }); - - expect(getHtmlUrlSpy).toHaveBeenCalledTimes(1); - expect(result).toBe( - `https://github.com/gitify-app/notifications-test/issues?${mockNotificationReferrer}`, - ); - expect(rendererLogErrorSpy).toHaveBeenCalledTimes(1); - }); - }); + // describe('Check Suite URLs', () => { + // it('successful workflow', async () => { + // const subject = { + // title: 'Demo workflow run succeeded for main branch', + // url: null, + // latest_comment_url: null, + // type: 'CheckSuite' as SubjectType, + // } as Subject; + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3ASUCCESS+branch%3Amain&${mockNotificationReferrer}`, + // ); + // }); + + // it('failed workflow', async () => { + // const subject = { + // title: 'Demo workflow run failed for main branch', + // url: null, + // latest_comment_url: null, + // type: 'CheckSuite' as SubjectType, + // } as Subject; + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3AFAILURE+branch%3Amain&${mockNotificationReferrer}`, + // ); + // }); + + // it('failed workflow multiple attempts', async () => { + // const subject = { + // title: 'Demo workflow run, Attempt #3 failed for main branch', + // url: null, + // latest_comment_url: null, + // type: 'CheckSuite' as SubjectType, + // } as Subject; + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3AFAILURE+branch%3Amain&${mockNotificationReferrer}`, + // ); + // }); + + // it('skipped workflow', async () => { + // const subject = { + // title: 'Demo workflow run skipped for main branch', + // url: null, + // latest_comment_url: null, + // type: 'CheckSuite' as SubjectType, + // } as Subject; + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3ASKIPPED+branch%3Amain&${mockNotificationReferrer}`, + // ); + // }); + + // it('unhandled workflow scenario', async () => { + // const subject = { + // title: 'unhandled workflow scenario', + // url: null, + // latest_comment_url: null, + // type: 'CheckSuite' as SubjectType, + // } as Subject; + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/actions?${mockNotificationReferrer}`, + // ); + // }); + + // it('unhandled status scenario', async () => { + // const subject = { + // title: 'Demo workflow run unhandled-status for main branch', + // url: null, + // latest_comment_url: null, + // type: 'CheckSuite' as SubjectType, + // } as Subject; + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+branch%3Amain&${mockNotificationReferrer}`, + // ); + // }); + + // it('unhandled check suite scenario', async () => { + // const subject = { + // title: 'Unhandled scenario', + // url: null, + // latest_comment_url: null, + // type: 'CheckSuite' as SubjectType, + // } as Subject; + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/actions?${mockNotificationReferrer}`, + // ); + // }); + // }); + + // describe('Discussions URLs', () => { + // const fetchDiscussionByNumberSpy = jest.spyOn( + // apiClient, + // 'fetchDiscussionByNumber', + // ); + + // it('when no discussion found via graphql api, default to base repository discussion url', async () => { + // const subject = { + // title: 'generate github web url unit tests', + // url: null, + // latest_comment_url: null, + // type: 'Discussion' as SubjectType, + // } as Subject; + + // fetchDiscussionByNumberSpy.mockResolvedValue({ + // data: { repository: { discussion: null } }, + // } as any); + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(fetchDiscussionByNumberSpy).toHaveBeenCalledTimes(1); + // expect(result).toBe( + // `${mockSingleNotification.repository.html_url}/discussions?${mockNotificationReferrer}`, + // ); + // }); + + // it('when error fetching discussion via graphql api, default to base repository discussion url', async () => { + // const rendererLogErrorSpy = jest + // .spyOn(logger, 'rendererLogError') + // .mockImplementation(); + + // const subject = { + // title: '1.16.0', + // url: null, + // latest_comment_url: null, + // type: 'Discussion' as SubjectType, + // } as Subject; + + // fetchDiscussionByNumberSpy.mockResolvedValue({ + // data: null, + // errors: [ + // { + // message: 'Something failed', + // }, + // ], + // } as unknown as ExecutionResult); + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(fetchDiscussionByNumberSpy).toHaveBeenCalledTimes(1); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/discussions?${mockNotificationReferrer}`, + // ); + // expect(rendererLogErrorSpy).toHaveBeenCalledTimes(1); + // }); + + // it('when discussion found via graphql api, link to matching discussion and comment hash', async () => { + // const subject = { + // title: '1.16.0', + // url: null, + // latest_comment_url: null, + // type: 'Discussion' as SubjectType, + // } as Subject; + + // fetchDiscussionByNumberSpy.mockResolvedValue({ + // data: mockDiscussionByNumberGraphQLResponse, + // } as ExecutionResult); + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(fetchDiscussionByNumberSpy).toHaveBeenCalledTimes(1); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/discussions/612?${mockNotificationReferrer}#discussioncomment-2300902`, + // ); + // }); + + // it('when api throws error, default to base repository discussion url', async () => { + // const rendererLogErrorSpy = jest + // .spyOn(logger, 'rendererLogError') + // .mockImplementation(); + + // const subject = { + // title: '1.16.0', + // url: null, + // latest_comment_url: null, + // type: 'Discussion' as SubjectType, + // } as Subject; + + // fetchDiscussionByNumberSpy.mockRejectedValue( + // new Error('Something failed'), + // ); + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(fetchDiscussionByNumberSpy).toHaveBeenCalledTimes(1); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/discussions?${mockNotificationReferrer}`, + // ); + // expect(rendererLogErrorSpy).toHaveBeenCalledTimes(1); + // }); + // }); + + // it('Repository Invitation url', async () => { + // const subject = { + // title: + // 'Invitation to join gitify-app/notifications-test from unit-tests', + // url: null, + // latest_comment_url: null, + // type: 'RepositoryInvitation' as SubjectType, + // } as Subject; + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/invitations?${mockNotificationReferrer}`, + // ); + // }); + + // it('Repository Dependabot Alerts Thread url', async () => { + // const subject = { + // title: 'Your repository has dependencies with security vulnerabilities', + // url: null, + // latest_comment_url: null, + // type: 'RepositoryDependabotAlertsThread' as SubjectType, + // }; + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/security/dependabot?${mockNotificationReferrer}`, + // ); + // }); + + // describe('Workflow Run URLs', () => { + // it('approval requested', async () => { + // const subject = { + // title: 'some-user requested your review to deploy to an environment', + // url: null, + // latest_comment_url: null, + // type: 'WorkflowRun' as SubjectType, + // } as Subject; + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/actions?query=is%3AWAITING&${mockNotificationReferrer}`, + // ); + // }); + + // it('unhandled status/action scenario', async () => { + // const subject = { + // title: + // 'some-user requested your unhandled-action to deploy to an environment', + // url: null, + // latest_comment_url: null, + // type: 'WorkflowRun' as SubjectType, + // } as Subject; + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/actions?${mockNotificationReferrer}`, + // ); + // }); + + // it('unhandled workflow scenario', async () => { + // const subject = { + // title: 'some unhandled scenario', + // url: null, + // latest_comment_url: null, + // type: 'WorkflowRun' as SubjectType, + // } as Subject; + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/actions?${mockNotificationReferrer}`, + // ); + // }); + // }); + + // describe('defaults web urls', () => { + // it('issues - defaults when no urls present in notification', async () => { + // const subject = { + // title: 'generate github web url unit tests', + // url: null, + // latest_comment_url: null, + // type: 'Issue' as SubjectType, + // } as Subject; + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/issues?${mockNotificationReferrer}`, + // ); + // }); + + // it('pull requests - defaults when no urls present in notification', async () => { + // const subject = { + // title: 'generate github web url unit tests', + // url: null, + // latest_comment_url: null, + // type: 'PullRequest' as SubjectType, + // } as Subject; + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/pulls?${mockNotificationReferrer}`, + // ); + // }); + + // it('other - defaults when no urls present in notification', async () => { + // const subject = { + // title: 'generate github web url unit tests', + // url: null, + // latest_comment_url: null, + // type: 'Commit' as SubjectType, + // } as Subject; + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test?${mockNotificationReferrer}`, + // ); + // }); + + // it('defaults when exception handled during specialized html enrichment process', async () => { + // const rendererLogErrorSpy = jest + // .spyOn(logger, 'rendererLogError') + // .mockImplementation(); + + // const subject = { + // title: 'generate github web url unit tests', + // url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link, + // latest_comment_url: null as Link, + // type: 'Issue' as SubjectType, + // } as Subject; + + // getHtmlUrlSpy.mockRejectedValue(new Error('Test error')); + + // const result = await generateGitHubWebUrl({ + // ...mockSingleNotification, + // subject: subject, + // }); + + // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(1); + // expect(result).toBe( + // `https://github.com/gitify-app/notifications-test/issues?${mockNotificationReferrer}`, + // ); + // expect(rendererLogErrorSpy).toHaveBeenCalledTimes(1); + // }); + // }); }); describe('getChevronDetails', () => { diff --git a/src/renderer/utils/notifications/handlers/discussion.test.ts b/src/renderer/utils/notifications/handlers/discussion.test.ts index 58a6a2922..50307166e 100644 --- a/src/renderer/utils/notifications/handlers/discussion.test.ts +++ b/src/renderer/utils/notifications/handlers/discussion.test.ts @@ -6,15 +6,19 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; -import type { Link } from '../../../types'; +import type { GitifySubject, Link } from '../../../types'; import type { Notification, Owner, Repository } from '../../../typesGitHub'; import type { AuthorFieldsFragment, Discussion, DiscussionStateReason, + FetchDiscussionByNumberQuery, } from '../../api/graphql/generated/graphql'; import { discussionHandler } from './discussion'; +type DiscussionResponse = + FetchDiscussionByNumberQuery['repository']['discussion']; + const mockDiscussionAuthor: AuthorFieldsFragment = { login: 'discussion-author', html_url: 'https://github.com/discussion-author' as Link, @@ -78,7 +82,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { labels: [], htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/1', - }); + } as GitifySubject); }); it('duplicate discussion state', async () => { @@ -110,7 +114,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { labels: [], htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/1', - }); + } as GitifySubject); }); it('open discussion state', async () => { @@ -142,7 +146,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { labels: [], htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/1', - }); + } as GitifySubject); }); it('outdated discussion state', async () => { @@ -174,7 +178,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { labels: [], htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/1', - }); + } as GitifySubject); }); it('reopened discussion state', async () => { @@ -206,7 +210,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { labels: [], htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/1', - }); + } as GitifySubject); }); it('resolved discussion state', async () => { @@ -238,7 +242,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { labels: [], htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/1', - }); + } as GitifySubject); }); it('discussion with labels', async () => { @@ -280,26 +284,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { labels: ['enhancement'], htmlUrl: 'https://github.com/gitify-app/notifications-test/discussions/1', - }); - }); - - it('early return if discussion state filtered', async () => { - nock('https://api.github.com') - .post('/graphql') - .reply(200, { - data: { - repository: { - discussion: mockDiscussionNode(null, false), - }, - }, - }); - - const result = await discussionHandler.enrich(mockNotification, { - ...mockSettings, - filterStates: ['closed'], - }); - - expect(result).toEqual(null); + } as GitifySubject); }); }); @@ -345,7 +330,7 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { function mockDiscussionNode( state: DiscussionStateReason, isAnswered: boolean, -): Partial { +): Partial { return { number: 123, title: 'This is a mock discussion', @@ -358,5 +343,5 @@ function mockDiscussionNode( totalCount: 0, }, labels: null, - } as unknown as Partial; + } as Partial; } diff --git a/src/renderer/utils/notifications/handlers/issue.test.ts b/src/renderer/utils/notifications/handlers/issue.test.ts index 5deb80518..7117b2ed8 100644 --- a/src/renderer/utils/notifications/handlers/issue.test.ts +++ b/src/renderer/utils/notifications/handlers/issue.test.ts @@ -7,11 +7,13 @@ import { } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; import { createPartialMockUser } from '../../../__mocks__/user-mocks'; -import type { Link } from '../../../types'; +import type { GitifySubject, Link } from '../../../types'; import type { Notification } from '../../../typesGitHub'; import type { FetchIssueByNumberQuery } from '../../api/graphql/generated/graphql'; import { issueHandler } from './issue'; +type IssueResponse = FetchIssueByNumberQuery['repository']['issue']; + describe('renderer/utils/notifications/handlers/issue.ts', () => { describe('enrich', () => { const mockAuthor = createPartialMockUser('some-author'); @@ -48,7 +50,8 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { author: mockAuthor, labels: { nodes: [] }, comments: { totalCount: 0, nodes: [] }, - } as FetchIssueByNumberQuery['repository']['issue'], + milestone: null, + } as IssueResponse, }, }, }); @@ -67,7 +70,8 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { comments: 0, htmlUrl: 'https://github.com/gitify-app/noticiation-test/issues/123', labels: [], - }); + milestone: null, + } as GitifySubject); }); it('closed issue state', async () => { @@ -85,7 +89,8 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { author: mockAuthor, labels: { nodes: [] }, comments: { totalCount: 0, nodes: [] }, - } as FetchIssueByNumberQuery['repository']['issue'], + milestone: null, + } as IssueResponse, }, }, }); @@ -104,210 +109,270 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { comments: 0, htmlUrl: 'https://github.com/gitify-app/noticiation-test/issues/123', labels: [], - }); + milestone: null, + } as GitifySubject); }); it('completed issue state', async () => { nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/1') + .post('/graphql') .reply(200, { - number: 123, - state: 'closed', - state_reason: 'completed', - user: mockAuthor, - labels: [], + data: { + repository: { + issue: { + __typename: 'Issue', + number: 123, + title: 'PR Title', + state: 'CLOSED', + stateReason: 'COMPLETED', + url: 'https://github.com/gitify-app/noticiation-test/issues/123', + author: mockAuthor, + labels: { nodes: [] }, + comments: { totalCount: 0, nodes: [] }, + milestone: null, + } as IssueResponse, + }, + }, }); - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/comments/302888448') - .reply(200, { user: mockCommenter }); - const result = await issueHandler.enrich(mockNotification, mockSettings); expect(result).toEqual({ number: 123, - state: 'completed', + state: 'COMPLETED', user: { - login: mockCommenter.login, - html_url: mockCommenter.html_url, - avatar_url: mockCommenter.avatar_url, - type: mockCommenter.type, + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, }, + comments: 0, + htmlUrl: 'https://github.com/gitify-app/noticiation-test/issues/123', labels: [], - }); + milestone: null, + } as GitifySubject); }); it('not_planned issue state', async () => { nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/1') + .post('/graphql') .reply(200, { - number: 123, - state: 'open', - state_reason: 'not_planned', - user: mockAuthor, - labels: [], + data: { + repository: { + issue: { + __typename: 'Issue', + number: 123, + title: 'PR Title', + state: 'CLOSED', + stateReason: 'NOT_PLANNED', + url: 'https://github.com/gitify-app/noticiation-test/issues/123', + author: mockAuthor, + labels: { nodes: [] }, + comments: { totalCount: 0, nodes: [] }, + milestone: null, + } as IssueResponse, + }, + }, }); - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/comments/302888448') - .reply(200, { user: mockCommenter }); - const result = await issueHandler.enrich(mockNotification, mockSettings); expect(result).toEqual({ number: 123, - state: 'not_planned', + state: 'NOT_PLANNED', user: { - login: mockCommenter.login, - html_url: mockCommenter.html_url, - avatar_url: mockCommenter.avatar_url, - type: mockCommenter.type, + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, }, + comments: 0, + htmlUrl: 'https://github.com/gitify-app/noticiation-test/issues/123', labels: [], - }); + milestone: null, + } as GitifySubject); }); it('reopened issue state', async () => { nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/1') + .post('/graphql') .reply(200, { - number: 123, - state: 'open', - state_reason: 'reopened', - user: mockAuthor, - labels: [], + data: { + repository: { + issue: { + __typename: 'Issue', + number: 123, + title: 'PR Title', + state: 'OPEN', + stateReason: 'REOPENED', + url: 'https://github.com/gitify-app/noticiation-test/issues/123', + author: mockAuthor, + labels: { nodes: [] }, + comments: { totalCount: 0, nodes: [] }, + milestone: null, + } as IssueResponse, + }, + }, }); + const result = await issueHandler.enrich(mockNotification, mockSettings); + + expect(result).toEqual({ + number: 123, + state: 'REOPENED', + user: { + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, + }, + comments: 0, + htmlUrl: 'https://github.com/gitify-app/noticiation-test/issues/123', + labels: [], + milestone: null, + } as GitifySubject); + }); + + it('with comments', async () => { nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/comments/302888448') - .reply(200, { user: mockCommenter }); + .post('/graphql') + .reply(200, { + data: { + repository: { + issue: { + __typename: 'Issue', + number: 123, + title: 'PR Title', + state: 'OPEN', + url: 'https://github.com/gitify-app/noticiation-test/issues/123', + author: mockAuthor, + labels: { + nodes: [], + }, + comments: { + totalCount: 1, + nodes: [ + { + author: mockCommenter, + url: 'https://github.com/gitify-app/noticiation-test/issues/123#issuecomment-1234', + }, + ], + }, + milestone: null, + } as IssueResponse, + }, + }, + }); const result = await issueHandler.enrich(mockNotification, mockSettings); expect(result).toEqual({ number: 123, - state: 'reopened', + state: 'OPEN', user: { login: mockCommenter.login, html_url: mockCommenter.html_url, avatar_url: mockCommenter.avatar_url, type: mockCommenter.type, }, + comments: 1, + htmlUrl: + 'https://github.com/gitify-app/noticiation-test/issues/123#issuecomment-1234', labels: [], - }); + milestone: null, + } as GitifySubject); }); - it('handle issues without latest_comment_url', async () => { - mockNotification.subject.latest_comment_url = null; - + it('with labels', async () => { nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/1') + .post('/graphql') .reply(200, { - number: 123, - state: 'open', - draft: false, - merged: false, - user: mockAuthor, - labels: [], + data: { + repository: { + issue: { + __typename: 'Issue', + number: 123, + title: 'PR Title', + state: 'OPEN', + url: 'https://github.com/gitify-app/noticiation-test/issues/123', + author: mockAuthor, + labels: { + nodes: [ + { + name: 'enhancement', + }, + ], + }, + comments: { totalCount: 0, nodes: [] }, + milestone: null, + } as IssueResponse, + }, + }, }); const result = await issueHandler.enrich(mockNotification, mockSettings); expect(result).toEqual({ number: 123, - state: 'open', + state: 'OPEN', user: { login: mockAuthor.login, html_url: mockAuthor.html_url, avatar_url: mockAuthor.avatar_url, type: mockAuthor.type, }, - labels: [], - }); - }); - - describe('Issue With Labels', () => { - it('with labels', async () => { - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/1') - .reply(200, { - number: 123, - state: 'open', - user: mockAuthor, - labels: [{ name: 'enhancement' }], - }); - - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/comments/302888448') - .reply(200, { user: mockCommenter }); - - const result = await issueHandler.enrich( - mockNotification, - mockSettings, - ); - - expect(result).toEqual({ - number: 123, - state: 'open', - user: { - login: mockCommenter.login, - html_url: mockCommenter.html_url, - avatar_url: mockCommenter.avatar_url, - type: mockCommenter.type, - }, - labels: ['enhancement'], - }); - }); - - it('handle null labels', async () => { - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/1') - .reply(200, { - number: 123, - state: 'open', - user: mockAuthor, - labels: null, - }); - - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/comments/302888448') - .reply(200, { user: mockCommenter }); - - const result = await issueHandler.enrich( - mockNotification, - mockSettings, - ); - - expect(result).toEqual({ - number: 123, - state: 'open', - user: { - login: mockCommenter.login, - html_url: mockCommenter.html_url, - avatar_url: mockCommenter.avatar_url, - type: mockCommenter.type, - }, - labels: [], - }); - }); + comments: 0, + htmlUrl: 'https://github.com/gitify-app/noticiation-test/issues/123', + labels: ['enhancement'], + milestone: null, + } as GitifySubject); }); - it('early return if issue state filtered out', async () => { + it('with milestone', async () => { nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/1') + .post('/graphql') .reply(200, { - number: 123, - state: 'open', - user: mockAuthor, - labels: [], + data: { + repository: { + issue: { + __typename: 'Issue', + number: 123, + title: 'PR Title', + state: 'OPEN', + url: 'https://github.com/gitify-app/noticiation-test/issues/123', + author: mockAuthor, + labels: { + nodes: [], + }, + comments: { totalCount: 0, nodes: [] }, + milestone: { + state: 'OPEN', + title: 'Open Milestone', + }, + } as IssueResponse, + }, + }, }); - const result = await issueHandler.enrich(mockNotification, { - ...mockSettings, - filterStates: ['closed'], - }); + const result = await issueHandler.enrich(mockNotification, mockSettings); - expect(result).toEqual(null); + expect(result).toEqual({ + number: 123, + state: 'OPEN', + user: { + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, + }, + comments: 0, + htmlUrl: 'https://github.com/gitify-app/noticiation-test/issues/123', + labels: [], + milestone: { + state: 'OPEN', + title: 'Open Milestone', + }, + } as GitifySubject); }); }); diff --git a/src/renderer/utils/notifications/handlers/pullRequest.test.ts b/src/renderer/utils/notifications/handlers/pullRequest.test.ts index 73b454e43..347e2d68b 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.test.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.test.ts @@ -7,14 +7,17 @@ import { } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; import { createPartialMockUser } from '../../../__mocks__/user-mocks'; -import type { Link } from '../../../types'; +import type { GitifySubject, Link } from '../../../types'; import type { Notification } from '../../../typesGitHub'; import type { - FetchPullByNumberQuery, + FetchPullRequestByNumberQuery, PullRequestReviewState, } from '../../api/graphql/generated/graphql'; import { getLatestReviewForReviewers, pullRequestHandler } from './pullRequest'; +type PullRequestResponse = + FetchPullRequestByNumberQuery['repository']['pullRequest']; + describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { let mockNotification: Notification; @@ -63,7 +66,11 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { totalCount: 0, nodes: [], }, - } as FetchPullByNumberQuery['repository']['pullRequest'], + milestone: null, + closingIssuesReferences: { + nodes: [], + }, + } as PullRequestResponse, }, }, }); @@ -88,7 +95,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { comments: 0, milestone: null, htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', - }); + } as GitifySubject); }); it('closed pull request state', async () => { @@ -116,7 +123,11 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { totalCount: 0, nodes: [], }, - } as FetchPullByNumberQuery['repository']['pullRequest'], + milestone: null, + closingIssuesReferences: { + nodes: [], + }, + } as PullRequestResponse, }, }, }); @@ -141,7 +152,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { comments: 0, milestone: null, htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', - }); + } as GitifySubject); }); it('draft pull request state', async () => { @@ -169,7 +180,11 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { totalCount: 0, nodes: [], }, - } as FetchPullByNumberQuery['repository']['pullRequest'], + milestone: null, + closingIssuesReferences: { + nodes: [], + }, + } as PullRequestResponse, }, }, }); @@ -194,7 +209,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { comments: 0, milestone: null, htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', - }); + } as GitifySubject); }); it('merged pull request state', async () => { @@ -222,7 +237,11 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { totalCount: 0, nodes: [], }, - } as FetchPullByNumberQuery['repository']['pullRequest'], + milestone: null, + closingIssuesReferences: { + nodes: [], + }, + } as PullRequestResponse, }, }, }); @@ -247,7 +266,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { comments: 0, milestone: null, htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', - }); + } as GitifySubject); }); it('with comments', async () => { @@ -282,7 +301,11 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { totalCount: 0, nodes: [], }, - } as FetchPullByNumberQuery['repository']['pullRequest'], + milestone: null, + closingIssuesReferences: { + nodes: [], + }, + } as PullRequestResponse, }, }, }); @@ -308,7 +331,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { milestone: null, htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123#issuecomment-1234', - }); + } as GitifySubject); }); it('with labels', async () => { @@ -338,7 +361,11 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { totalCount: 0, nodes: [], }, - } as FetchPullByNumberQuery['repository']['pullRequest'], + milestone: null, + closingIssuesReferences: { + nodes: [], + }, + } as PullRequestResponse, }, }, }); @@ -363,7 +390,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { comments: 0, milestone: null, htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', - }); + } as GitifySubject); }); it('with linked issues', async () => { @@ -391,6 +418,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { totalCount: 0, nodes: [], }, + milestone: null, closingIssuesReferences: { nodes: [ { @@ -398,7 +426,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { }, ], }, - } as FetchPullByNumberQuery['repository']['pullRequest'], + } as PullRequestResponse, }, }, }); @@ -423,7 +451,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { comments: 0, milestone: null, htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', - }); + } as GitifySubject); }); it('with milestone', async () => { @@ -457,7 +485,10 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'OPEN', title: 'Open Milestone', }, - } as FetchPullByNumberQuery['repository']['pullRequest'], + closingIssuesReferences: { + nodes: [], + }, + } as PullRequestResponse, }, }, }); @@ -480,9 +511,12 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { labels: [], linkedIssues: [], comments: 0, - milestone: null, + milestone: { + state: 'OPEN', + title: 'Open Milestone', + }, htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', - }); + } as GitifySubject); }); }); diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index ed9a61c29..c26288b6b 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -17,7 +17,7 @@ import type { } from '../../../types'; import type { Notification, Subject } from '../../../typesGitHub'; import { fetchPullByNumber } from '../../api/client'; -import type { FetchPullByNumberQuery } from '../../api/graphql/generated/graphql'; +import type { FetchPullRequestByNumberQuery } from '../../api/graphql/generated/graphql'; import { DefaultHandler } from './default'; import { getNotificationAuthor } from './utils'; @@ -54,7 +54,7 @@ class PullRequestHandler extends DefaultHandler { linkedIssues: pr.closingIssuesReferences?.nodes.map((issue) => `#${issue.number}`) ?? [], - milestone: null, //pr.milestone, + milestone: pr.milestone, htmlUrl: prComment?.url ?? pr.url, }; } @@ -82,7 +82,7 @@ class PullRequestHandler extends DefaultHandler { export const pullRequestHandler = new PullRequestHandler(); export function getLatestReviewForReviewers( - reviews: FetchPullByNumberQuery['repository']['pullRequest']['reviews']['nodes'], + reviews: FetchPullRequestByNumberQuery['repository']['pullRequest']['reviews']['nodes'], ): GitifyPullRequestReview[] { if (!reviews.length) { return null; From a9a11d2790dd4e7769a909927598dbb99a6bdf7e Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 23:10:34 +1000 Subject: [PATCH 13/25] Update issue.test.ts --- src/renderer/utils/notifications/handlers/issue.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/renderer/utils/notifications/handlers/issue.test.ts b/src/renderer/utils/notifications/handlers/issue.test.ts index 5deb80518..95461331c 100644 --- a/src/renderer/utils/notifications/handlers/issue.test.ts +++ b/src/renderer/utils/notifications/handlers/issue.test.ts @@ -44,7 +44,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { number: 123, title: 'PR Title', state: 'OPEN', - url: 'https://github.com/gitify-app/noticiation-test/issues/123', + url: 'https://github.com/gitify-app/notifications-test/issues/123', author: mockAuthor, labels: { nodes: [] }, comments: { totalCount: 0, nodes: [] }, @@ -65,7 +65,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { type: mockAuthor.type, }, comments: 0, - htmlUrl: 'https://github.com/gitify-app/noticiation-test/issues/123', + htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123', labels: [], }); }); @@ -81,7 +81,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { number: 123, title: 'PR Title', state: 'CLOSED', - url: 'https://github.com/gitify-app/noticiation-test/issues/123', + url: 'https://github.com/gitify-app/notifications-test/issues/123', author: mockAuthor, labels: { nodes: [] }, comments: { totalCount: 0, nodes: [] }, @@ -102,7 +102,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { type: mockAuthor.type, }, comments: 0, - htmlUrl: 'https://github.com/gitify-app/noticiation-test/issues/123', + htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123', labels: [], }); }); From 021c22c05f3fe9358ca0c494d0e97397f5018c09 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 23:15:11 +1000 Subject: [PATCH 14/25] refactor: tests Signed-off-by: Adam Setch --- src/renderer/utils/notifications/handlers/pullRequest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index c26288b6b..ae5e0edb1 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -94,7 +94,6 @@ export function getLatestReviewForReviewers( for (const prReview of sortedReviews) { const reviewerFound = latestReviews.find( (review) => review.author.login === prReview.author.login, - prReview.state, ); if (!reviewerFound) { From b0ff09b37e3aaf057ac8efdbcc9036c798889241 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 23:36:25 +1000 Subject: [PATCH 15/25] Merge branch 'main' into refactor/fetch-issue-graphql Signed-off-by: Adam Setch --- .../notifications/handlers/issue.test.ts | 26 +++++++------- .../handlers/pullRequest.test.ts | 34 +++++++++---------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/renderer/utils/notifications/handlers/issue.test.ts b/src/renderer/utils/notifications/handlers/issue.test.ts index a01c1e52f..a044ec43c 100644 --- a/src/renderer/utils/notifications/handlers/issue.test.ts +++ b/src/renderer/utils/notifications/handlers/issue.test.ts @@ -125,7 +125,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { title: 'PR Title', state: 'CLOSED', stateReason: 'COMPLETED', - url: 'https://github.com/gitify-app/noticiation-test/issues/123', + url: 'https://github.com/gitify-app/notifications-test/issues/123', author: mockAuthor, labels: { nodes: [] }, comments: { totalCount: 0, nodes: [] }, @@ -147,7 +147,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { type: mockAuthor.type, }, comments: 0, - htmlUrl: 'https://github.com/gitify-app/noticiation-test/issues/123', + htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123', labels: [], milestone: null, } as GitifySubject); @@ -165,7 +165,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { title: 'PR Title', state: 'CLOSED', stateReason: 'NOT_PLANNED', - url: 'https://github.com/gitify-app/noticiation-test/issues/123', + url: 'https://github.com/gitify-app/notifications-test/issues/123', author: mockAuthor, labels: { nodes: [] }, comments: { totalCount: 0, nodes: [] }, @@ -187,7 +187,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { type: mockAuthor.type, }, comments: 0, - htmlUrl: 'https://github.com/gitify-app/noticiation-test/issues/123', + htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123', labels: [], milestone: null, } as GitifySubject); @@ -205,7 +205,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { title: 'PR Title', state: 'OPEN', stateReason: 'REOPENED', - url: 'https://github.com/gitify-app/noticiation-test/issues/123', + url: 'https://github.com/gitify-app/notifications-test/issues/123', author: mockAuthor, labels: { nodes: [] }, comments: { totalCount: 0, nodes: [] }, @@ -227,7 +227,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { type: mockAuthor.type, }, comments: 0, - htmlUrl: 'https://github.com/gitify-app/noticiation-test/issues/123', + htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123', labels: [], milestone: null, } as GitifySubject); @@ -244,7 +244,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { number: 123, title: 'PR Title', state: 'OPEN', - url: 'https://github.com/gitify-app/noticiation-test/issues/123', + url: 'https://github.com/gitify-app/notifications-test/issues/123', author: mockAuthor, labels: { nodes: [], @@ -254,7 +254,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { nodes: [ { author: mockCommenter, - url: 'https://github.com/gitify-app/noticiation-test/issues/123#issuecomment-1234', + url: 'https://github.com/gitify-app/notifications-test/issues/123#issuecomment-1234', }, ], }, @@ -277,7 +277,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { }, comments: 1, htmlUrl: - 'https://github.com/gitify-app/noticiation-test/issues/123#issuecomment-1234', + 'https://github.com/gitify-app/notifications-test/issues/123#issuecomment-1234', labels: [], milestone: null, } as GitifySubject); @@ -294,7 +294,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { number: 123, title: 'PR Title', state: 'OPEN', - url: 'https://github.com/gitify-app/noticiation-test/issues/123', + url: 'https://github.com/gitify-app/notifications-test/issues/123', author: mockAuthor, labels: { nodes: [ @@ -322,7 +322,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { type: mockAuthor.type, }, comments: 0, - htmlUrl: 'https://github.com/gitify-app/noticiation-test/issues/123', + htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123', labels: ['enhancement'], milestone: null, } as GitifySubject); @@ -339,7 +339,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { number: 123, title: 'PR Title', state: 'OPEN', - url: 'https://github.com/gitify-app/noticiation-test/issues/123', + url: 'https://github.com/gitify-app/notifications-test/issues/123', author: mockAuthor, labels: { nodes: [], @@ -366,7 +366,7 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { type: mockAuthor.type, }, comments: 0, - htmlUrl: 'https://github.com/gitify-app/noticiation-test/issues/123', + htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123', labels: [], milestone: { state: 'OPEN', diff --git a/src/renderer/utils/notifications/handlers/pullRequest.test.ts b/src/renderer/utils/notifications/handlers/pullRequest.test.ts index 6b8a7c72a..efc73efe0 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.test.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.test.ts @@ -55,7 +55,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { isDraft: false, merged: false, isInMergeQueue: false, - url: 'https://github.com/gitify-app/noticiation-test/pulls/123', + url: 'https://github.com/gitify-app/notifications-test/pulls/123', author: mockAuthor, labels: null, comments: { @@ -94,7 +94,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { linkedIssues: [], comments: 0, milestone: null, - htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', + htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123', } as GitifySubject); }); @@ -112,7 +112,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { isDraft: false, merged: false, isInMergeQueue: false, - url: 'https://github.com/gitify-app/noticiation-test/pulls/123', + url: 'https://github.com/gitify-app/notifications-test/pulls/123', author: mockAuthor, labels: null, comments: { @@ -151,7 +151,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { linkedIssues: [], comments: 0, milestone: null, - htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', + htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123', } as GitifySubject); }); @@ -169,7 +169,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { isDraft: true, merged: false, isInMergeQueue: false, - url: 'https://github.com/gitify-app/noticiation-test/pulls/123', + url: 'https://github.com/gitify-app/notifications-test/pulls/123', author: mockAuthor, labels: null, comments: { @@ -208,7 +208,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { linkedIssues: [], comments: 0, milestone: null, - htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', + htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123', } as GitifySubject); }); @@ -226,7 +226,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { isDraft: false, merged: true, isInMergeQueue: false, - url: 'https://github.com/gitify-app/noticiation-test/pulls/123', + url: 'https://github.com/gitify-app/notifications-test/pulls/123', author: mockAuthor, labels: null, comments: { @@ -265,7 +265,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { linkedIssues: [], comments: 0, milestone: null, - htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', + htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123', } as GitifySubject); }); @@ -283,7 +283,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { isDraft: false, merged: false, isInMergeQueue: false, - url: 'https://github.com/gitify-app/noticiation-test/pulls/123', + url: 'https://github.com/gitify-app/notifications-test/pulls/123', author: mockAuthor, labels: { nodes: [{ name: 'enhancement' }], @@ -293,7 +293,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { nodes: [ { author: mockCommenter, - url: 'https://github.com/gitify-app/noticiation-test/pulls/123#issuecomment-1234', + url: 'https://github.com/gitify-app/notifications-test/pulls/123#issuecomment-1234', }, ], }, @@ -330,7 +330,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { comments: 1, milestone: null, htmlUrl: - 'https://github.com/gitify-app/noticiation-test/pulls/123#issuecomment-1234', + 'https://github.com/gitify-app/notifications-test/pulls/123#issuecomment-1234', } as GitifySubject); }); @@ -348,7 +348,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { isDraft: false, merged: false, isInMergeQueue: false, - url: 'https://github.com/gitify-app/noticiation-test/pulls/123', + url: 'https://github.com/gitify-app/notifications-test/pulls/123', author: mockAuthor, labels: { nodes: [{ name: 'enhancement' }], @@ -389,7 +389,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { linkedIssues: [], comments: 0, milestone: null, - htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', + htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123', } as GitifySubject); }); @@ -407,7 +407,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { isDraft: false, merged: false, isInMergeQueue: false, - url: 'https://github.com/gitify-app/noticiation-test/pulls/123', + url: 'https://github.com/gitify-app/notifications-test/pulls/123', author: mockAuthor, labels: null, comments: { @@ -450,7 +450,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { linkedIssues: ['#789'], comments: 0, milestone: null, - htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', + htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123', } as GitifySubject); }); @@ -468,7 +468,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { isDraft: false, merged: false, isInMergeQueue: false, - url: 'https://github.com/gitify-app/noticiation-test/pulls/123', + url: 'https://github.com/gitify-app/notifications-test/pulls/123', author: mockAuthor, labels: { nodes: [], @@ -515,7 +515,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'OPEN', title: 'Open Milestone', }, - htmlUrl: 'https://github.com/gitify-app/noticiation-test/pulls/123', + htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123', } as GitifySubject); }); }); From 6b35fc2624f05b293ad8382f433d9a2601a9ef0d Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 23:38:38 +1000 Subject: [PATCH 16/25] Merge branch 'main' into refactor/fetch-issue-graphql Signed-off-by: Adam Setch --- .../utils/notifications/handlers/checkSuite.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/renderer/utils/notifications/handlers/checkSuite.ts b/src/renderer/utils/notifications/handlers/checkSuite.ts index 35c5e566a..1985c50d8 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.ts @@ -9,12 +9,12 @@ import { XIcon, } from '@primer/octicons-react'; -import type { - GitifyCheckSuiteStatus, - GitifySubject, +import { + type GitifyCheckSuiteStatus, + type GitifySubject, IconColor, - Link, - SettingsState, + type Link, + type SettingsState, } from '../../../types'; import type { Notification, Subject } from '../../../typesGitHub'; import { actionsURL } from '../../helpers'; From 32eb1e0a53c91817f1243a96d1174291987a0a7a Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 23:42:28 +1000 Subject: [PATCH 17/25] Merge branch 'main' into refactor/fetch-issue-graphql Signed-off-by: Adam Setch --- .../utils/notifications/handlers/checkSuite.test.ts | 8 ++++---- .../utils/notifications/handlers/default.test.ts | 4 ++-- src/renderer/utils/notifications/handlers/issue.ts | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/renderer/utils/notifications/handlers/checkSuite.test.ts b/src/renderer/utils/notifications/handlers/checkSuite.test.ts index 5ece41dd5..27b3de0fe 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.test.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.test.ts @@ -199,25 +199,25 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { it('iconColor', () => { expect( checkSuiteHandler.iconColor( - createMockSubject({ type: 'CheckSuite', state: 'success' }), + createMockSubject({ type: 'CheckSuite', state: 'SUCCESS' }), ), ).toBe(IconColor.GREEN); expect( checkSuiteHandler.iconColor( - createMockSubject({ type: 'CheckSuite', state: 'failure' }), + createMockSubject({ type: 'CheckSuite', state: 'FAILURE' }), ), ).toBe(IconColor.RED); expect( checkSuiteHandler.iconColor( - createMockSubject({ type: 'CheckSuite', state: 'cancelled' }), + createMockSubject({ type: 'CheckSuite', state: 'CANCELLED' }), ), ).toBe(IconColor.GRAY); expect( checkSuiteHandler.iconColor( - createMockSubject({ type: 'CheckSuite', state: 'skipped' }), + createMockSubject({ type: 'CheckSuite', state: 'SKIPPED' }), ), ).toBe(IconColor.GRAY); diff --git a/src/renderer/utils/notifications/handlers/default.test.ts b/src/renderer/utils/notifications/handlers/default.test.ts index abe6594bc..bc7371727 100644 --- a/src/renderer/utils/notifications/handlers/default.test.ts +++ b/src/renderer/utils/notifications/handlers/default.test.ts @@ -37,8 +37,8 @@ describe('renderer/utils/notifications/handlers/default.ts', () => { describe('iconColor', () => { it('returns GRAY for any state (fallback behavior)', () => { - const states: Array = [ - 'unknown' as StateType, + const states: Array = [ + 'unknown' as GitifyNotificationState, null, undefined, ]; diff --git a/src/renderer/utils/notifications/handlers/issue.ts b/src/renderer/utils/notifications/handlers/issue.ts index 1af2a6810..77341de79 100644 --- a/src/renderer/utils/notifications/handlers/issue.ts +++ b/src/renderer/utils/notifications/handlers/issue.ts @@ -66,13 +66,13 @@ class IssueHandler extends DefaultHandler { } iconColor(subject: Subject): IconColor { - switch (subject.state) { - case 'open': - case 'reopened': + switch (subject.state as GitifyIssueState) { + case 'OPEN': + case 'REOPENED': return IconColor.GREEN; - case 'closed': + case 'CLOSED': return IconColor.RED; - case 'completed': + case 'COMPLETED': return IconColor.PURPLE; default: return defaultHandler.iconColor(subject); From f60b3a66ac27f0fdb5cc0a0ccbb0ede19a111d73 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 23:49:39 +1000 Subject: [PATCH 18/25] Merge branch 'main' into refactor/fetch-issue-graphql Signed-off-by: Adam Setch --- src/renderer/utils/notifications/handlers/default.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/utils/notifications/handlers/default.ts b/src/renderer/utils/notifications/handlers/default.ts index e502a61de..8c674b6a3 100644 --- a/src/renderer/utils/notifications/handlers/default.ts +++ b/src/renderer/utils/notifications/handlers/default.ts @@ -50,7 +50,7 @@ export class DefaultHandler implements NotificationTypeHandler { } defaultUrl(notification: Notification): Link { - return notification.repository.html_url as Link; + return notification.repository.html_url; } } From 361cdebbb88ffc036c8d912692b8bb0b5b956043 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 23:50:27 +1000 Subject: [PATCH 19/25] Merge branch 'main' into refactor/fetch-issue-graphql Signed-off-by: Adam Setch --- src/renderer/utils/notifications/handlers/pullRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index 32ac2fe0d..b6164756d 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -104,7 +104,7 @@ export function getLatestReviewForReviewers( // Find the most recent review for each reviewer const latestReviews = []; - const sortedReviews = reviews.reverse(); + const sortedReviews = reviews.toReversed(); for (const prReview of sortedReviews) { const reviewerFound = latestReviews.find( (review) => review.author.login === prReview.author.login, From 4c47945a72f2436c846be3796cb48ccfbdd7a5a1 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 23:55:08 +1000 Subject: [PATCH 20/25] Merge branch 'main' into refactor/fetch-issue-graphql Signed-off-by: Adam Setch --- .../utils/notifications/handlers/issue.test.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/renderer/utils/notifications/handlers/issue.test.ts b/src/renderer/utils/notifications/handlers/issue.test.ts index a044ec43c..3542d2f2f 100644 --- a/src/renderer/utils/notifications/handlers/issue.test.ts +++ b/src/renderer/utils/notifications/handlers/issue.test.ts @@ -413,6 +413,15 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { ).displayName, ).toBe('SkipIcon'); + expect( + issueHandler.iconType( + createMockSubject({ + type: 'Issue', + state: 'DUPLICATE', + }), + ).displayName, + ).toBe('SkipIcon'); + expect( issueHandler.iconType( createMockSubject({ @@ -448,12 +457,6 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { ), ).toBe(IconColor.PURPLE); - expect( - issueHandler.iconColor( - createMockSubject({ type: 'Issue', state: 'DRAFT' }), - ), - ).toBe(IconColor.GRAY); - expect( issueHandler.iconColor( createMockSubject({ type: 'Issue', state: 'NOT_PLANNED' }), From fe5ee69abc29ce64912be9156a8837c044b69d95 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 22 Dec 2025 23:56:43 +1000 Subject: [PATCH 21/25] Merge branch 'main' into refactor/fetch-issue-graphql Signed-off-by: Adam Setch --- src/renderer/utils/helpers.test.ts | 433 ----------------------------- 1 file changed, 433 deletions(-) diff --git a/src/renderer/utils/helpers.test.ts b/src/renderer/utils/helpers.test.ts index 5a01bff82..0bd6c7f3f 100644 --- a/src/renderer/utils/helpers.test.ts +++ b/src/renderer/utils/helpers.test.ts @@ -154,439 +154,6 @@ describe('renderer/utils/helpers.ts', () => { expect(getHtmlUrlSpy).toHaveBeenCalledWith(mockSubjectUrl, mockToken); expect(result).toBe(`${mockHtmlUrl}?${mockNotificationReferrer}`); }); - - // describe('Check Suite URLs', () => { - // it('successful workflow', async () => { - // const subject = { - // title: 'Demo workflow run succeeded for main branch', - // url: null, - // latest_comment_url: null, - // type: 'CheckSuite' as SubjectType, - // } as Subject; - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3ASUCCESS+branch%3Amain&${mockNotificationReferrer}`, - // ); - // }); - - // it('failed workflow', async () => { - // const subject = { - // title: 'Demo workflow run failed for main branch', - // url: null, - // latest_comment_url: null, - // type: 'CheckSuite' as SubjectType, - // } as Subject; - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3AFAILURE+branch%3Amain&${mockNotificationReferrer}`, - // ); - // }); - - // it('failed workflow multiple attempts', async () => { - // const subject = { - // title: 'Demo workflow run, Attempt #3 failed for main branch', - // url: null, - // latest_comment_url: null, - // type: 'CheckSuite' as SubjectType, - // } as Subject; - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3AFAILURE+branch%3Amain&${mockNotificationReferrer}`, - // ); - // }); - - // it('skipped workflow', async () => { - // const subject = { - // title: 'Demo workflow run skipped for main branch', - // url: null, - // latest_comment_url: null, - // type: 'CheckSuite' as SubjectType, - // } as Subject; - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3ASKIPPED+branch%3Amain&${mockNotificationReferrer}`, - // ); - // }); - - // it('unhandled workflow scenario', async () => { - // const subject = { - // title: 'unhandled workflow scenario', - // url: null, - // latest_comment_url: null, - // type: 'CheckSuite' as SubjectType, - // } as Subject; - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/actions?${mockNotificationReferrer}`, - // ); - // }); - - // it('unhandled status scenario', async () => { - // const subject = { - // title: 'Demo workflow run unhandled-status for main branch', - // url: null, - // latest_comment_url: null, - // type: 'CheckSuite' as SubjectType, - // } as Subject; - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+branch%3Amain&${mockNotificationReferrer}`, - // ); - // }); - - // it('unhandled check suite scenario', async () => { - // const subject = { - // title: 'Unhandled scenario', - // url: null, - // latest_comment_url: null, - // type: 'CheckSuite' as SubjectType, - // } as Subject; - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/actions?${mockNotificationReferrer}`, - // ); - // }); - // }); - - // describe('Discussions URLs', () => { - // const fetchDiscussionByNumberSpy = jest.spyOn( - // apiClient, - // 'fetchDiscussionByNumber', - // ); - - // it('when no discussion found via graphql api, default to base repository discussion url', async () => { - // const subject = { - // title: 'generate github web url unit tests', - // url: null, - // latest_comment_url: null, - // type: 'Discussion' as SubjectType, - // } as Subject; - - // fetchDiscussionByNumberSpy.mockResolvedValue({ - // data: { repository: { discussion: null } }, - // } as any); - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(fetchDiscussionByNumberSpy).toHaveBeenCalledTimes(1); - // expect(result).toBe( - // `${mockSingleNotification.repository.html_url}/discussions?${mockNotificationReferrer}`, - // ); - // }); - - // it('when error fetching discussion via graphql api, default to base repository discussion url', async () => { - // const rendererLogErrorSpy = jest - // .spyOn(logger, 'rendererLogError') - // .mockImplementation(); - - // const subject = { - // title: '1.16.0', - // url: null, - // latest_comment_url: null, - // type: 'Discussion' as SubjectType, - // } as Subject; - - // fetchDiscussionByNumberSpy.mockResolvedValue({ - // data: null, - // errors: [ - // { - // message: 'Something failed', - // }, - // ], - // } as unknown as ExecutionResult); - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(fetchDiscussionByNumberSpy).toHaveBeenCalledTimes(1); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/discussions?${mockNotificationReferrer}`, - // ); - // expect(rendererLogErrorSpy).toHaveBeenCalledTimes(1); - // }); - - // it('when discussion found via graphql api, link to matching discussion and comment hash', async () => { - // const subject = { - // title: '1.16.0', - // url: null, - // latest_comment_url: null, - // type: 'Discussion' as SubjectType, - // } as Subject; - - // fetchDiscussionByNumberSpy.mockResolvedValue({ - // data: mockDiscussionByNumberGraphQLResponse, - // } as ExecutionResult); - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(fetchDiscussionByNumberSpy).toHaveBeenCalledTimes(1); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/discussions/612?${mockNotificationReferrer}#discussioncomment-2300902`, - // ); - // }); - - // it('when api throws error, default to base repository discussion url', async () => { - // const rendererLogErrorSpy = jest - // .spyOn(logger, 'rendererLogError') - // .mockImplementation(); - - // const subject = { - // title: '1.16.0', - // url: null, - // latest_comment_url: null, - // type: 'Discussion' as SubjectType, - // } as Subject; - - // fetchDiscussionByNumberSpy.mockRejectedValue( - // new Error('Something failed'), - // ); - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(fetchDiscussionByNumberSpy).toHaveBeenCalledTimes(1); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/discussions?${mockNotificationReferrer}`, - // ); - // expect(rendererLogErrorSpy).toHaveBeenCalledTimes(1); - // }); - // }); - - // it('Repository Invitation url', async () => { - // const subject = { - // title: - // 'Invitation to join gitify-app/notifications-test from unit-tests', - // url: null, - // latest_comment_url: null, - // type: 'RepositoryInvitation' as SubjectType, - // } as Subject; - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/invitations?${mockNotificationReferrer}`, - // ); - // }); - - // it('Repository Dependabot Alerts Thread url', async () => { - // const subject = { - // title: 'Your repository has dependencies with security vulnerabilities', - // url: null, - // latest_comment_url: null, - // type: 'RepositoryDependabotAlertsThread' as SubjectType, - // }; - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/security/dependabot?${mockNotificationReferrer}`, - // ); - // }); - - // describe('Workflow Run URLs', () => { - // it('approval requested', async () => { - // const subject = { - // title: 'some-user requested your review to deploy to an environment', - // url: null, - // latest_comment_url: null, - // type: 'WorkflowRun' as SubjectType, - // } as Subject; - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/actions?query=is%3AWAITING&${mockNotificationReferrer}`, - // ); - // }); - - // it('unhandled status/action scenario', async () => { - // const subject = { - // title: - // 'some-user requested your unhandled-action to deploy to an environment', - // url: null, - // latest_comment_url: null, - // type: 'WorkflowRun' as SubjectType, - // } as Subject; - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/actions?${mockNotificationReferrer}`, - // ); - // }); - - // it('unhandled workflow scenario', async () => { - // const subject = { - // title: 'some unhandled scenario', - // url: null, - // latest_comment_url: null, - // type: 'WorkflowRun' as SubjectType, - // } as Subject; - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/actions?${mockNotificationReferrer}`, - // ); - // }); - // }); - - // describe('defaults web urls', () => { - // it('issues - defaults when no urls present in notification', async () => { - // const subject = { - // title: 'generate github web url unit tests', - // url: null, - // latest_comment_url: null, - // type: 'Issue' as SubjectType, - // } as Subject; - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/issues?${mockNotificationReferrer}`, - // ); - // }); - - // it('pull requests - defaults when no urls present in notification', async () => { - // const subject = { - // title: 'generate github web url unit tests', - // url: null, - // latest_comment_url: null, - // type: 'PullRequest' as SubjectType, - // } as Subject; - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/pulls?${mockNotificationReferrer}`, - // ); - // }); - - // it('other - defaults when no urls present in notification', async () => { - // const subject = { - // title: 'generate github web url unit tests', - // url: null, - // latest_comment_url: null, - // type: 'Commit' as SubjectType, - // } as Subject; - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(0); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test?${mockNotificationReferrer}`, - // ); - // }); - - // it('defaults when exception handled during specialized html enrichment process', async () => { - // const rendererLogErrorSpy = jest - // .spyOn(logger, 'rendererLogError') - // .mockImplementation(); - - // const subject = { - // title: 'generate github web url unit tests', - // url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link, - // latest_comment_url: null as Link, - // type: 'Issue' as SubjectType, - // } as Subject; - - // getHtmlUrlSpy.mockRejectedValue(new Error('Test error')); - - // const result = await generateGitHubWebUrl({ - // ...mockSingleNotification, - // subject: subject, - // }); - - // expect(getHtmlUrlSpy).toHaveBeenCalledTimes(1); - // expect(result).toBe( - // `https://github.com/gitify-app/notifications-test/issues?${mockNotificationReferrer}`, - // ); - // expect(rendererLogErrorSpy).toHaveBeenCalledTimes(1); - // }); - // }); }); describe('getChevronDetails', () => { From 18524e5f8a27654498f5a24644ecee43b9c77281 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Tue, 23 Dec 2025 07:07:24 +1000 Subject: [PATCH 22/25] simplify state filtering Signed-off-by: Adam Setch --- .../utils/notifications/filters/state.test.ts | 95 +++++++++---------- .../utils/notifications/filters/state.ts | 53 +++++------ .../notifications/handlers/index.test.ts | 30 +++--- 3 files changed, 85 insertions(+), 93 deletions(-) diff --git a/src/renderer/utils/notifications/filters/state.test.ts b/src/renderer/utils/notifications/filters/state.test.ts index 0aa631b2e..0e2db8750 100644 --- a/src/renderer/utils/notifications/filters/state.test.ts +++ b/src/renderer/utils/notifications/filters/state.test.ts @@ -1,3 +1,4 @@ +import type { FilterStateType, GitifyNotificationState } from '../../../types'; import type { Notification } from '../../../typesGitHub'; import { stateFilter } from './state'; @@ -6,56 +7,52 @@ describe('renderer/utils/notifications/filters/state.ts', () => { jest.clearAllMocks(); }); - it('can filter by notification states', () => { - const mockPartialNotification = { - subject: { - state: 'OPEN', - }, + describe('can filter by notification states', () => { + const mockNotification = { + subject: { state: 'OPEN' }, } as Partial as Notification; - // Open states - mockPartialNotification.subject.state = 'OPEN'; - expect( - stateFilter.filterNotification(mockPartialNotification, 'open'), - ).toBe(true); - - mockPartialNotification.subject.state = 'REOPENED'; - expect( - stateFilter.filterNotification(mockPartialNotification, 'open'), - ).toBe(true); - - // Closed states - mockPartialNotification.subject.state = 'CLOSED'; - expect( - stateFilter.filterNotification(mockPartialNotification, 'closed'), - ).toBe(true); - - mockPartialNotification.subject.state = 'COMPLETED'; - expect( - stateFilter.filterNotification(mockPartialNotification, 'closed'), - ).toBe(true); - - mockPartialNotification.subject.state = 'NOT_PLANNED'; - expect( - stateFilter.filterNotification(mockPartialNotification, 'closed'), - ).toBe(true); - - // Merged states - mockPartialNotification.subject.state = 'MERGED'; - expect( - stateFilter.filterNotification(mockPartialNotification, 'merged'), - ).toBe(true); - - // Draft states - mockPartialNotification.subject.state = 'DRAFT'; - expect( - stateFilter.filterNotification(mockPartialNotification, 'draft'), - ).toBe(true); - - // Other states - mockPartialNotification.subject.state = 'OUTDATED'; - expect( - stateFilter.filterNotification(mockPartialNotification, 'other'), - ).toBe(true); + const cases = { + OPEN: 'open', + REOPENED: 'open', + + CLOSED: 'closed', + COMPLETED: 'closed', + DUPLICATE: 'closed', + NOT_PLANNED: 'closed', + RESOLVED: 'closed', + + MERGED: 'merged', + DRAFT: 'draft', + + // Discussion-specific + ANSWERED: 'other', + OUTDATED: 'other', + + // Check suite / workflow states + ACTION_REQUIRED: 'other', + CANCELLED: 'other', + FAILURE: 'other', + IN_PROGRESS: 'other', + PENDING: 'other', + QUEUED: 'other', + REQUESTED: 'other', + SKIPPED: 'other', + STALE: 'other', + SUCCESS: 'other', + TIMED_OUT: 'other', + WAITING: 'other', + } satisfies Record; + + it.each( + Object.entries(cases) as Array< + [GitifyNotificationState, FilterStateType] + >, + )('filter notification with state %s as %s', (notificationState, expectedFilter) => { + mockNotification.subject.state = notificationState; + expect( + stateFilter.filterNotification(mockNotification, expectedFilter), + ).toBe(true); + }); }); }); diff --git a/src/renderer/utils/notifications/filters/state.ts b/src/renderer/utils/notifications/filters/state.ts index d7be8f486..2fc5451dc 100644 --- a/src/renderer/utils/notifications/filters/state.ts +++ b/src/renderer/utils/notifications/filters/state.ts @@ -21,7 +21,7 @@ const STATE_TYPE_DETAILS: Record = { }, closed: { title: 'Closed', - description: 'Closed, completed or not planned', + description: 'Closed, completed, duplicate, resolved or not planned', }, other: { title: 'Other', @@ -64,32 +64,29 @@ export const stateFilter: Filter = { notification: Notification, stateType: FilterStateType, ): boolean { - const allOpenStates: GitifyNotificationState[] = ['OPEN', 'REOPENED']; - const allClosedStates: GitifyNotificationState[] = [ - 'CLOSED', - 'COMPLETED', - 'NOT_PLANNED', - ]; - const allMergedStates: GitifyNotificationState[] = ['MERGED']; - const allDraftStates: GitifyNotificationState[] = ['DRAFT']; - const allFilterableStates = [ - ...allOpenStates, - ...allClosedStates, - ...allMergedStates, - ...allDraftStates, - ]; - - switch (stateType) { - case 'open': - return allOpenStates.includes(notification.subject?.state); - case 'closed': - return allClosedStates.includes(notification.subject?.state); - case 'merged': - return allMergedStates.includes(notification.subject?.state); - case 'draft': - return allDraftStates.includes(notification.subject?.state); - default: - return !allFilterableStates.includes(notification.subject?.state); - } + const mapped = mapStateToFilter(notification.subject?.state); + return stateType === mapped; }, }; + +function mapStateToFilter( + state: GitifyNotificationState | null | undefined, +): FilterStateType { + switch (state) { + case 'OPEN': + case 'REOPENED': + return 'open'; + case 'CLOSED': + case 'COMPLETED': + case 'DUPLICATE': + case 'NOT_PLANNED': + case 'RESOLVED': + return 'closed'; + case 'MERGED': + return 'merged'; + case 'DRAFT': + return 'draft'; + default: + return 'other'; + } +} diff --git a/src/renderer/utils/notifications/handlers/index.test.ts b/src/renderer/utils/notifications/handlers/index.test.ts index f06a83e99..c30155a9c 100644 --- a/src/renderer/utils/notifications/handlers/index.test.ts +++ b/src/renderer/utils/notifications/handlers/index.test.ts @@ -11,28 +11,26 @@ import { releaseHandler } from './release'; import { repositoryDependabotAlertsThreadHandler } from './repositoryDependabotAlertsThread'; import { repositoryInvitationHandler } from './repositoryInvitation'; import { repositoryVulnerabilityAlertHandler } from './repositoryVulnerabilityAlert'; +import type { NotificationTypeHandler } from './types'; import { workflowRunHandler } from './workflowRun'; describe('renderer/utils/notifications/handlers/index.ts', () => { describe('createNotificationHandler', () => { - const cases: Array<[SubjectType, object]> = [ - ['CheckSuite', checkSuiteHandler], - ['Commit', commitHandler], - ['Discussion', discussionHandler], - ['Issue', issueHandler], - ['PullRequest', pullRequestHandler], - ['Release', releaseHandler], - [ - 'RepositoryDependabotAlertsThread', - repositoryDependabotAlertsThreadHandler, - ], - ['RepositoryInvitation', repositoryInvitationHandler], - ['RepositoryVulnerabilityAlert', repositoryVulnerabilityAlertHandler], - ['WorkflowRun', workflowRunHandler], - ]; + const cases = { + CheckSuite: checkSuiteHandler, + Commit: commitHandler, + Discussion: discussionHandler, + Issue: issueHandler, + PullRequest: pullRequestHandler, + Release: releaseHandler, + RepositoryDependabotAlertsThread: repositoryDependabotAlertsThreadHandler, + RepositoryInvitation: repositoryInvitationHandler, + RepositoryVulnerabilityAlert: repositoryVulnerabilityAlertHandler, + WorkflowRun: workflowRunHandler, + } satisfies Record; it.each( - cases, + Object.entries(cases) as Array<[SubjectType, NotificationTypeHandler]>, )('returns expected handler instance for %s', (type, expected) => { const notification = createPartialMockNotification({ type }); const handler = createNotificationHandler(notification); From 8429aa977526b3bed512ade656a8117af400b6c6 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Tue, 23 Dec 2025 08:39:28 +1000 Subject: [PATCH 23/25] refactor test suites Signed-off-by: Adam Setch --- .../utils/api/__mocks__/response-mocks.ts | 55 --- .../utils/api/graphql/discussion.graphql | 1 + .../utils/api/graphql/generated/gql.ts | 6 +- .../utils/api/graphql/generated/graphql.ts | 3 +- .../notifications/handlers/checkSuite.test.ts | 130 +++-- .../notifications/handlers/discussion.test.ts | 328 +++++++------ .../notifications/handlers/discussion.ts | 4 +- .../notifications/handlers/issue.test.ts | 395 +++++----------- .../utils/notifications/handlers/issue.ts | 4 + .../handlers/pullRequest.test.ts | 444 ++++++------------ 10 files changed, 478 insertions(+), 892 deletions(-) diff --git a/src/renderer/utils/api/__mocks__/response-mocks.ts b/src/renderer/utils/api/__mocks__/response-mocks.ts index 6957a2a3c..dd2e8d4ad 100644 --- a/src/renderer/utils/api/__mocks__/response-mocks.ts +++ b/src/renderer/utils/api/__mocks__/response-mocks.ts @@ -4,7 +4,6 @@ import { } from '../../../__mocks__/account-mocks'; import type { Link } from '../../../types'; import type { Notification, Repository, User } from '../../../typesGitHub'; -import type { FetchDiscussionByNumberQuery } from '../graphql/generated/graphql'; export const mockNotificationUser: User = { login: 'octocat', @@ -368,58 +367,4 @@ export const mockEnterpriseNotifications: Notification[] = [ }, ]; -export const mockDiscussionByNumberGraphQLResponse: FetchDiscussionByNumberQuery = - { - repository: { - discussion: { - __typename: 'Discussion', - number: 123, - title: '1.16.0', - isAnswered: false, - stateReason: null, - url: 'https://github.com/gitify-app/notifications-test/discussions/612' as Link, - author: { - login: 'comment-user', - html_url: 'https://github.com/comment-user' as Link, - avatar_url: - 'https://avatars.githubusercontent.com/u/123456789?v=4' as Link, - type: 'User', - }, - comments: { - totalCount: 2, - nodes: [ - { - databaseId: 2258799, - createdAt: '2017-02-20T17:51:57Z', - author: { - login: 'comment-user', - html_url: 'https://github.com/comment-user' as Link, - avatar_url: - 'https://avatars.githubusercontent.com/u/123456789?v=4' as Link, - type: 'User', - }, - url: 'https://github.com/gitify-app/notifications-test/discussions/612#discussioncomment-67890', - replies: { - nodes: [ - { - databaseId: 2300902, - createdAt: '2017-05-20T17:51:57Z', - author: { - login: 'reply-user', - html_url: 'https://github.com/reply-user' as Link, - avatar_url: - 'https://avatars.githubusercontent.com/u/123456789?v=4' as Link, - type: 'User', - }, - url: 'https://github.com/gitify-app/notifications-test/discussions/612#discussioncomment-12345', - }, - ], - }, - }, - ], - }, - }, - }, - }; - export const mockSingleNotification: Notification = mockGitHubNotifications[0]; diff --git a/src/renderer/utils/api/graphql/discussion.graphql b/src/renderer/utils/api/graphql/discussion.graphql index 1095fc98a..2dbe24e1a 100644 --- a/src/renderer/utils/api/graphql/discussion.graphql +++ b/src/renderer/utils/api/graphql/discussion.graphql @@ -23,6 +23,7 @@ query FetchDiscussionByNumber( nodes { ...CommentFields replies(last: $lastReplies) { + totalCount nodes { ...CommentFields } diff --git a/src/renderer/utils/api/graphql/generated/gql.ts b/src/renderer/utils/api/graphql/generated/gql.ts index a3a5c4b74..1e0234ae7 100644 --- a/src/renderer/utils/api/graphql/generated/gql.ts +++ b/src/renderer/utils/api/graphql/generated/gql.ts @@ -16,13 +16,13 @@ import * as types from './graphql'; */ type Documents = { "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}": typeof types.AuthorFieldsFragmentDoc, - "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}": typeof types.FetchDiscussionByNumberDocument, + "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n totalCount\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}": typeof types.FetchDiscussionByNumberDocument, "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchIssueByNumberDocument, "query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: $lastReviews) {\n totalCount\n nodes {\n state\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: $firstClosingIssues) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.FetchPullRequestByNumberDocument, }; const documents: Documents = { "fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}": types.AuthorFieldsFragmentDoc, - "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}": types.FetchDiscussionByNumberDocument, + "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n totalCount\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}": types.FetchDiscussionByNumberDocument, "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchIssueByNumberDocument, "query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: $lastReviews) {\n totalCount\n nodes {\n state\n author {\n login\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: $firstClosingIssues) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.FetchPullRequestByNumberDocument, }; @@ -34,7 +34,7 @@ export function graphql(source: "fragment AuthorFields on Actor {\n login\n ht /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}"): typeof import('./graphql').FetchDiscussionByNumberDocument; +export function graphql(source: "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...CommentFields\n replies(last: $lastReplies) {\n totalCount\n nodes {\n ...CommentFields\n }\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}"): typeof import('./graphql').FetchDiscussionByNumberDocument; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/src/renderer/utils/api/graphql/generated/graphql.ts b/src/renderer/utils/api/graphql/generated/graphql.ts index 679ea040b..89a34f8ef 100644 --- a/src/renderer/utils/api/graphql/generated/graphql.ts +++ b/src/renderer/utils/api/graphql/generated/graphql.ts @@ -35929,7 +35929,7 @@ export type FetchDiscussionByNumberQuery = { __typename?: 'Query', repository?: | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } | { __typename?: 'Organization', login: string, html_url: any, avatar_url: any, type: 'Organization' } | { __typename?: 'User', login: string, html_url: any, avatar_url: any, type: 'User' } - | null, comments: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, replies: { __typename?: 'DiscussionCommentConnection', nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, author?: + | null, comments: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, replies: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, author?: | { __typename?: 'Bot', login: string, html_url: any, avatar_url: any, type: 'Bot' } | { __typename?: 'EnterpriseUserAccount', login: string, html_url: any, avatar_url: any, type: 'EnterpriseUserAccount' } | { __typename?: 'Mannequin', login: string, html_url: any, avatar_url: any, type: 'Mannequin' } @@ -36072,6 +36072,7 @@ export const FetchDiscussionByNumberDocument = new TypedDocumentString(` nodes { ...CommentFields replies(last: $lastReplies) { + totalCount nodes { ...CommentFields } diff --git a/src/renderer/utils/notifications/handlers/checkSuite.test.ts b/src/renderer/utils/notifications/handlers/checkSuite.test.ts index 27b3de0fe..1a992c7c6 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.test.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.test.ts @@ -3,7 +3,11 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; -import { IconColor, type Link } from '../../../types'; +import { + type GitifyCheckSuiteStatus, + IconColor, + type Link, +} from '../../../types'; import type { Notification } from '../../../typesGitHub'; import { checkSuiteHandler, getCheckSuiteAttributes } from './checkSuite'; @@ -152,80 +156,60 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { }); }); - it('iconType', () => { - expect( - checkSuiteHandler.iconType( - createMockSubject({ type: 'CheckSuite', state: null }), - ).displayName, - ).toBe('RocketIcon'); - - expect( - checkSuiteHandler.iconType( - createMockSubject({ - type: 'CheckSuite', - state: 'CANCELLED', - }), - ).displayName, - ).toBe('StopIcon'); - - expect( - checkSuiteHandler.iconType( - createMockSubject({ - type: 'CheckSuite', - state: 'FAILURE', - }), - ).displayName, - ).toBe('XIcon'); - - expect( - checkSuiteHandler.iconType( - createMockSubject({ - type: 'CheckSuite', - state: 'SKIPPED', - }), - ).displayName, - ).toBe('SkipIcon'); - - expect( - checkSuiteHandler.iconType( - createMockSubject({ - type: 'CheckSuite', - state: 'SUCCESS', - }), - ).displayName, - ).toBe('CheckIcon'); + describe('iconType', () => { + const cases = { + ACTION_REQUIRED: 'RocketIcon', + CANCELLED: 'StopIcon', + COMPLETED: 'RocketIcon', + FAILURE: 'XIcon', + IN_PROGRESS: 'RocketIcon', + PENDING: 'RocketIcon', + QUEUED: 'RocketIcon', + REQUESTED: 'RocketIcon', + SKIPPED: 'SkipIcon', + STALE: 'RocketIcon', + SUCCESS: 'CheckIcon', + TIMED_OUT: 'RocketIcon', + WAITING: 'RocketIcon', + } satisfies Record; + + it.each( + Object.entries(cases) as Array<[GitifyCheckSuiteStatus, IconColor]>, + )('iconType for check suite with status %s', (checkSuiteStatus, checkSuiteIconType) => { + expect( + checkSuiteHandler.iconType( + createMockSubject({ type: 'CheckSuite', state: checkSuiteStatus }), + ).displayName, + ).toBe(checkSuiteIconType); + }); }); - it('iconColor', () => { - expect( - checkSuiteHandler.iconColor( - createMockSubject({ type: 'CheckSuite', state: 'SUCCESS' }), - ), - ).toBe(IconColor.GREEN); - - expect( - checkSuiteHandler.iconColor( - createMockSubject({ type: 'CheckSuite', state: 'FAILURE' }), - ), - ).toBe(IconColor.RED); - - expect( - checkSuiteHandler.iconColor( - createMockSubject({ type: 'CheckSuite', state: 'CANCELLED' }), - ), - ).toBe(IconColor.GRAY); - - expect( - checkSuiteHandler.iconColor( - createMockSubject({ type: 'CheckSuite', state: 'SKIPPED' }), - ), - ).toBe(IconColor.GRAY); - - expect( - checkSuiteHandler.iconColor( - createMockSubject({ type: 'CheckSuite', state: null }), - ), - ).toBe(IconColor.GRAY); + describe('iconColor', () => { + const cases = { + ACTION_REQUIRED: IconColor.GRAY, + CANCELLED: IconColor.GRAY, + COMPLETED: IconColor.GRAY, + FAILURE: IconColor.RED, + IN_PROGRESS: IconColor.GRAY, + PENDING: IconColor.GRAY, + QUEUED: IconColor.GRAY, + REQUESTED: IconColor.GRAY, + SKIPPED: IconColor.GRAY, + STALE: IconColor.GRAY, + SUCCESS: IconColor.GREEN, + TIMED_OUT: IconColor.GRAY, + WAITING: IconColor.GRAY, + } satisfies Record; + + it.each( + Object.entries(cases) as Array<[GitifyCheckSuiteStatus, IconColor]>, + )('iconColor for check suite with status %s', (checkSuiteStatus, checkSuiteIconColor) => { + expect( + checkSuiteHandler.iconColor( + createMockSubject({ type: 'CheckSuite', state: checkSuiteStatus }), + ), + ).toBe(checkSuiteIconColor); + }); }); it('defaultUrl', () => { diff --git a/src/renderer/utils/notifications/handlers/discussion.test.ts b/src/renderer/utils/notifications/handlers/discussion.test.ts index 144e09944..69ab51b9c 100644 --- a/src/renderer/utils/notifications/handlers/discussion.test.ts +++ b/src/renderer/utils/notifications/handlers/discussion.test.ts @@ -6,11 +6,15 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; -import { type GitifySubject, IconColor, type Link } from '../../../types'; -import type { Notification, Owner, Repository } from '../../../typesGitHub'; +import { createPartialMockUser } from '../../../__mocks__/user-mocks'; +import { + type GitifyDiscussionState, + type GitifySubject, + IconColor, + type Link, +} from '../../../types'; +import type { Notification } from '../../../typesGitHub'; import type { - AuthorFieldsFragment, - Discussion, DiscussionStateReason, FetchDiscussionByNumberQuery, } from '../../api/graphql/generated/graphql'; @@ -19,33 +23,19 @@ import { discussionHandler } from './discussion'; type DiscussionResponse = FetchDiscussionByNumberQuery['repository']['discussion']; -const mockDiscussionAuthor: AuthorFieldsFragment = { - login: 'discussion-author', - html_url: 'https://github.com/discussion-author' as Link, - avatar_url: 'https://avatars.githubusercontent.com/u/123456789?v=4' as Link, - type: 'User', -}; +const mockAuthor = createPartialMockUser('discussion-author'); +const mockCommenter = createPartialMockUser('discussion-commenter'); +const mockReplier = createPartialMockUser('discussion-replier'); describe('renderer/utils/notifications/handlers/discussion.ts', () => { describe('enrich', () => { - const partialOwner: Partial = { - login: 'gitify-app', - }; - - const partialRepository: Partial = { - full_name: 'gitify-app/notifications-test', - owner: partialOwner as Owner, - }; - const mockNotification = createPartialMockNotification({ title: 'This is a mock discussion', type: 'Discussion', url: 'https://api.github.com/repos/gitify-app/notifications-test/discussions/123' as Link, + latest_comment_url: null, }); mockNotification.updated_at = '2024-01-01T00:00:00Z'; - mockNotification.repository = { - ...(partialRepository as Repository), - }; beforeEach(() => { // axios will default to using the XHR adapter which can't be intercepted @@ -53,13 +43,15 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { axios.defaults.adapter = 'http'; }); - it('answered discussion state', async () => { + it('answered discussion state - no stateReason', async () => { + const mockDiscussion = mockDiscussionResponseNode({ isAnswered: true }); + nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - discussion: mockDiscussionNode(null, true), + discussion: mockDiscussion, }, }, }); @@ -73,25 +65,27 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { number: 123, state: 'ANSWERED', user: { - login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.html_url, - avatar_url: mockDiscussionAuthor.avatar_url, - type: mockDiscussionAuthor.type, + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, }, comments: 0, labels: [], htmlUrl: - 'https://github.com/gitify-app/notifications-test/discussions/1', + 'https://github.com/gitify-app/notifications-test/discussions/123', } as GitifySubject); }); - it('duplicate discussion state', async () => { + it('open / unanswered discussion - no stateReason', async () => { + const mockDiscussion = mockDiscussionResponseNode({ isAnswered: false }); + nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - discussion: mockDiscussionNode('DUPLICATE', false), + discussion: mockDiscussion, }, }, }); @@ -103,27 +97,32 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { expect(result).toEqual({ number: 123, - state: 'DUPLICATE', + state: 'OPEN', user: { - login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.html_url, - avatar_url: mockDiscussionAuthor.avatar_url, - type: mockDiscussionAuthor.type, + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, }, comments: 0, labels: [], htmlUrl: - 'https://github.com/gitify-app/notifications-test/discussions/1', + 'https://github.com/gitify-app/notifications-test/discussions/123', } as GitifySubject); }); - it('open discussion state', async () => { + it('discussion with stateReason - stateReason always takes precedence', async () => { + const mockDiscussion = mockDiscussionResponseNode({ + isAnswered: true, + stateReason: 'DUPLICATE', + }); + nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - discussion: mockDiscussionNode(null, false), + discussion: mockDiscussion, }, }, }); @@ -135,27 +134,36 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { expect(result).toEqual({ number: 123, - state: 'OPEN', + state: 'DUPLICATE', user: { - login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.html_url, - avatar_url: mockDiscussionAuthor.avatar_url, - type: mockDiscussionAuthor.type, + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, }, comments: 0, labels: [], htmlUrl: - 'https://github.com/gitify-app/notifications-test/discussions/1', + 'https://github.com/gitify-app/notifications-test/discussions/123', } as GitifySubject); }); - it('outdated discussion state', async () => { + it('discussion with labels', async () => { + const mockDiscussion = mockDiscussionResponseNode({ isAnswered: true }); + mockDiscussion.labels = { + nodes: [ + { + name: 'enhancement', + }, + ], + }; + nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - discussion: mockDiscussionNode('OUTDATED', false), + discussion: mockDiscussion, }, }, }); @@ -167,59 +175,43 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { expect(result).toEqual({ number: 123, - state: 'OUTDATED', + state: 'ANSWERED', user: { - login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.html_url, - avatar_url: mockDiscussionAuthor.avatar_url, - type: mockDiscussionAuthor.type, + login: mockAuthor.login, + html_url: mockAuthor.html_url, + avatar_url: mockAuthor.avatar_url, + type: mockAuthor.type, }, comments: 0, - labels: [], + labels: ['enhancement'], htmlUrl: - 'https://github.com/gitify-app/notifications-test/discussions/1', + 'https://github.com/gitify-app/notifications-test/discussions/123', } as GitifySubject); }); - it('reopened discussion state', async () => { - nock('https://api.github.com') - .post('/graphql') - .reply(200, { - data: { - repository: { - discussion: mockDiscussionNode('REOPENED', false), + it('discussion with comments', async () => { + const mockDiscussion = mockDiscussionResponseNode({ isAnswered: true }); + mockDiscussion.comments = { + totalCount: 1, + nodes: [ + { + author: mockCommenter, + createdAt: '2024-02-01T00:00:00Z', + url: 'https://github.com/gitify-app/notifications-test/discussions/123#discussioncomment-1234', + replies: { + totalCount: 0, + nodes: [], }, }, - }); - - const result = await discussionHandler.enrich( - mockNotification, - mockSettings, - ); - - expect(result).toEqual({ - number: 123, - state: 'REOPENED', - user: { - login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.html_url, - avatar_url: mockDiscussionAuthor.avatar_url, - type: mockDiscussionAuthor.type, - }, - comments: 0, - labels: [], - htmlUrl: - 'https://github.com/gitify-app/notifications-test/discussions/1', - } as GitifySubject); - }); + ], + }; - it('resolved discussion state', async () => { nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - discussion: mockDiscussionNode('RESOLVED', true), + discussion: mockDiscussion, }, }, }); @@ -231,37 +223,49 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { expect(result).toEqual({ number: 123, - state: 'RESOLVED', + state: 'ANSWERED', user: { - login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.html_url, - avatar_url: mockDiscussionAuthor.avatar_url, - type: mockDiscussionAuthor.type, + login: mockCommenter.login, + html_url: mockCommenter.html_url, + avatar_url: mockCommenter.avatar_url, + type: mockCommenter.type, }, - comments: 0, + comments: 1, labels: [], htmlUrl: - 'https://github.com/gitify-app/notifications-test/discussions/1', + 'https://github.com/gitify-app/notifications-test/discussions/123#discussioncomment-1234', } as GitifySubject); }); - it('discussion with labels', async () => { - const mockDiscussion = mockDiscussionNode(null, true); - mockDiscussion.labels = { + it('discussion with comments and replies', async () => { + const mockDiscussion = mockDiscussionResponseNode({ isAnswered: true }); + mockDiscussion.comments = { + totalCount: 1, nodes: [ { - name: 'enhancement', + author: mockCommenter, + createdAt: '2024-01-01T00:00:00Z', + url: 'https://github.com/gitify-app/notifications-test/discussions/123#discussioncomment-1234', + replies: { + totalCount: 1, + nodes: [ + { + author: mockReplier, + createdAt: '2024-01-01T00:00:00Z', + url: 'https://github.com/gitify-app/notifications-test/discussions/123#discussioncomment-6789', + }, + ], + }, }, ], - } as Partial['labels']; + }; + nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - discussion: { - ...mockDiscussion, - }, + discussion: mockDiscussion, }, }, }); @@ -275,74 +279,59 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { number: 123, state: 'ANSWERED', user: { - login: mockDiscussionAuthor.login, - html_url: mockDiscussionAuthor.html_url, - avatar_url: mockDiscussionAuthor.avatar_url, - type: mockDiscussionAuthor.type, + login: mockReplier.login, + html_url: mockReplier.html_url, + avatar_url: mockReplier.avatar_url, + type: mockReplier.type, }, - comments: 0, - labels: ['enhancement'], + comments: 1, + labels: [], htmlUrl: - 'https://github.com/gitify-app/notifications-test/discussions/1', + 'https://github.com/gitify-app/notifications-test/discussions/123#discussioncomment-6789', } as GitifySubject); }); }); - it('iconType', () => { - expect( - discussionHandler.iconType(createMockSubject({ type: 'Discussion' })) - .displayName, - ).toBe('CommentDiscussionIcon'); - - expect( - discussionHandler.iconType( - createMockSubject({ type: 'Discussion', state: 'DUPLICATE' }), - ).displayName, - ).toBe('DiscussionDuplicateIcon'); - - expect( - discussionHandler.iconType( - createMockSubject({ type: 'Discussion', state: 'OUTDATED' }), - ).displayName, - ).toBe('DiscussionOutdatedIcon'); - - expect( - discussionHandler.iconType( - createMockSubject({ type: 'Discussion', state: 'RESOLVED' }), - ).displayName, - ).toBe('DiscussionClosedIcon'); + describe('iconType', () => { + const cases = { + ANSWERED: 'CommentDiscussionIcon', + DUPLICATE: 'DiscussionDuplicateIcon', + OPEN: 'CommentDiscussionIcon', + OUTDATED: 'DiscussionOutdatedIcon', + REOPENED: 'CommentDiscussionIcon', + RESOLVED: 'DiscussionClosedIcon', + } satisfies Record; + + it.each( + Object.entries(cases) as Array<[GitifyDiscussionState, IconColor]>, + )('iconType for discussion with state %s', (discussionState, discussionIconType) => { + expect( + discussionHandler.iconType( + createMockSubject({ type: 'Discussion', state: discussionState }), + ).displayName, + ).toBe(discussionIconType); + }); }); - it('iconColor', () => { - expect( - discussionHandler.iconColor( - createMockSubject({ type: 'Discussion', state: 'ANSWERED' }), - ), - ).toBe(IconColor.GREEN); - - expect( - discussionHandler.iconColor( - createMockSubject({ type: 'Discussion', state: 'RESOLVED' }), - ), - ).toBe(IconColor.PURPLE); - - expect( - discussionHandler.iconColor( - createMockSubject({ type: 'Discussion', state: 'DUPLICATE' }), - ), - ).toBe(IconColor.GRAY); - - expect( - discussionHandler.iconColor( - createMockSubject({ type: 'Discussion', state: 'OUTDATED' }), - ), - ).toBe(IconColor.GRAY); - - expect( - discussionHandler.iconColor( - createMockSubject({ type: 'Discussion', state: 'OPEN' }), - ), - ).toBe(IconColor.GRAY); + describe('iconColor', () => { + const cases = { + ANSWERED: IconColor.GREEN, + DUPLICATE: IconColor.GRAY, + OPEN: IconColor.GRAY, + OUTDATED: IconColor.GRAY, + REOPENED: IconColor.GRAY, + RESOLVED: IconColor.PURPLE, + } satisfies Record; + + it.each( + Object.entries(cases) as Array<[GitifyDiscussionState, IconColor]>, + )('iconColor for discussion with state %s', (discussionState, discussionIconColor) => { + expect( + discussionHandler.iconColor( + createMockSubject({ type: 'Discussion', state: discussionState }), + ), + ).toBe(discussionIconColor); + }); }); it('defaultUrl', () => { @@ -359,21 +348,22 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { }); }); -function mockDiscussionNode( - state: DiscussionStateReason, - isAnswered: boolean, -): Partial { +function mockDiscussionResponseNode(mocks: { + stateReason?: DiscussionStateReason; + isAnswered: boolean; +}): DiscussionResponse { return { + __typename: 'Discussion', number: 123, title: 'This is a mock discussion', - url: 'https://github.com/gitify-app/notifications-test/discussions/1' as Link, - stateReason: state, - isAnswered: isAnswered, - author: mockDiscussionAuthor, + url: 'https://github.com/gitify-app/notifications-test/discussions/123' as Link, + stateReason: mocks.stateReason, + isAnswered: mocks.isAnswered, + author: mockAuthor, comments: { nodes: [], totalCount: 0, }, labels: null, - } as Partial; + }; } diff --git a/src/renderer/utils/notifications/handlers/discussion.ts b/src/renderer/utils/notifications/handlers/discussion.ts index b04d236a0..cdd38b527 100644 --- a/src/renderer/utils/notifications/handlers/discussion.ts +++ b/src/renderer/utils/notifications/handlers/discussion.ts @@ -95,7 +95,7 @@ class DiscussionHandler extends DefaultHandler { } iconColor(subject: Subject): IconColor { - switch (subject.state as GitifyDiscussionState) { + switch (subject.state) { case 'ANSWERED': return IconColor.GREEN; case 'RESOLVED': @@ -125,8 +125,8 @@ export function getClosestDiscussionCommentOrReply( const targetTimestamp = notification.updated_at; const allCommentsAndReplies = comments.flatMap((comment) => [ - comment, ...comment.replies.nodes, + comment, ]); // Find the closest match using the target timestamp diff --git a/src/renderer/utils/notifications/handlers/issue.test.ts b/src/renderer/utils/notifications/handlers/issue.test.ts index 3542d2f2f..e9bb70804 100644 --- a/src/renderer/utils/notifications/handlers/issue.test.ts +++ b/src/renderer/utils/notifications/handlers/issue.test.ts @@ -7,18 +7,27 @@ import { } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; import { createPartialMockUser } from '../../../__mocks__/user-mocks'; -import { type GitifySubject, IconColor, type Link } from '../../../types'; +import { + type GitifyIssueState, + type GitifySubject, + IconColor, + type Link, +} from '../../../types'; import type { Notification } from '../../../typesGitHub'; -import type { FetchIssueByNumberQuery } from '../../api/graphql/generated/graphql'; +import type { + FetchIssueByNumberQuery, + IssueState, + IssueStateReason, +} from '../../api/graphql/generated/graphql'; import { issueHandler } from './issue'; type IssueResponse = FetchIssueByNumberQuery['repository']['issue']; +const mockAuthor = createPartialMockUser('issue-author'); +const mockCommenter = createPartialMockUser('issue-commenter'); + describe('renderer/utils/notifications/handlers/issue.ts', () => { describe('enrich', () => { - const mockAuthor = createPartialMockUser('some-author'); - const mockCommenter = createPartialMockUser('some-commenter'); - let mockNotification: Notification; beforeEach(() => { @@ -35,23 +44,17 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { axios.defaults.adapter = 'http'; }); - it('open issue state', async () => { + it('issue with only state', async () => { + const mockIssue = mockIssueResponseNode({ + state: 'OPEN', + }); + nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - issue: { - __typename: 'Issue', - number: 123, - title: 'PR Title', - state: 'OPEN', - url: 'https://github.com/gitify-app/notifications-test/issues/123', - author: mockAuthor, - labels: { nodes: [] }, - comments: { totalCount: 0, nodes: [] }, - milestone: null, - } as IssueResponse, + issue: mockIssue, }, }, }); @@ -74,63 +77,18 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { } as GitifySubject); }); - it('closed issue state', async () => { - nock('https://api.github.com') - .post('/graphql') - .reply(200, { - data: { - repository: { - issue: { - __typename: 'Issue', - number: 123, - title: 'PR Title', - state: 'CLOSED', - url: 'https://github.com/gitify-app/notifications-test/issues/123', - author: mockAuthor, - labels: { nodes: [] }, - comments: { totalCount: 0, nodes: [] }, - milestone: null, - } as IssueResponse, - }, - }, - }); - - const result = await issueHandler.enrich(mockNotification, mockSettings); - - expect(result).toEqual({ - number: 123, + it('issue with stateReason - prefer stateReason over state when available', async () => { + const mockIssue = mockIssueResponseNode({ state: 'CLOSED', - user: { - login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, - type: mockAuthor.type, - }, - comments: 0, - htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123', - labels: [], - milestone: null, - } as GitifySubject); - }); + stateReason: 'COMPLETED', + }); - it('completed issue state', async () => { nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - issue: { - __typename: 'Issue', - number: 123, - title: 'PR Title', - state: 'CLOSED', - stateReason: 'COMPLETED', - url: 'https://github.com/gitify-app/notifications-test/issues/123', - author: mockAuthor, - labels: { nodes: [] }, - comments: { totalCount: 0, nodes: [] }, - milestone: null, - } as IssueResponse, + issue: mockIssue, }, }, }); @@ -153,113 +111,26 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { } as GitifySubject); }); - it('not_planned issue state', async () => { - nock('https://api.github.com') - .post('/graphql') - .reply(200, { - data: { - repository: { - issue: { - __typename: 'Issue', - number: 123, - title: 'PR Title', - state: 'CLOSED', - stateReason: 'NOT_PLANNED', - url: 'https://github.com/gitify-app/notifications-test/issues/123', - author: mockAuthor, - labels: { nodes: [] }, - comments: { totalCount: 0, nodes: [] }, - milestone: null, - } as IssueResponse, - }, - }, - }); - - const result = await issueHandler.enrich(mockNotification, mockSettings); - - expect(result).toEqual({ - number: 123, - state: 'NOT_PLANNED', - user: { - login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, - type: mockAuthor.type, - }, - comments: 0, - htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123', - labels: [], - milestone: null, - } as GitifySubject); - }); - - it('reopened issue state', async () => { - nock('https://api.github.com') - .post('/graphql') - .reply(200, { - data: { - repository: { - issue: { - __typename: 'Issue', - number: 123, - title: 'PR Title', - state: 'OPEN', - stateReason: 'REOPENED', - url: 'https://github.com/gitify-app/notifications-test/issues/123', - author: mockAuthor, - labels: { nodes: [] }, - comments: { totalCount: 0, nodes: [] }, - milestone: null, - } as IssueResponse, - }, + it('issue with comments', async () => { + const mockIssue = mockIssueResponseNode({ + state: 'OPEN', + }); + mockIssue.comments = { + totalCount: 1, + nodes: [ + { + author: mockCommenter, + url: 'https://github.com/gitify-app/notifications-test/issues/123#issuecomment-1234', }, - }); - - const result = await issueHandler.enrich(mockNotification, mockSettings); + ], + }; - expect(result).toEqual({ - number: 123, - state: 'REOPENED', - user: { - login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, - type: mockAuthor.type, - }, - comments: 0, - htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123', - labels: [], - milestone: null, - } as GitifySubject); - }); - - it('with comments', async () => { nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - issue: { - __typename: 'Issue', - number: 123, - title: 'PR Title', - state: 'OPEN', - url: 'https://github.com/gitify-app/notifications-test/issues/123', - author: mockAuthor, - labels: { - nodes: [], - }, - comments: { - totalCount: 1, - nodes: [ - { - author: mockCommenter, - url: 'https://github.com/gitify-app/notifications-test/issues/123#issuecomment-1234', - }, - ], - }, - milestone: null, - } as IssueResponse, + issue: mockIssue, }, }, }); @@ -284,28 +155,19 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { }); it('with labels', async () => { + const mockIssue = mockIssueResponseNode({ + state: 'OPEN', + }); + mockIssue.labels = { + nodes: [{ name: 'enhancement' }], + }; + nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - issue: { - __typename: 'Issue', - number: 123, - title: 'PR Title', - state: 'OPEN', - url: 'https://github.com/gitify-app/notifications-test/issues/123', - author: mockAuthor, - labels: { - nodes: [ - { - name: 'enhancement', - }, - ], - }, - comments: { totalCount: 0, nodes: [] }, - milestone: null, - } as IssueResponse, + issue: mockIssue, }, }, }); @@ -329,27 +191,20 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { }); it('with milestone', async () => { + const mockIssue = mockIssueResponseNode({ + state: 'OPEN', + }); + mockIssue.milestone = { + state: 'OPEN', + title: 'Open Milestone', + }; + nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - issue: { - __typename: 'Issue', - number: 123, - title: 'PR Title', - state: 'OPEN', - url: 'https://github.com/gitify-app/notifications-test/issues/123', - author: mockAuthor, - labels: { - nodes: [], - }, - comments: { totalCount: 0, nodes: [] }, - milestone: { - state: 'OPEN', - title: 'Open Milestone', - }, - } as IssueResponse, + issue: mockIssue, }, }, }); @@ -376,92 +231,46 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { }); }); - it('iconType', () => { - expect( - issueHandler.iconType(createMockSubject({ type: 'Issue' })).displayName, - ).toBe('IssueOpenedIcon'); - - expect( - issueHandler.iconType(createMockSubject({ type: 'Issue', state: 'OPEN' })) - .displayName, - ).toBe('IssueOpenedIcon'); - - expect( - issueHandler.iconType( - createMockSubject({ - type: 'Issue', - state: 'CLOSED', - }), - ).displayName, - ).toBe('IssueClosedIcon'); - - expect( - issueHandler.iconType( - createMockSubject({ - type: 'Issue', - state: 'COMPLETED', - }), - ).displayName, - ).toBe('IssueClosedIcon'); - - expect( - issueHandler.iconType( - createMockSubject({ - type: 'Issue', - state: 'NOT_PLANNED', - }), - ).displayName, - ).toBe('SkipIcon'); - - expect( - issueHandler.iconType( - createMockSubject({ - type: 'Issue', - state: 'DUPLICATE', - }), - ).displayName, - ).toBe('SkipIcon'); - - expect( - issueHandler.iconType( - createMockSubject({ - type: 'Issue', - state: 'REOPENED', - }), - ).displayName, - ).toBe('IssueReopenedIcon'); + describe('iconType', () => { + const cases = { + CLOSED: 'IssueClosedIcon', + COMPLETED: 'IssueClosedIcon', + DUPLICATE: 'SkipIcon', + NOT_PLANNED: 'SkipIcon', + OPEN: 'IssueOpenedIcon', + REOPENED: 'IssueReopenedIcon', + } satisfies Record; + + it.each( + Object.entries(cases) as Array<[GitifyIssueState, IconColor]>, + )('iconType for issue with state %s', (issueState, issueIconType) => { + expect( + issueHandler.iconType( + createMockSubject({ type: 'Issue', state: issueState }), + ).displayName, + ).toBe(issueIconType); + }); }); - it('iconColor', () => { - expect( - issueHandler.iconColor( - createMockSubject({ type: 'Issue', state: 'OPEN' }), - ), - ).toBe(IconColor.GREEN); - - expect( - issueHandler.iconColor( - createMockSubject({ type: 'Issue', state: 'REOPENED' }), - ), - ).toBe(IconColor.GREEN); - - expect( - issueHandler.iconColor( - createMockSubject({ type: 'Issue', state: 'CLOSED' }), - ), - ).toBe(IconColor.RED); - - expect( - issueHandler.iconColor( - createMockSubject({ type: 'Issue', state: 'COMPLETED' }), - ), - ).toBe(IconColor.PURPLE); - - expect( - issueHandler.iconColor( - createMockSubject({ type: 'Issue', state: 'NOT_PLANNED' }), - ), - ).toBe(IconColor.GRAY); + describe('iconColor', () => { + const cases = { + CLOSED: IconColor.RED, + COMPLETED: IconColor.PURPLE, + DUPLICATE: IconColor.GRAY, + NOT_PLANNED: IconColor.GRAY, + OPEN: IconColor.GREEN, + REOPENED: IconColor.GREEN, + } satisfies Record; + + it.each( + Object.entries(cases) as Array<[GitifyIssueState, IconColor]>, + )('iconColor for issue with state %s', (issueState, issueIconColor) => { + expect( + issueHandler.iconColor( + createMockSubject({ type: 'Issue', state: issueState }), + ), + ).toBe(issueIconColor); + }); }); it('defaultUrl', () => { @@ -477,3 +286,21 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { ).toEqual(`${mockHtmlUrl}/issues`); }); }); + +function mockIssueResponseNode(mocks: { + state: IssueState; + stateReason?: IssueStateReason; +}): IssueResponse { + return { + __typename: 'Issue', + number: 123, + title: 'PR Title', + state: mocks.state, + stateReason: mocks.stateReason, + url: 'https://github.com/gitify-app/notifications-test/issues/123', + author: mockAuthor, + labels: { nodes: [] }, + comments: { totalCount: 0, nodes: [] }, + milestone: null, + }; +} diff --git a/src/renderer/utils/notifications/handlers/issue.ts b/src/renderer/utils/notifications/handlers/issue.ts index 77341de79..ff2c792e0 100644 --- a/src/renderer/utils/notifications/handlers/issue.ts +++ b/src/renderer/utils/notifications/handlers/issue.ts @@ -30,6 +30,10 @@ class IssueHandler extends DefaultHandler { const response = await fetchIssueByNumber(notification); const issue = response.data.repository?.issue; + if (!issue) { + return null; + } + const issueState = issue.stateReason ?? issue.state; const issueComment = issue.comments.nodes[0]; diff --git a/src/renderer/utils/notifications/handlers/pullRequest.test.ts b/src/renderer/utils/notifications/handlers/pullRequest.test.ts index efc73efe0..15cd7d8cb 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.test.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.test.ts @@ -7,17 +7,26 @@ import { } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; import { createPartialMockUser } from '../../../__mocks__/user-mocks'; -import { type GitifySubject, IconColor, type Link } from '../../../types'; +import { + type GitifyPullRequestState, + type GitifySubject, + IconColor, + type Link, +} from '../../../types'; import type { Notification } from '../../../typesGitHub'; import type { FetchPullRequestByNumberQuery, PullRequestReviewState, + PullRequestState, } from '../../api/graphql/generated/graphql'; import { getLatestReviewForReviewers, pullRequestHandler } from './pullRequest'; type PullRequestResponse = FetchPullRequestByNumberQuery['repository']['pullRequest']; +const mockAuthor = createPartialMockUser('some-author'); +const mockCommenter = createPartialMockUser('some-commenter'); + describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { let mockNotification: Notification; @@ -32,102 +41,21 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { }); describe('enrich', () => { - const mockAuthor = createPartialMockUser('some-author'); - const mockCommenter = createPartialMockUser('some-commenter'); - beforeEach(() => { // axios will default to using the XHR adapter which can't be intercepted // by nock. So, configure axios to use the node adapter. axios.defaults.adapter = 'http'; }); - it('open pull request state', async () => { - nock('https://api.github.com') - .post('/graphql') - .reply(200, { - data: { - repository: { - pullRequest: { - __typename: 'PullRequest', - number: 123, - title: 'Test PR', - state: 'OPEN', - isDraft: false, - merged: false, - isInMergeQueue: false, - url: 'https://github.com/gitify-app/notifications-test/pulls/123', - author: mockAuthor, - labels: null, - comments: { - totalCount: 0, - nodes: [], - }, - reviews: { - totalCount: 0, - nodes: [], - }, - milestone: null, - closingIssuesReferences: { - nodes: [], - }, - } as PullRequestResponse, - }, - }, - }); - - const result = await pullRequestHandler.enrich( - mockNotification, - mockSettings, - ); + it('pull request with state', async () => { + const mockPullRequest = mockPullRequestResponseNode({ state: 'CLOSED' }); - expect(result).toEqual({ - number: 123, - state: 'OPEN', - user: { - login: mockAuthor.login, - html_url: mockAuthor.html_url, - avatar_url: mockAuthor.avatar_url, - type: mockAuthor.type, - }, - reviews: null, - labels: [], - linkedIssues: [], - comments: 0, - milestone: null, - htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123', - } as GitifySubject); - }); - - it('closed pull request state', async () => { nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - pullRequest: { - __typename: 'PullRequest', - number: 123, - title: 'Test PR', - state: 'CLOSED', - isDraft: false, - merged: false, - isInMergeQueue: false, - url: 'https://github.com/gitify-app/notifications-test/pulls/123', - author: mockAuthor, - labels: null, - comments: { - totalCount: 0, - nodes: [], - }, - reviews: { - totalCount: 0, - nodes: [], - }, - milestone: null, - closingIssuesReferences: { - nodes: [], - }, - } as PullRequestResponse, + pullRequest: mockPullRequest, }, }, }); @@ -139,7 +67,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { expect(result).toEqual({ number: 123, - state: 'CLOSED', + state: 'OPEN', user: { login: mockAuthor.login, html_url: mockAuthor.html_url, @@ -156,35 +84,17 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { }); it('draft pull request state', async () => { + const mockPullRequest = mockPullRequestResponseNode({ + state: 'OPEN', + isDraft: true, + }); + nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - pullRequest: { - __typename: 'PullRequest', - number: 123, - title: 'Test PR', - state: 'OPEN', - isDraft: true, - merged: false, - isInMergeQueue: false, - url: 'https://github.com/gitify-app/notifications-test/pulls/123', - author: mockAuthor, - labels: null, - comments: { - totalCount: 0, - nodes: [], - }, - reviews: { - totalCount: 0, - nodes: [], - }, - milestone: null, - closingIssuesReferences: { - nodes: [], - }, - } as PullRequestResponse, + pullRequest: mockPullRequest, }, }, }); @@ -213,35 +123,17 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { }); it('merged pull request state', async () => { + const mockPullRequest = mockPullRequestResponseNode({ + state: 'MERGED', + merged: true, + }); + nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - pullRequest: { - __typename: 'PullRequest', - number: 123, - title: 'Test PR', - state: 'MERGED', - isDraft: false, - merged: true, - isInMergeQueue: false, - url: 'https://github.com/gitify-app/notifications-test/pulls/123', - author: mockAuthor, - labels: null, - comments: { - totalCount: 0, - nodes: [], - }, - reviews: { - totalCount: 0, - nodes: [], - }, - milestone: null, - closingIssuesReferences: { - nodes: [], - }, - } as PullRequestResponse, + pullRequest: mockPullRequest, }, }, }); @@ -270,42 +162,25 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { }); it('with comments', async () => { + const mockPullRequest = mockPullRequestResponseNode({ + state: 'OPEN', + }); + mockPullRequest.comments = { + totalCount: 1, + nodes: [ + { + author: mockCommenter, + url: 'https://github.com/gitify-app/notifications-test/pulls/123#issuecomment-1234', + }, + ], + }; + nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - pullRequest: { - __typename: 'PullRequest', - number: 123, - title: 'Test PR', - state: 'OPEN', - isDraft: false, - merged: false, - isInMergeQueue: false, - url: 'https://github.com/gitify-app/notifications-test/pulls/123', - author: mockAuthor, - labels: { - nodes: [{ name: 'enhancement' }], - }, - comments: { - totalCount: 1, - nodes: [ - { - author: mockCommenter, - url: 'https://github.com/gitify-app/notifications-test/pulls/123#issuecomment-1234', - }, - ], - }, - reviews: { - totalCount: 0, - nodes: [], - }, - milestone: null, - closingIssuesReferences: { - nodes: [], - }, - } as PullRequestResponse, + pullRequest: mockPullRequest, }, }, }); @@ -325,7 +200,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { type: mockCommenter.type, }, reviews: null, - labels: ['enhancement'], + labels: [], linkedIssues: [], comments: 1, milestone: null, @@ -335,37 +210,23 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { }); it('with labels', async () => { + const mockPullRequest = mockPullRequestResponseNode({ + state: 'OPEN', + }); + mockPullRequest.labels = { + nodes: [ + { + name: 'enhancement', + }, + ], + }; + nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - pullRequest: { - __typename: 'PullRequest', - number: 123, - title: 'Test PR', - state: 'OPEN', - isDraft: false, - merged: false, - isInMergeQueue: false, - url: 'https://github.com/gitify-app/notifications-test/pulls/123', - author: mockAuthor, - labels: { - nodes: [{ name: 'enhancement' }], - }, - comments: { - totalCount: 0, - nodes: [], - }, - reviews: { - totalCount: 0, - nodes: [], - }, - milestone: null, - closingIssuesReferences: { - nodes: [], - }, - } as PullRequestResponse, + pullRequest: mockPullRequest, }, }, }); @@ -394,39 +255,23 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { }); it('with linked issues', async () => { + const mockPullRequest = mockPullRequestResponseNode({ + state: 'OPEN', + }); + mockPullRequest.closingIssuesReferences = { + nodes: [ + { + number: 789, + }, + ], + }; + nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - pullRequest: { - __typename: 'PullRequest', - number: 123, - title: 'Test PR', - state: 'OPEN', - isDraft: false, - merged: false, - isInMergeQueue: false, - url: 'https://github.com/gitify-app/notifications-test/pulls/123', - author: mockAuthor, - labels: null, - comments: { - totalCount: 0, - nodes: [], - }, - reviews: { - totalCount: 0, - nodes: [], - }, - milestone: null, - closingIssuesReferences: { - nodes: [ - { - number: 789, - }, - ], - }, - } as PullRequestResponse, + pullRequest: mockPullRequest, }, }, }); @@ -455,40 +300,20 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { }); it('with milestone', async () => { + const mockPullRequest = mockPullRequestResponseNode({ + state: 'OPEN', + }); + mockPullRequest.milestone = { + state: 'OPEN', + title: 'Open Milestone', + }; + nock('https://api.github.com') .post('/graphql') .reply(200, { data: { repository: { - pullRequest: { - __typename: 'PullRequest', - number: 123, - title: 'Test PR', - state: 'OPEN', - isDraft: false, - merged: false, - isInMergeQueue: false, - url: 'https://github.com/gitify-app/notifications-test/pulls/123', - author: mockAuthor, - labels: { - nodes: [], - }, - comments: { - totalCount: 0, - nodes: [], - }, - reviews: { - totalCount: 0, - nodes: [], - }, - milestone: { - state: 'OPEN', - title: 'Open Milestone', - }, - closingIssuesReferences: { - nodes: [], - }, - } as PullRequestResponse, + pullRequest: mockPullRequest, }, }, }); @@ -520,64 +345,42 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { }); }); - it('iconType', () => { - expect( - pullRequestHandler.iconType(createMockSubject({ type: 'PullRequest' })) - .displayName, - ).toBe('GitPullRequestIcon'); - - expect( - pullRequestHandler.iconType( - createMockSubject({ - type: 'PullRequest', - state: 'DRAFT', - }), - ).displayName, - ).toBe('GitPullRequestDraftIcon'); - - expect( - pullRequestHandler.iconType( - createMockSubject({ - type: 'PullRequest', - state: 'CLOSED', - }), - ).displayName, - ).toBe('GitPullRequestClosedIcon'); - - expect( - pullRequestHandler.iconType( - createMockSubject({ - type: 'PullRequest', - state: 'MERGED', - }), - ).displayName, - ).toBe('GitMergeIcon'); + describe('iconType', () => { + const cases = { + CLOSED: 'GitPullRequestClosedIcon', + DRAFT: 'GitPullRequestDraftIcon', + MERGED: 'GitMergeIcon', + OPEN: 'GitPullRequestIcon', + } satisfies Record; + + it.each( + Object.entries(cases) as Array<[GitifyPullRequestState, IconColor]>, + )('iconType for pull request with state %s', (pullRequestState, pullRequestIconType) => { + expect( + pullRequestHandler.iconType( + createMockSubject({ type: 'PullRequest', state: pullRequestState }), + ).displayName, + ).toBe(pullRequestIconType); + }); }); - it('iconColor', () => { - expect( - pullRequestHandler.iconColor( - createMockSubject({ type: 'PullRequest', state: 'OPEN' }), - ), - ).toBe(IconColor.GREEN); - - expect( - pullRequestHandler.iconColor( - createMockSubject({ type: 'PullRequest', state: 'CLOSED' }), - ), - ).toBe(IconColor.RED); - - expect( - pullRequestHandler.iconColor( - createMockSubject({ type: 'PullRequest', state: 'MERGED' }), - ), - ).toBe(IconColor.PURPLE); - - expect( - pullRequestHandler.iconColor( - createMockSubject({ type: 'PullRequest', state: 'DRAFT' }), - ), - ).toBe(IconColor.GRAY); + describe('iconColor', () => { + const cases = { + CLOSED: IconColor.RED, + DRAFT: IconColor.GRAY, + MERGED: IconColor.PURPLE, + OPEN: IconColor.GREEN, + } satisfies Record; + + it.each( + Object.entries(cases) as Array<[GitifyPullRequestState, IconColor]>, + )('iconType for pull request with state %s', (pullRequestState, pullRequestIconColor) => { + expect( + pullRequestHandler.iconColor( + createMockSubject({ type: 'PullRequest', state: pullRequestState }), + ), + ).toBe(pullRequestIconColor); + }); }); it('defaultUrl', () => { @@ -637,3 +440,34 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { }); }); }); + +function mockPullRequestResponseNode(mocks: { + state: PullRequestState; + isDraft?: boolean; + merged?: boolean; +}): PullRequestResponse { + return { + __typename: 'PullRequest', + number: 123, + title: 'Test PR', + state: mocks.state, + isDraft: mocks.isDraft ?? false, + merged: mocks.merged ?? false, + isInMergeQueue: false, + url: 'https://github.com/gitify-app/notifications-test/pulls/123', + author: mockAuthor, + labels: { nodes: [] }, + comments: { + totalCount: 0, + nodes: [], + }, + reviews: { + totalCount: 0, + nodes: [], + }, + milestone: null, + closingIssuesReferences: { + nodes: [], + }, + }; +} From 0b79e8b27ccdb1efd1e1b3651f31da4670a04072 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Tue, 23 Dec 2025 08:40:02 +1000 Subject: [PATCH 24/25] refactor test suites Signed-off-by: Adam Setch --- src/renderer/utils/notifications/handlers/pullRequest.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/utils/notifications/handlers/pullRequest.test.ts b/src/renderer/utils/notifications/handlers/pullRequest.test.ts index 15cd7d8cb..d081f0cc3 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.test.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.test.ts @@ -67,7 +67,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { expect(result).toEqual({ number: 123, - state: 'OPEN', + state: 'CLOSED', user: { login: mockAuthor.login, html_url: mockAuthor.html_url, From f40fc817c86998664e4aa6b83d4a99a4877fe7a6 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Tue, 23 Dec 2025 09:48:43 +1000 Subject: [PATCH 25/25] refactor: remove unused code Signed-off-by: Adam Setch --- .../utils/notifications/handlers/discussion.ts | 4 ---- src/renderer/utils/notifications/handlers/issue.ts | 6 +----- .../utils/notifications/handlers/pullRequest.ts | 12 +++++------- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/renderer/utils/notifications/handlers/discussion.ts b/src/renderer/utils/notifications/handlers/discussion.ts index cdd38b527..30a993371 100644 --- a/src/renderer/utils/notifications/handlers/discussion.ts +++ b/src/renderer/utils/notifications/handlers/discussion.ts @@ -49,10 +49,6 @@ class DiscussionHandler extends DefaultHandler { const response = await fetchDiscussionByNumber(notification); const discussion = response.data.repository?.discussion; - if (!discussion) { - return null; - } - let discussionState: GitifyDiscussionState = 'OPEN'; if (discussion.isAnswered) { diff --git a/src/renderer/utils/notifications/handlers/issue.ts b/src/renderer/utils/notifications/handlers/issue.ts index ff2c792e0..2a1fb9206 100644 --- a/src/renderer/utils/notifications/handlers/issue.ts +++ b/src/renderer/utils/notifications/handlers/issue.ts @@ -30,10 +30,6 @@ class IssueHandler extends DefaultHandler { const response = await fetchIssueByNumber(notification); const issue = response.data.repository?.issue; - if (!issue) { - return null; - } - const issueState = issue.stateReason ?? issue.state; const issueComment = issue.comments.nodes[0]; @@ -48,7 +44,7 @@ class IssueHandler extends DefaultHandler { state: issueState, user: issueUser, comments: issue.comments.totalCount, - labels: issue.labels?.nodes.map((label) => label.name) ?? [], + labels: issue.labels?.nodes.map((label) => label.name), milestone: issue.milestone, htmlUrl: issueComment?.url ?? issue.url, }; diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index b6164756d..d3b35b412 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -41,9 +41,7 @@ class PullRequestHandler extends DefaultHandler { const prUser = getNotificationAuthor([prComment?.author, pr.author]); - const reviews = pr.reviews - ? getLatestReviewForReviewers(pr.reviews.nodes) - : null; + const reviews = getLatestReviewForReviewers(pr.reviews.nodes); return { number: pr.number, @@ -51,10 +49,10 @@ class PullRequestHandler extends DefaultHandler { user: prUser, reviews: reviews, comments: pr.comments.totalCount, - labels: pr.labels?.nodes.map((label) => label.name) ?? [], - linkedIssues: - pr.closingIssuesReferences?.nodes.map((issue) => `#${issue.number}`) ?? - [], + labels: pr.labels?.nodes.map((label) => label.name), + linkedIssues: pr.closingIssuesReferences?.nodes.map( + (issue) => `#${issue.number}`, + ), milestone: pr.milestone, htmlUrl: prComment?.url ?? pr.url, };