diff --git a/src/components/NotificationRow.test.tsx b/src/components/NotificationRow.test.tsx index 442393f4d..bb781f14a 100644 --- a/src/components/NotificationRow.test.tsx +++ b/src/components/NotificationRow.test.tsx @@ -61,48 +61,6 @@ describe('components/NotificationRow.tsx', () => { expect(tree).toMatchSnapshot(); }); - it('should render itself & its children when last_read_at is null', async () => { - jest - .spyOn(global.Date, 'now') - .mockImplementation(() => new Date('2024').valueOf()); - - const mockNotification = mockSingleNotification; - mockNotification.last_read_at = null; - - const props = { - notification: mockNotification, - account: mockGitHubCloudAccount, - }; - - const tree = render( - - - , - ); - expect(tree).toMatchSnapshot(); - }); - - it('should render itself & its children without avatar', async () => { - jest - .spyOn(global.Date, 'now') - .mockImplementation(() => new Date('2024').valueOf()); - - const mockNotification = mockSingleNotification; - mockNotification.subject.user = null; - - const props = { - notification: mockNotification, - account: mockGitHubCloudAccount, - }; - - const tree = render( - - - , - ); - expect(tree).toMatchSnapshot(); - }); - describe('notification interactions', () => { it('should open a notification in the browser - click', () => { const removeNotificationFromState = jest.fn(); diff --git a/src/components/NotificationRow.tsx b/src/components/NotificationRow.tsx index 50ea70125..bfcead590 100644 --- a/src/components/NotificationRow.tsx +++ b/src/components/NotificationRow.tsx @@ -1,9 +1,4 @@ -import { - BellSlashIcon, - CheckIcon, - FeedPersonIcon, - ReadIcon, -} from '@primer/octicons-react'; +import { BellSlashIcon, CheckIcon, ReadIcon } from '@primer/octicons-react'; import { type FC, type MouseEvent, @@ -12,24 +7,19 @@ import { useState, } from 'react'; import { AppContext } from '../context/App'; -import { IconColor, Opacity, Size } from '../types'; +import { Opacity, Size } from '../types'; import type { Notification } from '../typesGitHub'; import { cn } from '../utils/cn'; -import { - formatForDisplay, - formatNotificationUpdatedAt, -} from '../utils/helpers'; +import { formatForDisplay } from '../utils/helpers'; import { getNotificationTypeIcon, getNotificationTypeIconColor, } from '../utils/icons'; -import { openNotification, openUserProfile } from '../utils/links'; -import { formatReason } from '../utils/reason'; +import { openNotification } from '../utils/links'; import { HoverGroup } from './HoverGroup'; import { InteractionButton } from './buttons/InteractionButton'; -import { AvatarIcon } from './icons/AvatarIcon'; +import { NotificationFooter } from './notification/NotificationFooter'; import { NotificationHeader } from './notification/NotificationHeader'; -import { Pills } from './notification/Pills'; interface INotificationRow { notification: Notification; @@ -73,16 +63,10 @@ export const NotificationRow: FC = ({ unsubscribeNotification(notification); }; - const reason = formatReason(notification.reason); const NotificationIcon = getNotificationTypeIcon(notification.subject); const iconColor = getNotificationTypeIconColor(notification.subject); - const updatedAt = formatNotificationUpdatedAt(notification); - const updatedLabel = notification.subject.user - ? `${notification.subject.user.login} updated ${updatedAt}` - : `Updated ${updatedAt}`; - - const notificationTitle = formatForDisplay([ + const notificationType = formatForDisplay([ notification.subject.state, notification.subject.type, ]); @@ -101,7 +85,7 @@ export const NotificationRow: FC = ({ > = ({ {notification.subject.title} - - {notification.subject.user ? ( - ) => { - // Don't trigger onClick of parent element. - event.stopPropagation(); - openUserProfile(notification.subject.user); - }} - className="flex-shrink-0" - > - - - ) : ( - - - - )} - {reason.title} - {updatedAt} - - + diff --git a/src/components/__snapshots__/NotificationRow.test.tsx.snap b/src/components/__snapshots__/NotificationRow.test.tsx.snap index 2d9fb3738..30e7c1cd7 100644 --- a/src/components/__snapshots__/NotificationRow.test.tsx.snap +++ b/src/components/__snapshots__/NotificationRow.test.tsx.snap @@ -875,855 +875,3 @@ exports[`components/NotificationRow.tsx should render itself & its children - gr "unmount": [Function], } `; - -exports[`components/NotificationRow.tsx should render itself & its children when last_read_at is null 1`] = ` -{ - "asFragment": [Function], - "baseElement": - - - - - - - - - - - I am a robot and this is a test! - - - - - - - Updated - - - 7 years ago - - - - - - - 1 - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - , - "container": - - - - - - - - - - I am a robot and this is a test! - - - - - - - Updated - - - 7 years ago - - - - - - - 1 - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - , - "debug": [Function], - "findAllByAltText": [Function], - "findAllByDisplayValue": [Function], - "findAllByLabelText": [Function], - "findAllByPlaceholderText": [Function], - "findAllByRole": [Function], - "findAllByTestId": [Function], - "findAllByText": [Function], - "findAllByTitle": [Function], - "findByAltText": [Function], - "findByDisplayValue": [Function], - "findByLabelText": [Function], - "findByPlaceholderText": [Function], - "findByRole": [Function], - "findByTestId": [Function], - "findByText": [Function], - "findByTitle": [Function], - "getAllByAltText": [Function], - "getAllByDisplayValue": [Function], - "getAllByLabelText": [Function], - "getAllByPlaceholderText": [Function], - "getAllByRole": [Function], - "getAllByTestId": [Function], - "getAllByText": [Function], - "getAllByTitle": [Function], - "getByAltText": [Function], - "getByDisplayValue": [Function], - "getByLabelText": [Function], - "getByPlaceholderText": [Function], - "getByRole": [Function], - "getByTestId": [Function], - "getByText": [Function], - "getByTitle": [Function], - "queryAllByAltText": [Function], - "queryAllByDisplayValue": [Function], - "queryAllByLabelText": [Function], - "queryAllByPlaceholderText": [Function], - "queryAllByRole": [Function], - "queryAllByTestId": [Function], - "queryAllByText": [Function], - "queryAllByTitle": [Function], - "queryByAltText": [Function], - "queryByDisplayValue": [Function], - "queryByLabelText": [Function], - "queryByPlaceholderText": [Function], - "queryByRole": [Function], - "queryByTestId": [Function], - "queryByText": [Function], - "queryByTitle": [Function], - "rerender": [Function], - "unmount": [Function], -} -`; - -exports[`components/NotificationRow.tsx should render itself & its children without avatar 1`] = ` -{ - "asFragment": [Function], - "baseElement": - - - - - - - - - - - I am a robot and this is a test! - - - - - - - - - Updated - - - 7 years ago - - - - - - - 1 - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - , - "container": - - - - - - - - - - I am a robot and this is a test! - - - - - - - - - Updated - - - 7 years ago - - - - - - - 1 - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - , - "debug": [Function], - "findAllByAltText": [Function], - "findAllByDisplayValue": [Function], - "findAllByLabelText": [Function], - "findAllByPlaceholderText": [Function], - "findAllByRole": [Function], - "findAllByTestId": [Function], - "findAllByText": [Function], - "findAllByTitle": [Function], - "findByAltText": [Function], - "findByDisplayValue": [Function], - "findByLabelText": [Function], - "findByPlaceholderText": [Function], - "findByRole": [Function], - "findByTestId": [Function], - "findByText": [Function], - "findByTitle": [Function], - "getAllByAltText": [Function], - "getAllByDisplayValue": [Function], - "getAllByLabelText": [Function], - "getAllByPlaceholderText": [Function], - "getAllByRole": [Function], - "getAllByTestId": [Function], - "getAllByText": [Function], - "getAllByTitle": [Function], - "getByAltText": [Function], - "getByDisplayValue": [Function], - "getByLabelText": [Function], - "getByPlaceholderText": [Function], - "getByRole": [Function], - "getByTestId": [Function], - "getByText": [Function], - "getByTitle": [Function], - "queryAllByAltText": [Function], - "queryAllByDisplayValue": [Function], - "queryAllByLabelText": [Function], - "queryAllByPlaceholderText": [Function], - "queryAllByRole": [Function], - "queryAllByTestId": [Function], - "queryAllByText": [Function], - "queryAllByTitle": [Function], - "queryByAltText": [Function], - "queryByDisplayValue": [Function], - "queryByLabelText": [Function], - "queryByPlaceholderText": [Function], - "queryByRole": [Function], - "queryByTestId": [Function], - "queryByText": [Function], - "queryByTitle": [Function], - "rerender": [Function], - "unmount": [Function], -} -`; diff --git a/src/components/notification/NotificationFooter.test.tsx b/src/components/notification/NotificationFooter.test.tsx new file mode 100644 index 000000000..200b919bd --- /dev/null +++ b/src/components/notification/NotificationFooter.test.tsx @@ -0,0 +1,119 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { + mockGitHubCloudAccount, + mockSettings, +} from '../../__mocks__/state-mocks'; +import { AppContext } from '../../context/App'; +import { GroupBy, type Link } from '../../types'; +import type { UserType } from '../../typesGitHub'; +import { mockSingleNotification } from '../../utils/api/__mocks__/response-mocks'; +import * as comms from '../../utils/comms'; +import * as links from '../../utils/links'; +import { NotificationFooter } from './NotificationFooter'; + +describe('components/notification/NotificationFooter.tsx', () => { + beforeEach(() => { + jest.spyOn(links, 'openNotification'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render itself & its children', async () => { + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date('2024').valueOf()); + + const props = { + notification: mockSingleNotification, + }; + + const tree = render( + + + , + ); + expect(tree).toMatchSnapshot(); + }); + + it('should render itself & its children when last_read_at is null', async () => { + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date('2024').valueOf()); + + const mockNotification = mockSingleNotification; + mockNotification.last_read_at = null; + + const props = { + notification: mockNotification, + }; + + const tree = render( + + + , + ); + expect(tree).toMatchSnapshot(); + }); + + it('should render itself & its children without avatar', async () => { + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date('2024').valueOf()); + + const mockNotification = mockSingleNotification; + mockNotification.subject.user = null; + + const props = { + notification: mockNotification, + }; + + const tree = render( + + + , + ); + expect(tree).toMatchSnapshot(); + }); + + it('should open notification user profile', () => { + const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); + + const props = { + notification: { + ...mockSingleNotification, + subject: { + ...mockSingleNotification.subject, + user: { + login: 'some-user', + html_url: 'https://github.com/some-user' as Link, + avatar_url: + 'https://avatars.githubusercontent.com/u/123456789?v=4' as Link, + type: 'User' as UserType, + }, + reviews: null, + }, + }, + account: mockGitHubCloudAccount, + }; + + render( + + + , + ); + + fireEvent.click(screen.getByTitle('View User Profile')); + expect(openExternalLinkMock).toHaveBeenCalledTimes(1); + expect(openExternalLinkMock).toHaveBeenCalledWith( + props.notification.subject.user.html_url, + ); + }); +}); diff --git a/src/components/notification/NotificationFooter.tsx b/src/components/notification/NotificationFooter.tsx new file mode 100644 index 000000000..fa883512e --- /dev/null +++ b/src/components/notification/NotificationFooter.tsx @@ -0,0 +1,61 @@ +import { FeedPersonIcon } from '@primer/octicons-react'; +import type { FC, MouseEvent } from 'react'; +import { IconColor, Opacity, Size } from '../../types'; +import type { Notification } from '../../typesGitHub'; +import { cn } from '../../utils/cn'; +import { formatNotificationUpdatedAt } from '../../utils/helpers'; +import { openUserProfile } from '../../utils/links'; +import { formatReason } from '../../utils/reason'; +import { AvatarIcon } from '../icons/AvatarIcon'; +import { Pills } from './Pills'; + +interface INotificationFooter { + notification: Notification; +} + +export const NotificationFooter: FC = ({ + notification, +}: INotificationFooter) => { + const reason = formatReason(notification.reason); + + const updatedAt = formatNotificationUpdatedAt(notification); + const updatedLabel = notification.subject.user + ? `${notification.subject.user.login} updated ${updatedAt}` + : `Updated ${updatedAt}`; + + return ( + + {notification.subject.user ? ( + ) => { + // Don't trigger onClick of parent element. + event.stopPropagation(); + openUserProfile(notification.subject.user); + }} + className="flex-shrink-0" + > + + + ) : ( + + + + )} + {reason.title} + {updatedAt} + + + ); +}; diff --git a/src/components/notification/__snapshots__/NotificationFooter.test.tsx.snap b/src/components/notification/__snapshots__/NotificationFooter.test.tsx.snap new file mode 100644 index 000000000..5d59c1f55 --- /dev/null +++ b/src/components/notification/__snapshots__/NotificationFooter.test.tsx.snap @@ -0,0 +1,638 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/notification/NotificationFooter.tsx should render itself & its children 1`] = ` +{ + "asFragment": [Function], + "baseElement": + + + + + + + Updated + + + 7 years ago + + + + + + + 1 + + + + + + 1 + + + + + , + "container": + + + + + + Updated + + + 7 years ago + + + + + + + 1 + + + + + + 1 + + + + , + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; + +exports[`components/notification/NotificationFooter.tsx should render itself & its children when last_read_at is null 1`] = ` +{ + "asFragment": [Function], + "baseElement": + + + + + + + Updated + + + 7 years ago + + + + + + + 1 + + + + + + 1 + + + + + , + "container": + + + + + + Updated + + + 7 years ago + + + + + + + 1 + + + + + + 1 + + + + , + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; + +exports[`components/notification/NotificationFooter.tsx should render itself & its children without avatar 1`] = ` +{ + "asFragment": [Function], + "baseElement": + + + + + + + + + Updated + + + 7 years ago + + + + + + + 1 + + + + + + 1 + + + + + , + "container": + + + + + + + + Updated + + + 7 years ago + + + + + + + 1 + + + + + + 1 + + + + , + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`;