Skip to content
Merged
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
128 changes: 128 additions & 0 deletions src/renderer/routes/Accounts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ import {

import { useAccountsStore } from '../stores';

import { IconColor } from '../types';

import {
getAlternateScopeNames,
getRecommendedScopeNames,
getRequiredScopeNames,
} from '../utils/auth/scopes';
import { getAccountUUID } from '../utils/auth/utils';
import { Errors } from '../utils/core/errors';
import * as logger from '../utils/core/logger';
import * as comms from '../utils/system/comms';
Expand Down Expand Up @@ -61,6 +69,124 @@ describe('renderer/routes/Accounts.tsx', () => {
});
});

describe('Scopes icon', () => {
const scopeCases = [
{
name: 'recommended scopes',
scopes: getRecommendedScopeNames(),
color: IconColor.GREEN,
variant: 'default',
},
{
name: 'alternate scopes only',
scopes: getAlternateScopeNames(),
color: 'text-gitify-warning',
variant: 'default',
},
{
name: 'neither scope set',
scopes: getRequiredScopeNames(),
color: null,
variant: 'danger',
},
{
name: 'both scope sets',
scopes: [...new Set([...getRecommendedScopeNames(), ...getAlternateScopeNames()])],
color: IconColor.GREEN,
variant: 'default',
},
];

it.each(scopeCases)(
'preserves the icon color and button variant for $name',
async ({ scopes, color, variant }) => {
await act(async () => {
renderWithProviders(<AccountsRoute />, {
accounts: [{ ...mockPersonalAccessTokenAccount, scopes }],
});
});

const button = screen.getByTestId('account-view-scopes');
const icon = button.querySelector('svg');

expect(icon).toHaveClass('octicon-shield-check');
expect(button).toHaveAttribute('data-variant', variant);
expect(button).toHaveAttribute('data-size', 'small');
expect(button).toHaveAccessibleName(
`View scopes for ${mockPersonalAccessTokenAccount.user?.login}`,
);
if (color) {
expect(icon).toHaveClass(color);
}
for (const scopeColor of [IconColor.GREEN, 'text-gitify-warning']) {
if (scopeColor !== color) {
expect(icon).not.toHaveClass(scopeColor);
}
}
},
);

it.each(scopeCases)(
'retains the icon across an unrelated account update for $name',
async ({ scopes }) => {
const account = { ...mockPersonalAccessTokenAccount, scopes };

await act(async () => {
renderWithProviders(<AccountsRoute />, { accounts: [account] });
});

const icon = screen.getByTestId('account-view-scopes').querySelector('svg');
expect(icon).toBeInTheDocument();

const updatedAccount = {
...account,
user: { ...account.user!, name: 'Updated account name' },
};
expect(getAccountUUID(updatedAccount)).toBe(getAccountUUID(account));

await act(async () => {
useAccountsStore.setState({ accounts: [updatedAccount] });
});

expect(screen.getByText('Updated account name')).toBeInTheDocument();
expect(screen.getByTestId('account-view-scopes').querySelector('svg')).toBe(icon);
},
);

it('updates the icon, variant, and navigation account when scopes change', async () => {
const account = {
...mockPersonalAccessTokenAccount,
scopes: getRequiredScopeNames(),
};

await act(async () => {
renderWithProviders(<AccountsRoute />, { accounts: [account] });
});

const button = screen.getByTestId('account-view-scopes');
expect(button).toHaveAttribute('data-variant', 'danger');
expect(button.querySelector('svg')).not.toHaveClass(IconColor.GREEN, 'text-gitify-warning');

const updatedAccount = { ...account, scopes: getRecommendedScopeNames() };
expect(getAccountUUID(updatedAccount)).toBe(getAccountUUID(account));

await act(async () => {
useAccountsStore.setState({ accounts: [updatedAccount] });
});

const updatedButton = screen.getByTestId('account-view-scopes');
expect(updatedButton.querySelector('svg')).toHaveClass(IconColor.GREEN);
expect(updatedButton.querySelector('svg')).not.toHaveClass('text-gitify-warning');
expect(updatedButton).toHaveAttribute('data-variant', 'default');

await userEvent.click(updatedButton);

expect(navigateMock).toHaveBeenCalledWith('/account-scopes', {
state: { account: updatedAccount },
});
});
});

describe('Account interactions', () => {
const openExternalLinkSpy = vi.spyOn(comms, 'openExternalLink').mockImplementation(vi.fn());

Expand Down Expand Up @@ -332,6 +458,8 @@ describe('renderer/routes/Accounts.tsx', () => {
});
});

expect(screen.queryByTestId('account-view-scopes')).not.toBeInTheDocument();

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

expect(navigateMock).toHaveBeenCalledTimes(1);
Expand Down
21 changes: 16 additions & 5 deletions src/renderer/routes/Accounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ArrowLeftIcon,
ChevronRightIcon,
KeyIcon,
type OcticonProps,
PersonAddIcon,
PersonIcon,
ShieldCheckIcon,
Expand All @@ -16,6 +17,8 @@ import {
} from '@primer/octicons-react';
import { ActionList, ActionMenu, Button, IconButton, Stack, Text } from '@primer/react';

import { cn } from 'cn';

import { useLogins } from '../hooks/useLogins';
import { useNotifications } from '../hooks/useNotifications';
import { useAccountsStore } from '../stores';
Expand All @@ -38,14 +41,22 @@ import { getAdapter, listAdapters } from '../utils/forges/registry';
import { openAccountProfile, openAccountSettings, openHost } from '../utils/system/links';
import { getPlatformIcon } from '../utils/ui/icons';

function getScopeIconClassName(account: Account): string {
const RecommendedScopesIcon: FC<OcticonProps> = ({ className, ...props }) => (
<ShieldCheckIcon {...props} className={cn(IconColor.GREEN, className)} />
);

const AlternateScopesIcon: FC<OcticonProps> = ({ className, ...props }) => (
<ShieldCheckIcon {...props} className={cn('text-gitify-warning', className)} />
);

function getScopeIcon(account: Account): FC<OcticonProps> {
if (hasRecommendedScopes(account)) {
return IconColor.GREEN;
return RecommendedScopesIcon;
}
if (hasAlternateScopes(account)) {
return 'text-gitify-warning';
return AlternateScopesIcon;
}
return '';
return ShieldCheckIcon;
}

export const AccountsRoute: FC = () => {
Expand Down Expand Up @@ -242,7 +253,7 @@ export const AccountsRoute: FC = () => {
<IconButton
aria-label={`View scopes for ${account.user?.login}`}
data-testid="account-view-scopes"
icon={() => <ShieldCheckIcon className={getScopeIconClassName(account)} />}
icon={getScopeIcon(account)}
onClick={() =>
navigate('/account-scopes', {
state: { account },
Expand Down
Loading