101 lines
3.2 KiB
TypeScript
101 lines
3.2 KiB
TypeScript
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');
|
|
});
|
|
});
|