Files
cinny/src/app/state/room/markedUnread.test.ts
T

90 lines
3.5 KiB
TypeScript
Raw Normal View History

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { MatrixEvent } from 'matrix-js-sdk';
import { myMainReceiptPresent, receiptIsMine, setMarkedUnread } from './markedUnread';
// MSC2867 mark-as-unread: reading a room (our own receipt) clears the flag, so
// `receiptIsMine` must detect only OUR receipt and ignore others'. And a write
// must land on BOTH the stable `m.marked_unread` and the unstable
// `com.famedly.marked_unread` key so it round-trips across servers/clients.
const ME = '@me:server';
const OTHER = '@friend:server';
const receiptEvent = (content: object): MatrixEvent =>
({ getContent: () => content }) as MatrixEvent;
test('receiptIsMine: true when the receipt content carries our user id', () => {
const event = receiptEvent({
$abc: { 'm.read': { [ME]: { ts: 1 } } },
});
assert.equal(receiptIsMine(event, ME), true);
});
test('receiptIsMine: false when only another user has a receipt', () => {
const event = receiptEvent({
$abc: { 'm.read': { [OTHER]: { ts: 1 } } },
});
assert.equal(receiptIsMine(event, ME), false);
});
test('receiptIsMine: tolerates empty / malformed content', () => {
assert.equal(receiptIsMine(receiptEvent({}), ME), false);
assert.equal(receiptIsMine(receiptEvent({ $x: {} }), ME), false);
});
// myMainReceiptPresent gates the auto-clear to main-timeline reads, so reading a
// single thread does not wipe the whole-room marked-unread flag.
test('myMainReceiptPresent: true for an unthreaded receipt (no thread_id)', () => {
const event = receiptEvent({ $abc: { 'm.read': { [ME]: { ts: 1 } } } });
assert.equal(myMainReceiptPresent(event, ME), true);
});
test('myMainReceiptPresent: true for a thread_id "main" receipt', () => {
const event = receiptEvent({ $abc: { 'm.read': { [ME]: { ts: 1, thread_id: 'main' } } } });
assert.equal(myMainReceiptPresent(event, ME), true);
});
test('myMainReceiptPresent: false for a thread-scoped receipt', () => {
2026-07-11 13:52:36 -04:00
const event = receiptEvent({
$abc: { 'm.read': { [ME]: { ts: 1, thread_id: '$root:server' } } },
});
assert.equal(myMainReceiptPresent(event, ME), false);
});
test('myMainReceiptPresent: false when only another user has a main receipt', () => {
const event = receiptEvent({ $abc: { 'm.read': { [OTHER]: { ts: 1 } } } });
assert.equal(myMainReceiptPresent(event, ME), false);
});
test('setMarkedUnread writes both the stable and unstable keys with the flag', async () => {
const calls: Array<{ type: string; content: unknown }> = [];
const mx = {
setRoomAccountData: (_roomId: string, type: string, content: unknown) => {
calls.push({ type, content });
return Promise.resolve();
},
} as any;
await setMarkedUnread(mx, '!room:server', true);
const types = calls.map((c) => c.type).sort();
assert.deepEqual(types, ['com.famedly.marked_unread', 'm.marked_unread']);
assert.ok(calls.every((c) => (c.content as { unread: boolean }).unread === true));
});
test('setMarkedUnread(false) clears both keys and does not reject if the unstable write fails', async () => {
const seen: string[] = [];
const mx = {
setRoomAccountData: (_roomId: string, type: string) => {
seen.push(type);
// Simulate an older server rejecting the unstable key — must not reject.
if (type === 'com.famedly.marked_unread') return Promise.reject(new Error('unknown type'));
return Promise.resolve();
},
} as any;
await assert.doesNotReject(() => setMarkedUnread(mx, '!room:server', false));
assert.ok(seen.includes('m.marked_unread'));
});