fix(ui): profile page directive + avatar dropdown links/fallbacks
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AvatarUpload,
|
AvatarUpload,
|
||||||
ProfileHeader,
|
ProfileHeader,
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export const AvatarDropdown = () => {
|
|||||||
user?.image && !remoteImageUrl ? { storageId: user.image } : 'skip',
|
user?.image && !remoteImageUrl ? { storageId: user.image } : 'skip',
|
||||||
);
|
);
|
||||||
const avatarUrl = remoteImageUrl ?? currentImageUrl;
|
const avatarUrl = remoteImageUrl ?? currentImageUrl;
|
||||||
|
// Empty/whitespace names must fall through to the email, so intentionally
|
||||||
|
// treat a blank string as "no name" (nullish coalescing would keep it).
|
||||||
|
const trimmedName = user?.name?.trim();
|
||||||
|
const displayName = trimmedName?.length ? trimmedName : user?.email?.trim();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -61,16 +65,16 @@ export const AvatarDropdown = () => {
|
|||||||
/>
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align='end'>
|
<DropdownMenuContent align='end'>
|
||||||
{(user?.name ?? user?.email) && (
|
{displayName && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuLabel className='text-center font-bold'>
|
<DropdownMenuLabel className='text-center font-bold'>
|
||||||
{user.name?.trim() ?? user.email?.trim()}
|
{displayName}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href='/profile' className='w-full cursor-pointer'>
|
<Link href='/settings/profile' className='w-full cursor-pointer'>
|
||||||
Edit Profile
|
Edit Profile
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import type { ComponentProps, ReactNode } from 'react';
|
||||||
|
import { cleanup, render, screen } from '@testing-library/react';
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { api } from '@spoon/backend/convex/_generated/api.js';
|
||||||
|
|
||||||
|
import { AvatarDropdown } from '../../src/components/layout/header/controls/AvatarDropdown';
|
||||||
|
|
||||||
|
// Stable sentinels: the real Convex `api` proxy hands back a fresh reference on
|
||||||
|
// every access, so `===` comparisons in the mocks below would never match.
|
||||||
|
vi.mock('@spoon/backend/convex/_generated/api.js', () => ({
|
||||||
|
api: {
|
||||||
|
auth: {
|
||||||
|
getUser: 'auth:getUser',
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
getImageUrl: 'files:getImageUrl',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { mockUseConvexAuth, mockUseQuery } = vi.hoisted(() => ({
|
||||||
|
mockUseConvexAuth: vi.fn(),
|
||||||
|
mockUseQuery: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('convex/react', () => ({
|
||||||
|
useConvexAuth: mockUseConvexAuth,
|
||||||
|
useQuery: mockUseQuery,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@convex-dev/auth/react', () => ({
|
||||||
|
useAuthActions: () => ({ signOut: vi.fn() }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('next/navigation', () => ({
|
||||||
|
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/lib/avatar', () => ({
|
||||||
|
isRemoteImageUrl: () => false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Render the dropdown primitives as pass-through wrappers so the menu content
|
||||||
|
// is deterministically present under jsdom (Radix pointer behavior is flaky
|
||||||
|
// here). We are exercising AvatarDropdown's logic, not Radix.
|
||||||
|
vi.mock('@spoon/ui', () => ({
|
||||||
|
BasedAvatar: () => <div>avatar</div>,
|
||||||
|
Button: (props: ComponentProps<'button'>) => <button {...props} />,
|
||||||
|
DropdownMenu: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
|
||||||
|
DropdownMenuTrigger: ({ children }: { children?: ReactNode }) => (
|
||||||
|
<>{children}</>
|
||||||
|
),
|
||||||
|
DropdownMenuContent: ({ children }: { children?: ReactNode }) => (
|
||||||
|
<div>{children}</div>
|
||||||
|
),
|
||||||
|
DropdownMenuItem: ({ children }: { children?: ReactNode }) => <>{children}</>,
|
||||||
|
DropdownMenuLabel: ({ children }: { children?: ReactNode }) => (
|
||||||
|
<div>{children}</div>
|
||||||
|
),
|
||||||
|
DropdownMenuSeparator: () => <hr />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const seedUser = (user: { name: string; email: string }) => {
|
||||||
|
mockUseConvexAuth.mockReturnValue({
|
||||||
|
isAuthenticated: true,
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
mockUseQuery.mockImplementation((reference: unknown) => {
|
||||||
|
if (reference === api.auth.getUser) return user;
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('AvatarDropdown', () => {
|
||||||
|
it('falls back to the email when the name is an empty string', () => {
|
||||||
|
seedUser({ name: '', email: 'me@x.com' });
|
||||||
|
render(<AvatarDropdown />);
|
||||||
|
expect(screen.getByText('me@x.com')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the trimmed name when it is present', () => {
|
||||||
|
seedUser({ name: 'Gib', email: 'me@x.com' });
|
||||||
|
render(<AvatarDropdown />);
|
||||||
|
expect(screen.getByText('Gib')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('me@x.com')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('links Edit Profile directly to the settings profile page', () => {
|
||||||
|
seedUser({ name: 'Gib', email: 'me@x.com' });
|
||||||
|
render(<AvatarDropdown />);
|
||||||
|
const editLink = screen.getByRole('link', { name: /edit profile/i });
|
||||||
|
expect(editLink).toHaveAttribute('href', '/settings/profile');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user