import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import type { Application, Artifact, Approval } from '@/types' // Mock the api module inline so vi.mock factory is self-contained vi.mock('@/api', () => ({ getApplications: vi.fn(), getArtifacts: vi.fn(), createCoverLetter: vi.fn(), createApproval: vi.fn(), confirmApproval: vi.fn(), rejectApproval: vi.fn(), outboxSend: vi.fn(), HttpError: class HttpError extends Error { status: number body: unknown constructor(status: number, body: unknown, msg?: string) { super(msg ?? `HTTP ${status}`) this.status = status this.body = body } } })) function makeApp(): Application { return { id: 'app-1', job_posting_id: 'j-1', state: 'drafting', score: 90, score_rationale: null, notes: '', state_changed_at: '2026-01-01T00:00:00Z', created_at: '2026-01-01T00:00:00Z', posting: { id: 'j-1', source: 'manual_url', external_id: null, url: 'http://x', company: 'Acme', title: 'Engineer', location: 'Remote', description: '', raw: {}, fetched_at: '2026-01-01T00:00:00Z' } } } function makeArtifact(): Artifact { return { id: 'art-1', application_id: 'app-1', kind: 'cover_letter', filename: 'cover.pdf', content_hash: 'abcdef0123456789', storage_path: '/tmp/cover.pdf', version: 1, origin: 'user_drafted', created_at: '2026-01-01T00:00:00Z' } } function makeApproval(confirmed: boolean): Approval { return { id: 'appr-1', application_id: 'app-1', artifact_id: 'art-1', artifact_hash: 'abcdef0123456789', action: 'send_email', confirmed_by_user: confirmed, confirmed_at: confirmed ? '2026-01-01T00:00:00Z' : null, expires_at: '2026-01-02T00:00:00Z', created_at: '2026-01-01T00:00:00Z' } } async function mountDetail(app: Application, artifacts: Artifact[]) { const pinia = createPinia() setActivePinia(pinia) const api = await import('@/api') ;(api.getApplications as ReturnType).mockResolvedValue([app]) ;(api.getArtifacts as ReturnType).mockResolvedValue(artifacts) const ApplicationDetail = (await import('@/views/ApplicationDetail.vue')).default const wrapper = mount(ApplicationDetail, { props: { id: 'app-1' } }) await flushPromises() return { wrapper, pinia } } /** Find the Send button (has class ml-2 to distinguish from the Save button) */ function findSendBtn(wrapper: ReturnType): ReturnType { return wrapper.find('button.ml-2') } describe('Approval widget', () => { beforeEach(() => { vi.clearAllMocks() }) it('disables Send until confirmed', async () => { const { wrapper } = await mountDetail(makeApp(), [makeArtifact()]) const api = await import('@/api') // Request approval (not confirmed yet) ;(api.createApproval as ReturnType).mockResolvedValue(makeApproval(false)) // Select artifact and action const selects = wrapper.findAll('select') await selects[0].setValue('art-1') await selects[1].setValue('send_email') // Click Request Approval (bg-gray-700) await wrapper.find('button.bg-gray-700').trigger('click') await flushPromises() // Send button should be disabled because not confirmed const sendBtn = findSendBtn(wrapper) expect(sendBtn.exists()).toBe(true) expect(sendBtn.attributes('disabled')).toBeDefined() // Now confirm ;(api.confirmApproval as ReturnType).mockResolvedValue(makeApproval(true)) const confirmBtn = wrapper.find('button.bg-green-600') await confirmBtn.trigger('click') await flushPromises() // Send button should now be enabled expect(sendBtn.attributes('disabled')).toBeUndefined() }) it('shows error toast on 409 send failure', async () => { const { wrapper, pinia } = await mountDetail(makeApp(), [makeArtifact()]) const api = await import('@/api') // Setup: approval already confirmed ;(api.createApproval as ReturnType).mockResolvedValue(makeApproval(true)) const selects = wrapper.findAll('select') await selects[0].setValue('art-1') await selects[1].setValue('send_email') await wrapper.find('button.bg-gray-700').trigger('click') await flushPromises() // Simulate 409 on send const httpErr = new api.HttpError(409, { error: { code: 'HASH_MISMATCH', message: 'Hash mismatch' } }) ;(api.outboxSend as ReturnType).mockRejectedValue(httpErr) const sendBtn = findSendBtn(wrapper) await sendBtn.trigger('click') await flushPromises() // Check the toast store for the error message const toastState = pinia.state.value.toast expect(toastState).toBeTruthy() expect(toastState.toasts.length).toBeGreaterThanOrEqual(1) const errorToasts = toastState.toasts.filter((t: { type: string }) => t.type === 'error') expect(errorToasts.length).toBeGreaterThanOrEqual(1) expect(errorToasts[0].message).toContain('Hash mismatch') }) })