import { describe, it, expect, click, $, sleep, waitFor } from './runner.js?v=4'; // Chat plugin tests — UI-focused. No send/receive here: that would // require a real relay, and the user's "no mocks" rule means we don't // fake one. Runs against the real /api/chat endpoints, which respond // cleanly on a fresh install (empty conversation list). const ensureFlyoutClosed = async () => { const f = $('#chat-flyout'); if (!f) return; if (!f.classList.contains('hidden')) { click('#chat-close-btn'); await waitFor(() => f.classList.contains('hidden')); } }; describe('Chat: flyout', () => { it('footer button is injected', () => { expect($('#chat-footer-btn')).toExist(); }); it('toggles open and closed', async () => { await ensureFlyoutClosed(); const f = $('#chat-flyout'); click('#chat-footer-btn'); await waitFor(() => !f.classList.contains('hidden')); expect(f.classList.contains('hidden')).toBe(false); click('#chat-close-btn'); await waitFor(() => f.classList.contains('hidden')); expect(f.classList.contains('hidden')).toBe(true); }); it('header-back visibility matches active view', async () => { await ensureFlyoutClosed(); click('#chat-footer-btn'); await waitFor(() => !$('#chat-flyout').classList.contains('hidden')); // Wait for loadConversations + initial view pick to settle. await sleep(150); const back = $('#chat-header-back'); const threadShown = !$('#chat-thread-view').classList.contains('hidden'); const listShown = !$('#chat-list-view').classList.contains('hidden'); // Exactly one of thread / list should be active here (pick view // is only reached via the + button). expect(threadShown || listShown).toBe(true); if (threadShown) expect(back.classList.contains('hidden')).toBe(false); if (listShown) expect(back.classList.contains('hidden')).toBe(true); await ensureFlyoutClosed(); }); it('new-conversation button opens pick view', async () => { await ensureFlyoutClosed(); click('#chat-footer-btn'); await waitFor(() => !$('#chat-flyout').classList.contains('hidden')); await sleep(100); click('#chat-new-btn'); await sleep(50); expect($('#chat-pick-view').classList.contains('hidden')).toBe(false); expect($('#chat-list-view').classList.contains('hidden')).toBe(true); expect($('#chat-thread-view').classList.contains('hidden')).toBe(true); // Header back is only for thread view — not needed when pick has // its own dedicated back button. expect($('#chat-header-back').classList.contains('hidden')).toBe(true); click('#chat-pick-back'); await sleep(50); expect($('#chat-pick-view').classList.contains('hidden')).toBe(true); await ensureFlyoutClosed(); }); }); describe('Chat: thread DOM structure', () => { it('peer-strip element lives inside thread view', () => { const strip = $('#chat-thread-peers'); expect(strip).toExist(); expect(strip.closest('#chat-thread-view')).toExist(); }); it('thread form, input and send button exist', () => { expect($('#chat-thread-form')).toExist(); expect($('#chat-thread-input')).toExist(); expect($('#chat-thread-send')).toExist(); }); }); describe('Chat: API', () => { it('/api/chat/conversations returns expected shape', async () => { const res = await fetch('api/chat/conversations').then(r => r.json()); expect(Array.isArray(res.conversations)).toBe(true); expect(typeof res.total_unread).toBe('number'); }); it('/api/chat/messages without peer returns 400', async () => { const r = await fetch('api/chat/messages'); expect(r.status).toBe(400); }); it('/api/chat/messages with unknown peer returns empty array', async () => { const res = await fetch('api/chat/messages?peer=__test_no_such_peer__') .then(r => r.json()); expect(Array.isArray(res.messages)).toBe(true); expect(res.messages.length).toBe(0); }); });