Files
electric-cherry/tests/unit/ui-moderation.test.ts
T
2026-10-17 21:30:43 -04:00

124 lines
4.8 KiB
TypeScript

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<typeof currentPage.subscribe>[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('&amp;', '&'),
'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.'
);
});