import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('../../../shared/state', async () => { const sendMessage = vi.fn(); const escapeHtml = (s: string): string => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]!)); return { sendMessage, escapeHtml, popOutToTab: vi.fn(), isInTab: vi.fn(() => false), openVaultTab: vi.fn() }; }); import { renderAttachmentsDisclosure, wireAttachmentsDisclosure } from '../attachments-disclosure'; import { sendMessage } from '../../../shared/state'; import type { AttachmentRef } from '../../../shared/types'; const REF1: AttachmentRef = { id: 'a1', filename: 'doc.pdf', mime_type: 'application/pdf', size: 12345, created: 1700000000 }; const REF2: AttachmentRef = { id: 'a2', filename: 'photo.png', mime_type: 'image/png', size: 240000, created: 1700000001 }; describe('attachments-disclosure render', () => { it('renders empty state with no rows in edit mode', () => { const html = renderAttachmentsDisclosure({ itemId: 'i1', attachments: [], mode: 'edit', onChange: vi.fn() }); expect(html).toContain('attachments'); expect(html).toContain('+ attach file'); expect(html).not.toContain('attachment-row'); }); it('renders rows + remove buttons in edit mode', () => { const html = renderAttachmentsDisclosure({ itemId: 'i1', attachments: [REF1, REF2], mode: 'edit', onChange: vi.fn() }); expect(html).toContain('doc.pdf'); expect(html).toContain('photo.png'); expect(html).toContain('×'); expect(html).toContain('attachment-row__thumb'); // image-mime row gets thumb hook }); it('renders rows + download buttons in view mode (no add btn)', () => { const html = renderAttachmentsDisclosure({ itemId: 'i1', attachments: [REF1], mode: 'view' }); expect(html).toContain('↓'); expect(html).not.toContain('+ attach file'); }); }); describe('attachments-disclosure wiring', () => { beforeEach(() => { vi.mocked(sendMessage).mockReset(); }); it('clicking + attach triggers file input click', () => { document.body.innerHTML = renderAttachmentsDisclosure({ itemId: 'i1', attachments: [], mode: 'edit', onChange: vi.fn() }); const fileInput = document.querySelector('.attachments-disclosure__file-input') as HTMLInputElement; const clickSpy = vi.spyOn(fileInput, 'click'); wireAttachmentsDisclosure(document.body, { itemId: 'i1', attachments: [], mode: 'edit', onChange: vi.fn() }); (document.querySelector('.attachment-add-btn') as HTMLButtonElement).click(); expect(clickSpy).toHaveBeenCalled(); }); it('clicking × calls onChange with the attachment removed', () => { const onChange = vi.fn(); document.body.innerHTML = renderAttachmentsDisclosure({ itemId: 'i1', attachments: [REF1, REF2], mode: 'edit', onChange }); wireAttachmentsDisclosure(document.body, { itemId: 'i1', attachments: [REF1, REF2], mode: 'edit', onChange }); (document.querySelectorAll('.attachment-row__remove')[0] as HTMLElement).click(); expect(onChange).toHaveBeenCalledWith([REF2]); }); it('clicking ↓ in view mode sends download_attachment', async () => { vi.mocked(sendMessage).mockResolvedValueOnce({ ok: true, data: { bytes: new ArrayBuffer(10), filename: 'doc.pdf', mimeType: 'application/pdf' } }); document.body.innerHTML = renderAttachmentsDisclosure({ itemId: 'i1', attachments: [REF1], mode: 'view' }); wireAttachmentsDisclosure(document.body, { itemId: 'i1', attachments: [REF1], mode: 'view' }); (document.querySelector('.attachment-row__download') as HTMLElement).click(); await new Promise((r) => setTimeout(r, 50)); expect(vi.mocked(sendMessage)).toHaveBeenCalledWith(expect.objectContaining({ type: 'download_attachment', itemId: 'i1', attachmentId: 'a1', })); }); });