import { test } from 'node:test'; import assert from 'node:assert/strict'; import type { Room, MatrixEvent } from 'matrix-js-sdk'; import { ReceiptType } from 'matrix-js-sdk/lib/@types/read_receipts'; import { computeUpdatedPositions, getReceiptUserIds } from './useRoomReadPositions'; // Fake, renderable (no reaction/edit relation) event. const fakeEvent = (id: string) => ({ getId: () => id, getRelation: () => null, }) as unknown as MatrixEvent; // Minimal fake room: a fixed live timeline plus a mutable per-user read-up-to map, // which the test mutates between calls to simulate new receipts arriving. const makeFakeRoom = (eventIds: string[]) => { const readUpTo = new Map(); const events = eventIds.map(fakeEvent); const room = { getLiveTimeline: () => ({ getEvents: () => events }), getEventReadUpTo: (userId: string) => readUpTo.get(userId) ?? null, } as unknown as Room; return { room, readUpTo }; }; test('getReceiptUserIds collects every userId across events/types in the content', () => { const content = { $a: { [ReceiptType.Read]: { '@alice:hs': { ts: 1 }, '@bob:hs': { ts: 2 } }, }, $b: { [ReceiptType.ReadPrivate]: { '@carol:hs': { ts: 3 } }, }, }; const ids = getReceiptUserIds(content).sort(); assert.deepEqual(ids, ['@alice:hs', '@bob:hs', '@carol:hs']); }); test('getReceiptUserIds returns empty for an empty receipt content', () => { assert.deepEqual(getReceiptUserIds({}), []); }); test('computeUpdatedPositions returns the SAME map reference when no named user changed position', () => { const { room, readUpTo } = makeFakeRoom(['$a', '$b']); readUpTo.set('@alice:hs', '$a'); const prev = computeUpdatedPositions(room, '@me:hs', new Map(), ['@alice:hs']); assert.deepEqual(prev.get('$a'), ['@alice:hs']); // Alice's read-up-to did not move; recompute should be a no-op reference-wise. const next = computeUpdatedPositions(room, '@me:hs', prev, ['@alice:hs']); assert.equal(next, prev); }); test('computeUpdatedPositions moves only the named user, leaving other entries untouched by reference', () => { const { room, readUpTo } = makeFakeRoom(['$a', '$b']); readUpTo.set('@alice:hs', '$a'); readUpTo.set('@bob:hs', '$a'); const prev = computeUpdatedPositions(room, '@me:hs', new Map(), ['@alice:hs', '@bob:hs']); const bArrayBefore = prev.get('$a'); // Only alice moves to $b. readUpTo.set('@alice:hs', '$b'); const next = computeUpdatedPositions(room, '@me:hs', prev, ['@alice:hs']); assert.notEqual(next, prev); assert.deepEqual(next.get('$a'), ['@bob:hs']); assert.deepEqual(next.get('$b'), ['@alice:hs']); // Untouched arrays for other targets should keep their old reference where possible. assert.notEqual(next.get('$a'), bArrayBefore); // this one WAS filtered, expected to change }); test('computeUpdatedPositions ignores changedUserIds not present after filtering myUserId', () => { const { room } = makeFakeRoom(['$a']); const prev = new Map([['$a', ['@bob:hs']]]); const next = computeUpdatedPositions(room, '@me:hs', prev, ['@me:hs']); assert.equal(next, prev); }); test('computeUpdatedPositions removes a user with no read-up-to event from the map', () => { const { room, readUpTo } = makeFakeRoom(['$a']); readUpTo.set('@alice:hs', '$a'); const prev = computeUpdatedPositions(room, '@me:hs', new Map(), ['@alice:hs']); assert.deepEqual(prev.get('$a'), ['@alice:hs']); readUpTo.delete('@alice:hs'); const next = computeUpdatedPositions(room, '@me:hs', prev, ['@alice:hs']); assert.equal(next.has('$a'), false); });