Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"@primer/react": "38.39.0",
"@tailwindcss/vite": "4.3.3",
"@tanstack/react-query": "5.102.8",
"@tanstack/react-virtual": "3.14.13",
"@testing-library/jest-dom": "7.0.1",
"@testing-library/react": "16.3.3",
"@testing-library/user-event": "14.6.7",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/renderer/components/layout/Contents.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FC, ReactNode } from 'react';
import type { FC, ReactNode, Ref } from 'react';

import { cn } from 'cn';

Expand All @@ -7,6 +7,7 @@ interface IContents {
paddingHorizontal?: boolean;
paddingBottom?: boolean;
scrollFade?: boolean;
ref?: Ref<HTMLDivElement>;
}

/**
Expand All @@ -18,6 +19,7 @@ export const Contents: FC<IContents> = ({
paddingHorizontal = true,
paddingBottom = false,
scrollFade = false,
ref,
}) => {
return (
<div
Expand All @@ -27,6 +29,7 @@ export const Contents: FC<IContents> = ({
paddingBottom && 'pb-2',
scrollFade && 'gitify-scroll-fade',
)}
ref={ref}
>
{children}
</div>
Expand Down
109 changes: 109 additions & 0 deletions src/renderer/components/notifications/AccountHeader.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { renderWithProviders } from '../../__helpers__/test-utils';
import { mockGitHubCloudAccount } from '../../__mocks__/account-mocks';

import * as links from '../../utils/system/links';
import { AccountHeader, type AccountHeaderProps } from './AccountHeader';

describe('renderer/components/notifications/AccountHeader.tsx', () => {
const props: AccountHeaderProps = {
account: mockGitHubCloudAccount,
error: null,
notificationCount: 3,
isCollapsed: false,
onToggle: vi.fn(),
};

it('renders the managed GitHub account identity', () => {
renderWithProviders(
<AccountHeader
{...props}
account={{
...mockGitHubCloudAccount,
user: {
...mockGitHubCloudAccount.user!,
login: 'octocat_gitify',
name: 'Mona Lisa Octocat',
},
}}
/>,
);

expect(screen.getByText('octocat_gitify')).toBeInTheDocument();
expect(screen.getByAltText('octocat_gitify')).toBeInTheDocument();
});

it('should open profile when clicked', async () => {
const openAccountProfileSpy = vi.spyOn(links, 'openAccountProfile').mockImplementation(vi.fn());
const onToggle = vi.fn();

renderWithProviders(<AccountHeader {...props} onToggle={onToggle} />);

await userEvent.click(screen.getByTestId('account-profile'));

expect(openAccountProfileSpy).toHaveBeenCalledWith(mockGitHubCloudAccount);
// The header's own click toggles collapse; the profile button must not.
expect(onToggle).not.toHaveBeenCalled();
});

it('should open my issues when clicked', async () => {
const openHostIssuesSpy = vi.spyOn(links, 'openHostIssues').mockImplementation(vi.fn());

renderWithProviders(<AccountHeader {...props} />);

await userEvent.click(screen.getByTestId('account-issues'));

expect(openHostIssuesSpy).toHaveBeenCalledWith(mockGitHubCloudAccount);
});

it('should open my pull requests when clicked', async () => {
const openHostPullsSpy = vi.spyOn(links, 'openHostPulls').mockImplementation(vi.fn());

renderWithProviders(<AccountHeader {...props} />);

await userEvent.click(screen.getByTestId('account-pull-requests'));

expect(openHostPullsSpy).toHaveBeenCalledWith(mockGitHubCloudAccount);
});

it('should request a collapse toggle when toggled', async () => {
const onToggle = vi.fn();

renderWithProviders(<AccountHeader {...props} onToggle={onToggle} />);

await userEvent.click(screen.getByTestId('account-toggle'));

expect(onToggle).toHaveBeenCalledTimes(1);
});

it('should label the toggle by collapsed state', () => {
const { unmount } = renderWithProviders(<AccountHeader {...props} />)!;

expect(screen.getByTestId('account-toggle')).toHaveAttribute(
'title',
'Hide account notifications',
);

unmount();
renderWithProviders(<AccountHeader {...props} isCollapsed />);

expect(screen.getByTestId('account-toggle')).toHaveAttribute(
'title',
'Show account notifications',
);
});

it('should render an error background when the account errored', () => {
const tree = renderWithProviders(
<AccountHeader
{...props}
error={{ title: 'Error title', descriptions: ['Error description'], emojis: ['🔥'] }}
notificationCount={0}
/>,
);

expect(tree!.container).toMatchSnapshot();
});
});
92 changes: 92 additions & 0 deletions src/renderer/components/notifications/AccountHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { type FC, type MouseEvent } from 'react';

import { GitPullRequestIcon, IssueOpenedIcon } from '@primer/octicons-react';
import { Button, Stack } from '@primer/react';

import { cn } from 'cn';

import { HoverButton } from '../primitives/HoverButton';
import { HoverGroup } from '../primitives/HoverGroup';

import { type Account, type GitifyError, Size } from '../../types';

import { getAdapter } from '../../utils/forges/registry';
import { openAccountProfile, openHostIssues, openHostPulls } from '../../utils/system/links';
import { getChevronDetails } from '../../utils/ui/display';
import { AvatarWithFallback } from '../avatars/AvatarWithFallback';

export interface AccountHeaderProps {
account: Account;
error: GitifyError | null;
notificationCount: number;
isCollapsed: boolean;
onToggle: () => void;
}

export const AccountHeader: FC<AccountHeaderProps> = ({
account,
error,
notificationCount,
isCollapsed,
onToggle,
}) => {
const Chevron = getChevronDetails(notificationCount > 0, !isCollapsed, 'account');

return (
<Stack
className={cn(
'group relative pr-1 py-0.5',
error ? 'bg-gitify-account-error' : 'bg-gitify-account-rest',
)}
direction="horizontal"
onClick={onToggle}
>
<Button
alignContent="center"
count={notificationCount}
data-testid="account-profile"
onClick={(event: MouseEvent<HTMLElement>) => {
// Don't trigger onClick of parent element.
event.stopPropagation();
openAccountProfile(account);
}}
title="Open account profile"
variant="invisible"
>
<AvatarWithFallback
alt={getAdapter(account).formatUserLogin(account.user!.login)}
name={getAdapter(account).formatUserLogin(account.user!.login)}
size={Size.MEDIUM}
src={account.user!.avatar ?? undefined}
/>
</Button>

<HoverGroup
bgColor={
error ? 'group-hover:bg-gitify-account-error' : 'group-hover:bg-gitify-account-rest'
}
>
<HoverButton
action={() => openHostIssues(account)}
icon={IssueOpenedIcon}
label="My issues ↗"
testid="account-issues"
/>

<HoverButton
action={() => openHostPulls(account)}
icon={GitPullRequestIcon}
label="My pull requests ↗"
testid="account-pull-requests"
/>

<HoverButton
action={onToggle}
icon={Chevron.icon}
label={Chevron.label}
testid="account-toggle"
/>
</HoverGroup>
</Stack>
);
};
Loading
Loading