134 lines
4.8 KiB
TypeScript
134 lines
4.8 KiB
TypeScript
|
|
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);
|
||
|
|
});
|