atagen

malice

public
Star0
Watch0
850d5a52e9497c4bff4a5ca7532b304405795f36
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/test/notification-model.test.tsTypescript122 lines5.3 KB
1
import assert from 'node:assert/strict';
2
import { test } from 'node:test';
3
import {
4
  arrivalIn,
5
  badgeTotal,
6
  isRoomWatched,
7
  notifyingCount,
8
  planNotification,
9
  sourceFromTimeline,
10
  type NotificationSource
11
} from '../src/lib/notification-model.ts';
12
import type { RoomSummary, TimelineItem } from '../src/lib/types.ts';
13
14
const NOW = 1_000_000_000;
15
16
function room(extra: Partial<RoomSummary> = {}): RoomSummary {
17
  return {
18
    room_id: '!r:x',
19
    name: 'General',
20
    membership: 'Joined',
21
    unread_messages: 0,
22
    unread_mentions: 0,
23
    unread_notifications: 0,
24
    space: false,
25
    direct: false,
26
    notification_mode: null,
27
    latest_event_timestamp_ms: NOW - 1000,
28
    avatar: 'mxc://x/room',
29
    ...extra
30
  } as RoomSummary;
31
}
32
33
function source(extra: Partial<NotificationSource> = {}): NotificationSource {
34
  return { senderId: '@bob:x', senderName: 'Bob', senderAvatar: 'mxc://x/bob', kind: 'Text', body: 'hi', undecryptable: false, highlight: false, ...extra };
35
}
36
37
test('counts only notifying unread per mode', () => {
38
  assert.equal(notifyingCount(room({ unread_notifications: 0, unread_messages: 9 })), 9);
39
  assert.equal(notifyingCount(room({ direct: true, unread_messages: 2, unread_mentions: 1 })), 3);
40
  assert.equal(notifyingCount(room({ notification_mode: 'Mute', unread_mentions: 4 })), 0);
41
  assert.equal(notifyingCount(room({ notification_mode: 'MentionsAndKeywordsOnly', unread_notifications: 5, unread_mentions: 1 })), 1);
42
  assert.equal(notifyingCount(room({ space: true, unread_mentions: 1 })), 0);
43
  assert.equal(notifyingCount(room({ membership: 'Invited', unread_mentions: 1 })), 0);
44
});
45
46
test('badge sums notifying counts across rooms', () => {
47
  const rooms = [room({ unread_messages: 2 }), room({ direct: true, unread_messages: 1 }), room({ notification_mode: 'Mute', unread_messages: 9 })];
48
  assert.equal(badgeTotal(rooms), 3);
49
  assert.equal(badgeTotal([]), 0);
50
});
51
52
test('arrival needs a known previous room and a count increase', () => {
53
  const before = room({ unread_messages: 1 });
54
  assert.equal(arrivalIn(undefined, before, NOW), null);
55
  assert.equal(arrivalIn(before, before, NOW), null);
56
  assert.equal(arrivalIn(before, room({ unread_messages: 0 }), NOW), null);
57
  assert.deepEqual(arrivalIn(before, room({ unread_messages: 2 }), NOW), { mention: false });
58
  assert.deepEqual(arrivalIn(before, room({ unread_messages: 2, unread_mentions: 1 }), NOW), { mention: true });
59
});
60
61
test('arrival ignores stale backfill', () => {
62
  const next = room({ unread_messages: 1, latest_event_timestamp_ms: NOW - 10 * 60 * 1000 });
63
  assert.equal(arrivalIn(room(), next, NOW), null);
64
});
65
66
test('only the focused open room is watched', () => {
67
  assert.equal(isRoomWatched('!a', '!a', true, true), true);
68
  assert.equal(isRoomWatched('!a', '!a', false, true), false);
69
  assert.equal(isRoomWatched('!a', '!a', true, false), false);
70
  assert.equal(isRoomWatched('!a', '!b', true, true), false);
71
});
72
73
test('group notification prefixes the sender and tags per room', () => {
74
  const plan = planNotification(room(), source(), { mention: false });
75
  assert.equal(plan.title, 'General');
76
  assert.equal(plan.body, 'Bob: hi');
77
  assert.equal(plan.tag, 'room:!r:x');
78
  assert.equal(plan.renotify, false);
79
  assert.equal(plan.iconMxc, 'mxc://x/room');
80
});
81
82
test('direct notification titles the sender and uses their avatar', () => {
83
  const plan = planNotification(room({ direct: true, name: 'Bob' }), source(), { mention: false });
84
  assert.equal(plan.title, 'Bob');
85
  assert.equal(plan.body, 'hi');
86
  assert.equal(plan.iconMxc, 'mxc://x/bob');
87
});
88
89
test('mentions and keyword highlights renotify', () => {
90
  assert.equal(planNotification(room(), source(), { mention: true }).renotify, true);
91
  assert.equal(planNotification(room(), source({ highlight: true }), { mention: false }).renotify, true);
92
});
93
94
test('undecryptable and missing content get a generic body', () => {
95
  assert.equal(planNotification(room(), source({ undecryptable: true, body: null }), { mention: false }).body, 'Bob: New messages');
96
  assert.equal(planNotification(room({ direct: true }), null, { mention: false }).body, 'New message');
97
});
98
99
test('media and emotes get readable previews', () => {
100
  assert.equal(planNotification(room({ direct: true }), source({ kind: 'Image', body: 'cat.png' }), { mention: false }).body, 'Sent an image');
101
  assert.equal(planNotification(room(), source({ kind: 'Emote', body: 'waves' }), { mention: false }).body, 'Bob waves');
102
});
103
104
test('falls back to the user id local part when no display name is known', () => {
105
  assert.equal(planNotification(room(), source({ senderName: null }), { mention: false }).body, 'bob: hi');
106
});
107
108
test('timeline source skips own newest and non-message items', () => {
109
  const message = (own: boolean, body: string): TimelineItem =>
110
    ({
111
      kind: 'event',
112
      event: {
113
        own,
114
        sender: { user_id: '@bob:x', display_name: 'Bob', avatar: null, name_ambiguous: false },
115
        content: { kind: 'Message', message: { kind: 'Text', body } }
116
      }
117
    }) as TimelineItem;
118
  const membership = { kind: 'event', event: { own: false, sender: { user_id: '@c:x' }, content: { kind: 'RoomCreated' } } } as TimelineItem;
119
  assert.equal(sourceFromTimeline([message(false, 'a'), membership])?.body, 'a');
120
  assert.equal(sourceFromTimeline([message(false, 'a'), message(true, 'b')]), null);
121
  assert.equal(sourceFromTimeline(undefined), null);
122
});