// @vitest-environment happy-dom import '@testing-library/jest-dom/vitest' import { render, screen, fireEvent, waitFor, act } from '@testing-library/react' import { vi, describe, it, expect, beforeEach } from 'vitest' import { UpdateCredentialsDialog } from './UpdateCredentialsDialog' // Captured mutation options from the latest useMutation call let capturedOnSuccess: (() => void) | undefined let capturedOnError: ((err: { message: string }) => void) | undefined const mockMutate = vi.fn() const mockInvalidate = vi.fn() vi.mock('@/lib/trpc', () => ({ trpc: { updateAccountAuth: { useMutation: vi.fn((opts?: { onSuccess?: () => void onError?: (err: { message: string }) => void }) => { capturedOnSuccess = opts?.onSuccess capturedOnError = opts?.onError return { mutate: mockMutate, isPending: false } }), }, useUtils: vi.fn(() => ({ systemHealthCheck: { invalidate: mockInvalidate }, })), }, })) vi.mock('sonner', () => ({ toast: { success: vi.fn() }, })) import { toast } from 'sonner' const account = { id: 'acc-1', email: 'alice@example.com', provider: 'claude', } describe('UpdateCredentialsDialog', () => { beforeEach(() => { vi.clearAllMocks() capturedOnSuccess = undefined capturedOnError = undefined }) // Test 1: Renders with read-only identity it('renders account email and provider as non-editable', () => { render() expect(screen.getByText('alice@example.com')).toBeInTheDocument() expect(screen.getByText('claude')).toBeInTheDocument() // They must not be editable inputs const emailEl = screen.getByText('alice@example.com') expect(emailEl.tagName).not.toBe('INPUT') const providerEl = screen.getByText('claude') expect(providerEl.tagName).not.toBe('INPUT') }) // Test 2: Tab A submit — empty token — shows Required, no mutation it('shows Required error when token is empty and does not call mutation', async () => { render() fireEvent.click(screen.getByRole('button', { name: /update credentials/i })) await waitFor(() => expect(screen.getByText('Required')).toBeInTheDocument()) expect(mockMutate).not.toHaveBeenCalled() }) // Test 3: Tab A submit — valid token — calls mutation with correct input it('calls mutation with correct token input', async () => { render() fireEvent.change(screen.getByLabelText(/setup token/i), { target: { value: 'test-token' } }) fireEvent.click(screen.getByRole('button', { name: /update credentials/i })) await waitFor(() => expect(mockMutate).toHaveBeenCalledWith({ id: 'acc-1', configJson: '{"hasCompletedOnboarding":true}', credentials: '{"claudeAiOauth":{"accessToken":"test-token"}}', })) }) // Test 4: Tab B submit — invalid config JSON — shows "Invalid JSON", no mutation it('shows Invalid JSON error for bad config JSON and does not call mutation', async () => { render() fireEvent.click(screen.getByText('By credentials JSON')) fireEvent.change(screen.getByLabelText(/config json/i), { target: { value: 'bad json' } }) fireEvent.click(screen.getByRole('button', { name: /update credentials/i })) await waitFor(() => expect(screen.getByText('Invalid JSON')).toBeInTheDocument()) expect(mockMutate).not.toHaveBeenCalled() }) // Test 5: Tab B submit — empty textareas — calls mutation with '{}' defaults it("calls mutation with '{}' defaults when credentials textareas are empty", async () => { render() fireEvent.click(screen.getByText('By credentials JSON')) fireEvent.click(screen.getByRole('button', { name: /update credentials/i })) await waitFor(() => expect(mockMutate).toHaveBeenCalledWith({ id: 'acc-1', configJson: '{}', credentials: '{}', })) }) // Test 6: Server error — shows inline error, dialog stays open it('shows server error inline and does not close dialog on error', async () => { const onOpenChange = vi.fn() render() fireEvent.change(screen.getByLabelText(/setup token/i), { target: { value: 'some-token' } }) fireEvent.click(screen.getByRole('button', { name: /update credentials/i })) act(() => { capturedOnError?.({ message: 'Token expired' }) }) await waitFor(() => expect(screen.getByText('Token expired')).toBeInTheDocument()) expect(onOpenChange).not.toHaveBeenCalled() }) // Test 7: Success — toasts and closes dialog it('shows success toast and closes dialog on success', async () => { const onOpenChange = vi.fn() render() fireEvent.change(screen.getByLabelText(/setup token/i), { target: { value: 'some-token' } }) fireEvent.click(screen.getByRole('button', { name: /update credentials/i })) act(() => { capturedOnSuccess?.() }) await waitFor(() => { expect(toast.success).toHaveBeenCalledWith('Credentials updated for alice@example.com.') expect(onOpenChange).toHaveBeenCalledWith(false) }) }) // Test 8: State reset on re-open it('resets state when dialog is re-opened', async () => { const { rerender } = render( ) // Type a token and trigger server error fireEvent.change(screen.getByLabelText(/setup token/i), { target: { value: 'old-token' } }) fireEvent.click(screen.getByRole('button', { name: /update credentials/i })) act(() => { capturedOnError?.({ message: 'Server error' }) }) await waitFor(() => expect(screen.getByText('Server error')).toBeInTheDocument()) // Close dialog rerender() // Re-open dialog rerender() // Token should be cleared const tokenInput = screen.getByLabelText(/setup token/i) as HTMLInputElement expect(tokenInput.value).toBe('') // Server error should be gone expect(screen.queryByText('Server error')).not.toBeInTheDocument() }) })