// Der Bus entscheidet, was eine frisch verbundene Browser-Source sieht. // Danach richtet sich, ob das Overlay nach einem Neustart vollstaendig ist // oder leer im Bild steht - und ob eine geloeschte Nachricht geloescht bleibt. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createBus, type ChatEvent, type OverlayEvent } from './bus.ts'; function nachricht(over: Partial = {}): ChatEvent { return { kind: 'chat', id: 'm1', userId: 'u1', login: 'kevin', name: 'Kevin', text: 'moin', color: null, emotes: [], gifs: [], first: false, mod: false, sub: false, broadcaster: false, bits: 0, ...over, }; } function schnappschuss(bus: ReturnType) { const s = bus.snapshot(); assert.equal(s.kind, 'state'); return s as Extract; } test('ein neuer Zuhoerer bekommt den bisherigen Chat', () => { const bus = createBus(); bus.publish(nachricht({ id: 'a' })); bus.publish(nachricht({ id: 'b' })); const s = schnappschuss(bus); assert.deepEqual(s.chat.map((m) => m.id), ['a', 'b']); }); test('der Rueckblick waechst nicht unbegrenzt', () => { const bus = createBus(); for (let i = 0; i < 100; i++) bus.publish(nachricht({ id: `m${i}` })); const s = schnappschuss(bus); assert.equal(s.chat.length, 30); assert.equal(s.chat[s.chat.length - 1]?.id, 'm99', 'die neuesten bleiben'); }); test('der zuletzt gemeldete Stream-Zustand steht im Schnappschuss', () => { const bus = createBus(); bus.publish({ kind: 'stream', live: true, title: 'Titel', category: 'Elden Ring', viewers: 12, startedAt: '2026-09-17T18:00:00Z' }); bus.publish({ kind: 'stream', live: true, title: 'Titel', category: 'Elden Ring', viewers: 19, startedAt: '2026-09-17T18:00:00Z' }); assert.equal(schnappschuss(bus).stream.viewers, 19); }); test('eine geloeschte Nachricht verschwindet auch aus dem Rueckblick', () => { // Sonst holt der naechste Neustart der Browser-Source sie zurueck ins Bild - // und zwar genau die eine, die ein Mod entfernt hat. const bus = createBus(); bus.publish(nachricht({ id: 'a' })); bus.publish(nachricht({ id: 'b' })); bus.publish({ kind: 'clear', messageId: 'a' }); assert.deepEqual(schnappschuss(bus).chat.map((m) => m.id), ['b']); }); test('ein gesperrter Chatter verschwindet mit allen Nachrichten', () => { const bus = createBus(); bus.publish(nachricht({ id: 'a', userId: 'u1' })); bus.publish(nachricht({ id: 'b', userId: 'u2' })); bus.publish(nachricht({ id: 'c', userId: 'u1' })); bus.publish({ kind: 'clear', userId: 'u1' }); assert.deepEqual(schnappschuss(bus).chat.map((m) => m.id), ['b']); }); test('ein geleerter Chat leert auch den Rueckblick', () => { const bus = createBus(); bus.publish(nachricht({ id: 'a' })); bus.publish({ kind: 'clear' }); assert.deepEqual(schnappschuss(bus).chat, []); }); test('alte Einblendungen werden nicht endlos aufbewahrt', () => { const bus = createBus(); for (let i = 0; i < 20; i++) { bus.publish({ kind: 'alert', type: 'sub', id: `a${i}`, name: 'x' }); } assert.equal(schnappschuss(bus).alerts.length, 5); }); test('ein Zuhoerer, der wirft, reisst die anderen nicht mit', () => { // Eine Browser-Source, deren Verbindung gerade zusammengebrochen ist, darf // den Chat nicht fuer alle anderen anhalten. const bus = createBus(); const gesehen: string[] = []; bus.subscribe(() => { throw new Error('Verbindung weg'); }); bus.subscribe((ev) => { gesehen.push(ev.kind); }); bus.publish(nachricht()); assert.deepEqual(gesehen, ['chat']); }); test('abgemeldete Zuhoerer bekommen nichts mehr', () => { const bus = createBus(); let n = 0; const ab = bus.subscribe(() => { n++; }); bus.publish(nachricht()); ab(); bus.publish(nachricht()); assert.equal(n, 1); assert.equal(bus.clients(), 0); });