diff --git a/apps/next/src/app/(app)/settings/profile/page.tsx b/apps/next/src/app/(app)/settings/profile/page.tsx index d6e80ca..a585525 100644 --- a/apps/next/src/app/(app)/settings/profile/page.tsx +++ b/apps/next/src/app/(app)/settings/profile/page.tsx @@ -1,5 +1,3 @@ -'use server'; - import { AvatarUpload, ProfileHeader, diff --git a/apps/next/src/components/layout/header/controls/AvatarDropdown.tsx b/apps/next/src/components/layout/header/controls/AvatarDropdown.tsx index 2af472e..21c598e 100644 --- a/apps/next/src/components/layout/header/controls/AvatarDropdown.tsx +++ b/apps/next/src/components/layout/header/controls/AvatarDropdown.tsx @@ -29,6 +29,10 @@ export const AvatarDropdown = () => { user?.image && !remoteImageUrl ? { storageId: user.image } : 'skip', ); 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) { return ( @@ -61,16 +65,16 @@ export const AvatarDropdown = () => { /> - {(user?.name ?? user?.email) && ( + {displayName && ( <> - {user.name?.trim() ?? user.email?.trim()} + {displayName} )} - + Edit Profile diff --git a/apps/next/tests/component/avatar-dropdown.test.tsx b/apps/next/tests/component/avatar-dropdown.test.tsx new file mode 100644 index 0000000..66e71db --- /dev/null +++ b/apps/next/tests/component/avatar-dropdown.test.tsx @@ -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: () =>
avatar
, + Button: (props: ComponentProps<'button'>) =>