import { describe, expect, it } from 'vitest'; import { parseGameChat, parseGameChatTimeline } from '../../src/lib/game-chat.js'; describe('game chat', () => { it('extracts say messages and ignores joins', () => { expect(parseGameChat([ ' 61:19 J;6493949;3;Conda1989', ' 63:09 say;4229696;0;alienfr;\x15just noticed i didnt have ANY perks go figure', ' 63:22 say;2989140;1;mikakyu;\x15whoopsie' ])).toEqual([ { time: '63:09', guid: '4229696', slot: 0, player: 'alienfr', message: 'just noticed i didnt have ANY perks go figure' }, { time: '63:22', guid: '2989140', slot: 1, player: 'mikakyu', message: 'whoopsie' } ]); }); it('preserves semicolons, Unicode, repeated messages and log order across timer resets', () => { const lines = ['120:59 say;abc;2;^2Player;\x15hello; café friends!', '0:01 say;abc;2;Player;hello', '0:01 say;abc;2;Player;hello']; const messages = parseGameChat(lines); expect(messages.map(message => message.time)).toEqual(['120:59', '0:01', '0:01']); expect(messages[0]).toMatchObject({ player: 'Player', message: 'hello; café friends!' }); expect(messages).toHaveLength(3); }); it('ignores malformed, unrelated and empty entries', () => { expect(parseGameChat(['', 'say;123;0;Player;no timestamp', '1:00 say;123;0;Player', '1:00 say;123;bad;Player;hello', '1:00 say;123;0;Player;\x15^7', '1:00 InitGame: say;123;0;Player;hello', '1:00 sayteam;123;0;Player;hello'])).toEqual([]); }); it('separates matches even when elapsed time keeps increasing', () => { const entries = parseGameChatTimeline([ '1200:00 InitGame: \\mapname\\zombie_theater', '1201:00 say;abc;0;Player;first match', '1270:00 ExitLevel: executed', '1270:01 ShutdownGame:', '1280:00 InitGame:', '1290:00 say;abc;0;Player;second match', '1295:00 ExitLevel: executed' ]); expect(entries.map(entry => entry.type === 'boundary' ? entry.event : entry.message.message)) .toEqual(['start', 'first match', 'end', 'start', 'second match', 'end']); expect(entries[0]).toEqual({ type: 'boundary', event: 'start', time: '1200:00' }); }); it('handles untimed boundaries and avoids an extra divider when InitGame resets the clock', () => { const entries = parseGameChatTimeline([ '120:00 say;abc;0;Player;earlier match', 'ExitLevel: executed', 'ShutdownGame:', 'InitGame:', '0:01 say;abc;0;Player;next match', '0:00 InitGame:', '0:02 say;abc;0;Player;another match' ]); expect(entries.filter(entry => entry.type === 'boundary')).toEqual([ { type: 'boundary', event: 'end', time: null }, { type: 'boundary', event: 'start', time: null }, { type: 'boundary', event: 'start', time: '0:00' } ]); }); it('detects missing start events from timer resets, including on non-chat lines', () => { const entries = parseGameChatTimeline([ '5:00 say;abc;0;Player;earlier match', '0:00 J;abc;0;Player', '6:00 say;abc;0;Player;next match', '0:01 say;abc;0;Player;another match' ]); expect(entries.map(entry => entry.type === 'boundary' ? entry.event : entry.message.message)) .toEqual(['earlier match', 'reset', 'next match', 'reset', 'another match']); }); it('does not infer matches from chat content, repeated times or long gaps', () => { const entries = parseGameChatTimeline([ '1:00 say;abc;0;Player;InitGame:', '1:00 say;abc;0;Player;ShutdownGame:', '900:00 say;abc;0;Player;ExitLevel: executed' ]); expect(entries).toHaveLength(3); expect(entries.every(entry => entry.type === 'message')).toBe(true); }); });