Extends chat.test.js to cover the new conversation shape (kind/key fields), the /api/chat/rooms endpoint, the thread-settings round-trip, and the group-creation pick-view toggle. Room-info view DOM is structurally checked. Bumps to 1.9.4. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
176 lines
7.1 KiB
JavaScript
176 lines
7.1 KiB
JavaScript
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();
|
|
});
|
|
});
|