// @vitest-environment jsdom import { beforeEach, describe, expect, it, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { EntryForm } from './EntryForm'; const { getPlatformAccessTokenMock, createManualEntryMock, updateManualEntryMock, authMock } = vi.hoisted(() => ({ getPlatformAccessTokenMock: vi.fn(), createManualEntryMock: vi.fn(), updateManualEntryMock: vi.fn(), authMock: { user: { id: 'user-1' } as any } })); vi.mock('../components/AuthContext', () => ({ useAuth: () => authMock })); vi.mock('../lib/authSession', () => ({ getPlatformAccessToken: getPlatformAccessTokenMock })); vi.mock('../lib/manualEntriesApi', () => ({ createManualEntry: createManualEntryMock, updateManualEntry: updateManualEntryMock })); describe('EntryForm DOM flow', () => { const alertMock = vi.fn(); beforeEach(() => { vi.clearAllMocks(); createManualEntryMock.mockReset(); updateManualEntryMock.mockReset(); getPlatformAccessTokenMock.mockReset(); authMock.user = { id: 'user-1' }; createManualEntryMock.mockResolvedValue({}); updateManualEntryMock.mockResolvedValue({}); getPlatformAccessTokenMock.mockRejectedValue(new Error('Not authenticated')); vi.stubGlobal('fetch', vi.fn()); vi.stubGlobal('confirm', vi.fn(() => true)); vi.stubGlobal('alert', alertMock); vi.spyOn(crypto, 'randomUUID').mockReturnValue('11111111-1111-1111-1111-111111111111'); }); it('inserts a new manual entry with numeric normalization', async () => { const onSuccess = vi.fn(); const user = userEvent.setup(); render(); await user.type(screen.getByPlaceholderText('BTC/USD'), 'BTC/USD'); await user.type(screen.getByPlaceholderText('0.00'), '100'); await user.type(screen.getByPlaceholderText('0'), '1'); await user.click(screen.getByRole('button', { name: 'Add' })); await waitFor(() => { expect(createManualEntryMock).toHaveBeenCalled(); expect(onSuccess).toHaveBeenCalled(); }); }); it('alerts error when trade execution fails', async () => { const onSuccess = vi.fn(); const user = userEvent.setup(); getPlatformAccessTokenMock.mockResolvedValue('valid-token'); vi.mocked(fetch).mockResolvedValue({ ok: false, json: async () => ({ success: false, error: 'Insufficient funds' }) } as any); render(); await user.type(screen.getByPlaceholderText('BTC/USD'), 'BTC/USD'); await user.type(screen.getByPlaceholderText('0'), '1'); await user.click(screen.getByRole('checkbox', { name: /Execute/i })); await user.click(screen.getByRole('button', { name: 'Add' })); await waitFor(() => { expect(alertMock).toHaveBeenCalledWith(expect.stringContaining('Execution Failed: Insufficient funds')); }); }); it('journals a past trade (entry + exit both provided)', async () => { const onSuccess = vi.fn(); const user = userEvent.setup(); render(); await user.type(screen.getByPlaceholderText('BTC/USD'), 'BTC/USD'); await user.type(screen.getByPlaceholderText('0.00'), '100'); await user.type(screen.getByPlaceholderText('Exit'), '110'); await user.type(screen.getByPlaceholderText('0'), '1'); await user.click(screen.getByRole('button', { name: 'Add' })); await waitFor(() => { expect(createManualEntryMock).toHaveBeenCalledTimes(1); }); expect(createManualEntryMock.mock.calls[0][0]).toEqual(expect.objectContaining({ symbol: 'BTC/USD', buy_price: 100, sell_price: 110 })); }); it('handles database error by alerting user', async () => { createManualEntryMock.mockRejectedValueOnce(new Error('DB Error')); const user = userEvent.setup(); render(); await user.type(screen.getByPlaceholderText('BTC/USD'), 'FAIL/USD'); await user.click(screen.getByRole('button', { name: 'Add' })); await waitFor(() => { expect(alertMock).toHaveBeenCalledWith(expect.stringContaining('DB Error')); }); }); it('does nothing when submitted without a user', async () => { authMock.user = null; const user = userEvent.setup(); render(); await user.type(screen.getByPlaceholderText('BTC/USD'), 'BTC/USD'); await user.click(screen.getByRole('button', { name: 'Add' })); expect(createManualEntryMock).not.toHaveBeenCalled(); }); });