// @vitest-environment happy-dom import { render, screen, waitFor } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; import React, { useState } from 'react'; import { describe, expect, it, vi } from 'vitest'; import { VaultContext, type VaultStatus } from '../contexts/vault-context.js'; import { VaultUnlock } from '../screens/vault-unlock.js'; function makeCtx( overrides: Partial<{ status: VaultStatus; error: string | null; unlock: (pw: string) => Promise; upload: (file: File, force?: boolean) => Promise; }> = {}, ) { return { status: 'locked' as VaultStatus, error: null as string | null, unlock: vi.fn().mockResolvedValue(undefined), create: vi.fn(), upload: vi.fn().mockResolvedValue(undefined), ...overrides, }; } function renderUnlock(ctx = makeCtx()) { return render( , ); } describe('VaultUnlock', () => { it('renders the password input and submit button', () => { renderUnlock(); expect(screen.getByLabelText(/master password/i)).toBeTruthy(); expect(screen.getByRole('button', { name: /unlock/i })).toBeTruthy(); }); it('calls unlock with the entered password on submit', async () => { const unlock = vi.fn().mockResolvedValue(undefined); renderUnlock(makeCtx({ unlock })); await userEvent.type(screen.getByLabelText(/master password/i), 'secret123'); await userEvent.click(screen.getByRole('button', { name: /unlock/i })); expect(unlock).toHaveBeenCalledWith('secret123'); }); it('shows an error message when unlock sets vault.error', async () => { // Stateful wrapper simulates the real context updating error after a failed unlock. function UnlockWrapper() { const [error, setError] = useState(null); const unlock = vi.fn().mockImplementation(async () => { setError('Wrong password. Please try again.'); }); return ( ); } render(); await userEvent.type(screen.getByLabelText(/master password/i), 'badpass'); await userEvent.click(screen.getByRole('button', { name: /unlock/i })); await waitFor(() => { expect(screen.getByRole('alert').textContent).toContain('Wrong password'); }); }); it('does not show an error on initial render', () => { renderUnlock(); expect(screen.queryByRole('alert')).toBeNull(); }); }); describe('VaultContext upload shape', () => { it('renders without error when upload is provided in context', () => { function UploadWrapper() { const [status, setStatus] = useState('locked'); const upload = vi.fn().mockImplementation(async () => setStatus('locked')); return ( ); } render(); expect(screen.getByLabelText(/master password/i)).toBeTruthy(); }); }); describe('VaultUnlock — file upload', () => { it('renders the upload file input', () => { renderUnlock(); expect(screen.getByLabelText(/or upload a different vault/i)).toBeTruthy(); }); it('calls vault.upload when a file is selected', async () => { const upload = vi.fn().mockResolvedValue(undefined); renderUnlock(makeCtx({ upload })); const input = screen.getByLabelText(/or upload a different vault/i); const file = new File(['blob'], 'test.vault'); await userEvent.upload(input, file); expect(upload).toHaveBeenCalledWith(file); }); it('shows confirm dialog and calls upload with force=true on 409', async () => { const ApiError = (await import('../lib/api.js')).ApiError; const upload = vi .fn() .mockRejectedValueOnce(new ApiError(409, 'vault-already-exists')) .mockResolvedValueOnce(undefined); vi.stubGlobal('confirm', vi.fn().mockReturnValue(true)); renderUnlock(makeCtx({ upload })); await userEvent.upload(screen.getByLabelText(/or upload a different vault/i), new File(['b'], 'v')); await waitFor(() => expect(upload).toHaveBeenCalledTimes(2)); expect(upload).toHaveBeenLastCalledWith(expect.any(File), true); vi.unstubAllGlobals(); }); it('shows uploadError when force upload also fails', async () => { const upload = vi .fn() .mockRejectedValue(new (await import('../lib/api.js')).ApiError(409, 'x')); vi.stubGlobal('confirm', vi.fn().mockReturnValue(true)); renderUnlock(makeCtx({ upload })); await userEvent.upload(screen.getByLabelText(/or upload a different vault/i), new File(['b'], 'v')); await waitFor(() => expect(screen.getByRole('alert').textContent).toContain('Failed to replace'), ); vi.unstubAllGlobals(); }); });