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'); // Every entry carries kind + key so the UI can route 1:1 vs. room. for (const c of res.conversations) { expect(c.kind === 'peer' || c.kind === 'room').toBe(true); expect(typeof c.key).toBe('string'); } }); it('/api/chat/messages without peer or room_uid 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); }); it('/api/chat/messages with unknown room returns empty array', async () => { const res = await fetch('api/chat/messages?room_uid=__no_such_room__') .then(r => r.json()); expect(Array.isArray(res.messages)).toBe(true); expect(res.messages.length).toBe(0); }); it('/api/chat/rooms returns rooms array', async () => { const res = await fetch('api/chat/rooms').then(r => r.json()); expect(res.ok).toBe(true); expect(Array.isArray(res.rooms)).toBe(true); }); it('/api/chat/thread-settings round-trips disposable flag', async () => { const peer = '__test_disposable_flip__'; // Default: 0 const initial = await fetch('api/chat/thread-settings?peer=' + encodeURIComponent(peer)) .then(r => r.json()); expect(initial.disposable).toBe(0); // Set to 1 const csrf = document.querySelector('meta[name="csrf"]')?.content || ''; const set = await fetch('api/chat/thread-settings', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf }, body: JSON.stringify({ peer, disposable: true }), }).then(r => r.json()); expect(set.disposable).toBe(1); // Verify const after = await fetch('api/chat/thread-settings?peer=' + encodeURIComponent(peer)) .then(r => r.json()); expect(after.disposable).toBe(1); }); }); describe('Chat: group creation UI', () => { 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')); } }; it('pick view has 1:1 / group mode switch', async () => { await ensureFlyoutClosed(); click('#chat-footer-btn'); await waitFor(() => !$('#chat-flyout').classList.contains('hidden')); await sleep(80); click('#chat-new-btn'); await sleep(60); const mode = document.querySelectorAll('.chat-pick-mode-btn'); expect(mode.length).toBe(2); // Toggling to group reveals the name input and action bar. const groupBtn = document.querySelector('.chat-pick-mode-btn[data-mode="group"]'); click(groupBtn); await sleep(30); expect($('#chat-group-name').classList.contains('hidden')).toBe(false); expect($('#chat-group-actions').classList.contains('hidden')).toBe(false); await ensureFlyoutClosed(); }); it('room-info view element exists', () => { expect($('#chat-room-info-view')).toExist(); expect($('#chat-room-info-back')).toExist(); expect($('#chat-room-info-body')).toExist(); }); });