diff --git a/apps/web/app/accounts/AccountsList.test.tsx b/apps/web/app/accounts/AccountsList.test.tsx new file mode 100644 index 0000000..5cab37c --- /dev/null +++ b/apps/web/app/accounts/AccountsList.test.tsx @@ -0,0 +1,104 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { AccountsList } from './AccountsList'; + +const mockAccounts = [ + { id: 'acc_1', jid: '111@s.whatsapp.net', displayName: 'Account One', status: 'ACTIVE', platform: 'whatsapp' }, + { id: 'acc_2', jid: '222@s.whatsapp.net', displayName: null, status: 'DISCONNECTED', platform: 'whatsapp' }, +]; + +let fetchSpy: jest.SpyInstance; + +beforeEach(() => { + fetchSpy = jest.spyOn(global, 'fetch'); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('AccountsList', () => { + it('renders an AccountCard for each initial account', () => { + render(); + expect(screen.getByText('Account One')).toBeInTheDocument(); + expect(screen.getByText('222@s.whatsapp.net')).toBeInTheDocument(); + }); + + it('shows empty state when no accounts', () => { + render(); + expect(screen.getByText(/no accounts yet/i)).toBeInTheDocument(); + }); + + it('renders Add Account button', () => { + render(); + expect(screen.getByRole('button', { name: /add account/i })).toBeInTheDocument(); + }); + + it('renders display name input', () => { + render(); + expect(screen.getByPlaceholderText(/display name/i)).toBeInTheDocument(); + }); + + it('calls POST /api/accounts when Add Account is clicked', async () => { + fetchSpy.mockResolvedValue( + new Response( + JSON.stringify({ id: 'acc_new', jid: 'pending_x@placeholder', displayName: null, status: 'ACTIVE', platform: 'whatsapp' }), + { status: 201, headers: { 'Content-Type': 'application/json' } }, + ), + ); + render(); + fireEvent.click(screen.getByRole('button', { name: /add account/i })); + await waitFor(() => + expect(fetchSpy).toHaveBeenCalledWith('/api/accounts', expect.objectContaining({ method: 'POST' })), + ); + }); + + it('adds new account to list after successful POST', async () => { + const newAccount = { id: 'acc_new', jid: 'pending_x@placeholder', displayName: 'New Device', status: 'ACTIVE', platform: 'whatsapp' }; + fetchSpy.mockResolvedValue( + new Response(JSON.stringify(newAccount), { + status: 201, + headers: { 'Content-Type': 'application/json' }, + }), + ); + render(); + const input = screen.getByPlaceholderText(/display name/i); + fireEvent.change(input, { target: { value: 'New Device' } }); + fireEvent.click(screen.getByRole('button', { name: /add account/i })); + await waitFor(() => expect(screen.getByText('New Device')).toBeInTheDocument()); + }); + + it('sends displayName in POST body when entered', async () => { + fetchSpy.mockResolvedValue( + new Response( + JSON.stringify({ id: 'acc_new', jid: 'pending_x@placeholder', displayName: 'Test Name', status: 'ACTIVE', platform: 'whatsapp' }), + { status: 201, headers: { 'Content-Type': 'application/json' } }, + ), + ); + render(); + const input = screen.getByPlaceholderText(/display name/i); + fireEvent.change(input, { target: { value: 'Test Name' } }); + fireEvent.click(screen.getByRole('button', { name: /add account/i })); + await waitFor(() => + expect(fetchSpy).toHaveBeenCalledWith( + '/api/accounts', + expect.objectContaining({ + body: JSON.stringify({ displayName: 'Test Name' }), + }), + ), + ); + }); + + it('clears the input after successful account creation', async () => { + fetchSpy.mockResolvedValue( + new Response( + JSON.stringify({ id: 'acc_new', jid: 'pending_x@placeholder', displayName: null, status: 'ACTIVE', platform: 'whatsapp' }), + { status: 201, headers: { 'Content-Type': 'application/json' } }, + ), + ); + render(); + const input = screen.getByPlaceholderText(/display name/i) as HTMLInputElement; + fireEvent.change(input, { target: { value: 'My Device' } }); + fireEvent.click(screen.getByRole('button', { name: /add account/i })); + await waitFor(() => expect(input.value).toBe('')); + }); +}); diff --git a/apps/web/app/accounts/AccountsList.tsx b/apps/web/app/accounts/AccountsList.tsx new file mode 100644 index 0000000..4b638d7 --- /dev/null +++ b/apps/web/app/accounts/AccountsList.tsx @@ -0,0 +1,60 @@ +'use client'; +import { useState } from 'react'; +import { AccountCard } from './AccountCard'; + +interface Account { + id: string; + jid: string; + displayName: string | null; + status: string; + platform: string; +} + +export function AccountsList({ initialAccounts }: { initialAccounts: Account[] }) { + const [accounts, setAccounts] = useState(initialAccounts); + const [displayName, setDisplayName] = useState(''); + + async function handleAdd() { + try { + const res = await fetch('/api/accounts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ displayName: displayName || undefined }), + }); + if (!res.ok) return; + const account: Account = await res.json(); + setAccounts((prev) => [...prev, account]); + setDisplayName(''); + } catch {} + } + + return ( +
+
+ setDisplayName(e.target.value)} + placeholder="Display name (optional)" + className="flex-1 border rounded px-3 py-2 text-sm" + /> + +
+ + {accounts.length === 0 ? ( +

No accounts yet. Add one above to get started.

+ ) : ( +
+ {accounts.map((a) => ( + + ))} +
+ )} +
+ ); +} diff --git a/apps/web/app/accounts/page.tsx b/apps/web/app/accounts/page.tsx index 61c375d..d076132 100644 --- a/apps/web/app/accounts/page.tsx +++ b/apps/web/app/accounts/page.tsx @@ -1,4 +1,4 @@ -import { AccountCard } from './AccountCard'; +import { AccountsList } from './AccountsList'; interface Account { id: string; @@ -19,15 +19,7 @@ export default async function AccountsPage() { return (

Accounts

- {accounts.length === 0 ? ( -

No accounts found.

- ) : ( -
- {accounts.map((a) => ( - - ))} -
- )} +
); } diff --git a/apps/web/app/api/accounts/route.ts b/apps/web/app/api/accounts/route.ts index fc71ed7..dbbd90c 100644 --- a/apps/web/app/api/accounts/route.ts +++ b/apps/web/app/api/accounts/route.ts @@ -4,3 +4,13 @@ export async function GET() { const res = await fetch(`${API_URL}/accounts`, { cache: 'no-store' }); return Response.json(await res.json(), { status: res.status }); } + +export async function POST(req: Request) { + const body = await req.json(); + const res = await fetch(`${API_URL}/accounts`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + return Response.json(await res.json(), { status: res.status }); +}