import { afterEach, expect, it, vi } from 'vitest'; import { render } from 'svelte/server'; import { writable } from 'svelte/store'; import ChatPanel from '../../src/lib/ui/components/ChatPanel.svelte'; import PlayerScoreboard from '../../src/lib/ui/components/PlayerScoreboard.svelte'; import { moderationEventDetail, moderationHref } from '../../src/lib/ui/moderation.js'; import type { UserRole } from '../../src/lib/auth.js'; const currentPage = writable({ url: new URL('http://localhost/servers/t6_zm_transit_1'), data: { user: { role: 'moderator' as UserRole } } }); vi.mock('$app/stores', () => ({ page: { subscribe: (run: Parameters[0]) => currentPage.subscribe(run) } })); afterEach(() => currentPage.update((value) => ({ ...value, data: { user: { role: 'moderator' } } })) ); it('keeps ambiguous engine chat IDs in search and preserves selected evidence for verified lookup', () => { const body = render(ChatPanel, { props: { game: 't6', history: [ { type: 'message', matchId: 12, message: { guid: 'abcdef', player: 'Tank', message: 'Review & investigate', slot: 2, time: '0:30' }, context: { realTime: null, round: null, matchElapsedMs: null } } ] } }).body; const target = new URL( body.match(/href="([^"]*\/moderation[^"]*)"/)![1].replaceAll('&', '&'), 'http://localhost' ); expect(target.searchParams.get('q')).toBe('abcdef'); expect(target.searchParams.has('guid')).toBe(false); expect(target.searchParams.get('game')).toBe('t6'); expect(target.searchParams.get('matchId')).toBe('12'); expect(target.searchParams.get('message')).toBe('Review & investigate'); }); it('links scoreboard actions to the exact telemetry GUID and hides moderation from ordinary viewers', () => { const props = { report: { server: 't6_zm_transit_1', map: 'zm_transit', event: 'game_start' as const, receivedAt: '2026-10-15T12:00:00Z', players: [{ guid: '18446744073709551615', name: 'Tank' }] } }; const body = render(PlayerScoreboard, { props }).body; expect(body).toContain('guid=18446744073709551615'); expect(body).toContain('server=t6_zm_transit_1'); currentPage.update((value) => ({ ...value, data: { user: { role: 'viewer' } } })); expect(render(PlayerScoreboard, { props }).body).not.toContain('/moderation?'); }); it('does not invent a game namespace for an unscoped profile', () => { const target = new URL(moderationHref({ guid: '42', game: 'unknown' }), 'http://localhost'); expect(target.searchParams.get('guid')).toBe('42'); expect(target.searchParams.has('game')).toBe(false); }); it('presents case decisions and ownership changes as readable history while preserving staff text', () => { expect( moderationEventDetail( JSON.stringify({ title: { from: 'open', to: 'constructor' }, assignee: { from: 'open', to: 'constructor' } }), 'case.updated' ) ).toBe('Title: open → constructor\nAssigned to: open → constructor'); expect( moderationEventDetail( JSON.stringify({ title: { from: 'Report', to: 'Appeal review' }, assignee: { from: null, to: 'Moderator' }, assigneeId: { from: null, to: 'private-internal-user-id' }, status: { from: 'open', to: 'in_review' }, reason: 'Reviewing additional evidence' }), 'case.updated' ) ).toBe( 'Title: Report → Appeal review\nAssigned to: Unassigned → Moderator\nStatus: Open → In progress\nReviewing additional evidence' ); expect( moderationEventDetail( JSON.stringify({ from: 'appeal_denied', reason: 'New evidence' }), 'case.reopened' ) ).toBe('Previous status: Appeal denied\nNew evidence'); expect(moderationEventDetail('Decision with {unusual text}', 'case.resolved')).toBe( 'Decision with {unusual text}' ); expect( moderationEventDetail(JSON.stringify({ count: 2, matchId: 42 }), 'case.evidence_attached') ).toBe('2 archived chat entries attached from match #42.'); }); it('explains migrated status without hiding the historical decision', () => { expect( moderationEventDetail( JSON.stringify({ from: 'warning_issued', to: 'closed' }), 'case.status_migrated' ) ).toBe( 'Previous status: Warning issued → Closed. Earlier decisions and actions are preserved.' ); });