153 lines
6.3 KiB
TypeScript
153 lines
6.3 KiB
TypeScript
import { afterEach, expect, it, vi } from 'vitest';
|
|
import { flushSync, mount, tick, unmount } from 'svelte';
|
|
import { writable } from 'svelte/store';
|
|
import ChatPanel from '../../src/lib/ui/components/ChatPanel.svelte';
|
|
import type { ContextualChatEntry } from '../../src/lib/game-chat.js';
|
|
import type { UserRole } from '../../src/lib/auth.js';
|
|
|
|
const currentPage = writable({ data: { user: { role: 'moderator' as UserRole } } });
|
|
vi.mock('$app/stores', () => ({
|
|
page: {
|
|
subscribe: (run: Parameters<typeof currentPage.subscribe>[0]) => currentPage.subscribe(run)
|
|
}
|
|
}));
|
|
|
|
let component: ReturnType<typeof mount> | undefined;
|
|
afterEach(async () => {
|
|
if (component) await unmount(component);
|
|
component = undefined;
|
|
document.body.replaceChildren();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
const history: ContextualChatEntry[] = [
|
|
['11', 'Subject', 'A concerning reply'],
|
|
['22', 'Other player', 'The question that explains it'],
|
|
['11', 'Subject', 'A later reply']
|
|
].map(([guid, player, message], index) => ({
|
|
type: 'message',
|
|
matchId: 17,
|
|
messageId: `17:message-${index}:1`,
|
|
message: { guid, player, message, time: `1:0${index}`, slot: index },
|
|
context: { realTime: null, matchElapsedMs: index * 1000, round: 2 }
|
|
}));
|
|
|
|
const button = (label: string) =>
|
|
[...document.querySelectorAll('button')].find((node) => node.textContent?.trim() === label)!;
|
|
const response = (value: unknown) => new Response(JSON.stringify(value));
|
|
function show(role: UserRole = 'moderator') {
|
|
currentPage.set({ data: { user: { role } } });
|
|
component = mount(ChatPanel, { target: document.body, props: { game: 't5', history } });
|
|
flushSync();
|
|
}
|
|
function select(label: string, value: string) {
|
|
const labelNode = [...document.querySelectorAll('dialog label')].find(
|
|
(node) => node.textContent?.replace('*', '').trim() === label
|
|
)!;
|
|
const node = document.querySelector<HTMLSelectElement>(`[aria-labelledby="${labelNode.id}"]`)!;
|
|
const originalQuery = node.querySelector.bind(node);
|
|
node.querySelector = ((selector: string) =>
|
|
selector === ':checked'
|
|
? (node.selectedOptions[0] ?? null)
|
|
: originalQuery(selector)) as typeof node.querySelector;
|
|
node.value = value;
|
|
node.dispatchEvent(new Event('change', { bubbles: true }));
|
|
flushSync();
|
|
}
|
|
const submit = () =>
|
|
document
|
|
.querySelector('dialog form')!
|
|
.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
|
|
it('selects messages across participants, previews their context, and attaches to an existing case', async () => {
|
|
const fetch = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(
|
|
response({
|
|
cases: [{ id: 'case-1', title: 'Existing review', status: 'open', revision: 4 }],
|
|
total: 1
|
|
})
|
|
)
|
|
.mockResolvedValueOnce(response({ ok: true, id: 'case-1' }));
|
|
vi.stubGlobal('fetch', fetch);
|
|
show();
|
|
expect(button('Attach to case').disabled).toBe(true);
|
|
const checkboxes = document.querySelectorAll<HTMLInputElement>('input[type="checkbox"]');
|
|
checkboxes[0].click();
|
|
checkboxes[1].click();
|
|
flushSync();
|
|
expect(document.body.textContent).toContain('2 selected');
|
|
button('Attach to case').click();
|
|
await vi.waitFor(() =>
|
|
expect(document.querySelector('dialog')?.textContent).toContain('Existing review')
|
|
);
|
|
expect(fetch.mock.calls[0][0]).toContain('game=t5&guid=11');
|
|
expect(document.querySelector('dialog')?.textContent).toContain('GUID 22');
|
|
expect(document.querySelector('dialog')?.textContent).toContain('Unknown time');
|
|
const excerpt = document.querySelector('dialog [aria-label="Preserved conversation excerpt"]')!;
|
|
expect(excerpt.querySelectorAll('li')).toHaveLength(3);
|
|
expect(excerpt.querySelector('.highlighted')?.textContent).toContain('A concerning reply');
|
|
expect(excerpt.textContent).toContain('Selected context');
|
|
select('Destination', 'case-1');
|
|
submit();
|
|
await vi.waitFor(() => expect(document.body.textContent).toContain('Selected evidence saved.'));
|
|
expect(JSON.parse(fetch.mock.calls[1][1].body)).toEqual({
|
|
op: 'attach-evidence',
|
|
id: 'case-1',
|
|
revision: 4,
|
|
evidence: {
|
|
matchId: 17,
|
|
messageIds: ['17:message-0:1', '17:message-1:1'],
|
|
contextBefore: 3,
|
|
contextAfter: 3
|
|
},
|
|
idempotencyKey: expect.any(String)
|
|
});
|
|
expect(document.querySelector('a[href="/moderation/cases/case-1"]')).not.toBeNull();
|
|
expect(
|
|
[...document.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')].every(
|
|
(node) => !node.checked
|
|
)
|
|
).toBe(true);
|
|
});
|
|
|
|
it('opens a new case with all selected messages and retries an uncertain request unchanged', async () => {
|
|
const fetch = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(response({ cases: [], total: 0 }))
|
|
.mockRejectedValueOnce(new Error('Connection lost'))
|
|
.mockResolvedValueOnce(response({ ok: true, id: 'new-case' }));
|
|
vi.stubGlobal('fetch', fetch);
|
|
show();
|
|
button('Select all').click();
|
|
flushSync();
|
|
button('Attach to case').click();
|
|
await vi.waitFor(() =>
|
|
expect(document.querySelector('dialog')?.textContent).toContain('No existing cases')
|
|
);
|
|
const title = document.querySelector<HTMLInputElement>('dialog input:not([type="number"])')!;
|
|
title.value = 'Conversation review';
|
|
title.dispatchEvent(new Event('input', { bubbles: true }));
|
|
flushSync();
|
|
submit();
|
|
await vi.waitFor(() => expect(button('Retry same request')).toBeDefined());
|
|
expect(document.querySelector('dialog fieldset')?.hasAttribute('disabled')).toBe(true);
|
|
button('Retry same request').click();
|
|
await vi.waitFor(() => expect(fetch).toHaveBeenCalledTimes(3));
|
|
expect(fetch.mock.calls[2][1].body).toBe(fetch.mock.calls[1][1].body);
|
|
expect(JSON.parse(fetch.mock.calls[1][1].body)).toMatchObject({
|
|
op: 'create-case',
|
|
game: 't5',
|
|
guid: '11',
|
|
title: 'Conversation review',
|
|
evidence: { messageIds: history.map((entry) => entry.messageId) }
|
|
});
|
|
await tick();
|
|
});
|
|
|
|
it('keeps evidence selection unavailable to read-only viewers', () => {
|
|
show('super_viewer');
|
|
expect(document.querySelector('input[type="checkbox"]')).toBeNull();
|
|
expect(button('Attach to case')).toBeUndefined();
|
|
});
|