import { test } from 'node:test'; import assert from 'node:assert/strict'; import { EventStatus, MatrixEvent, RelationType } from 'matrix-js-sdk'; import { getThreadSummary, isPendingThreadReply } from './threadSummaryData'; // getThreadSummary reads either the live Thread (preferred) or the // server-aggregated `m.thread` bundle. We stub only the members it touches and // cast through `unknown` to MatrixEvent, mirroring the light mocking used in // the state tests. type ThreadStub = { length: number; lastReplyTs?: number }; type BundleStub = { count: number; latestTs?: number }; const makeRootEvent = (opts: { thread?: ThreadStub; bundle?: BundleStub }): MatrixEvent => { const thread = opts.thread ? { length: opts.thread.length, lastReply: () => opts.thread?.lastReplyTs === undefined ? null : ({ getTs: () => opts.thread?.lastReplyTs } as unknown as MatrixEvent), } : undefined; return { getThread: () => thread, getServerAggregatedRelation: (relType: string) => { if (relType !== RelationType.Thread || !opts.bundle) return undefined; return { count: opts.bundle.count, latest_event: opts.bundle.latestTs === undefined ? undefined : { origin_server_ts: opts.bundle.latestTs }, }; }, } as unknown as MatrixEvent; }; // --------------------------------------------------------------------------- // getThreadSummary // --------------------------------------------------------------------------- test('prefers the live thread: count from length, latestTs from lastReply', () => { const rootEvent = makeRootEvent({ thread: { length: 3, lastReplyTs: 1700 }, bundle: { count: 99, latestTs: 1 }, }); assert.deepEqual(getThreadSummary(rootEvent), { count: 3, latestTs: 1700 }); }); test('live thread with no replies yields undefined latestTs', () => { const rootEvent = makeRootEvent({ thread: { length: 0 } }); assert.deepEqual(getThreadSummary(rootEvent), { count: 0, latestTs: undefined }); }); test('falls back to the server bundle when no live thread', () => { const rootEvent = makeRootEvent({ bundle: { count: 5, latestTs: 1234 } }); assert.deepEqual(getThreadSummary(rootEvent), { count: 5, latestTs: 1234 }); }); test('bundle without latest_event yields undefined latestTs', () => { const rootEvent = makeRootEvent({ bundle: { count: 2 } }); assert.deepEqual(getThreadSummary(rootEvent), { count: 2, latestTs: undefined }); }); test('returns undefined when there is neither a thread nor a bundle', () => { const rootEvent = makeRootEvent({}); assert.equal(getThreadSummary(rootEvent), undefined); }); // --------------------------------------------------------------------------- // isPendingThreadReply // --------------------------------------------------------------------------- const ROOT = '$root:server'; const makeReply = (opts: { status: EventStatus | null; threadRootId?: string; relation?: { rel_type?: string; event_id?: string } | null; }): MatrixEvent => ({ status: opts.status, threadRootId: opts.threadRootId, getRelation: () => opts.relation ?? null, }) as unknown as MatrixEvent; test('SENDING with matching threadRootId is pending', () => { const event = makeReply({ status: EventStatus.SENDING, threadRootId: ROOT }); assert.equal(isPendingThreadReply(event, ROOT), true); }); test('NOT_SENT with matching threadRootId is pending', () => { const event = makeReply({ status: EventStatus.NOT_SENT, threadRootId: ROOT }); assert.equal(isPendingThreadReply(event, ROOT), true); }); test('SENDING resolved via the m.thread relation content is pending', () => { const event = makeReply({ status: EventStatus.SENDING, relation: { rel_type: RelationType.Thread, event_id: ROOT }, }); assert.equal(isPendingThreadReply(event, ROOT), true); }); test('SENT (confirmed) event is not pending', () => { const event = makeReply({ status: EventStatus.SENT, threadRootId: ROOT }); assert.equal(isPendingThreadReply(event, ROOT), false); }); test('null status is not pending', () => { const event = makeReply({ status: null, threadRootId: ROOT }); assert.equal(isPendingThreadReply(event, ROOT), false); }); test('SENDING but for a different thread is not pending', () => { const event = makeReply({ status: EventStatus.SENDING, threadRootId: '$other:server' }); assert.equal(isPendingThreadReply(event, ROOT), false); }); test('SENDING with a non-thread relation is not pending', () => { const event = makeReply({ status: EventStatus.SENDING, relation: { rel_type: RelationType.Reference, event_id: ROOT }, }); assert.equal(isPendingThreadReply(event, ROOT), false); }); test('SENDING with no relation and no threadRootId is not pending', () => { const event = makeReply({ status: EventStatus.SENDING }); assert.equal(isPendingThreadReply(event, ROOT), false); });