polfan-server-js-client 0.2.104 → 0.2.106
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.idea/workspace.xml +73 -52
- package/build/index.cjs.js +394 -95
- package/build/index.cjs.js.map +1 -1
- package/build/index.umd.js +1 -1
- package/build/index.umd.js.map +1 -1
- package/build/types/IndexedObjectCollection.d.ts +8 -0
- package/build/types/state-tracker/FollowedTopicsManager.d.ts +18 -0
- package/build/types/state-tracker/RoomMessagesHistory.d.ts +12 -0
- package/build/types/state-tracker/TopicHistoryWindow.d.ts +2 -2
- package/package.json +1 -1
- package/src/IndexedObjectCollection.ts +31 -0
- package/src/state-tracker/EmoticonsManager.ts +72 -68
- package/src/state-tracker/FollowedTopicsManager.ts +54 -6
- package/src/state-tracker/MessagesManager.ts +21 -2
- package/src/state-tracker/PermissionsManager.ts +5 -1
- package/src/state-tracker/RelationshipsManager.ts +5 -5
- package/src/state-tracker/RoomMessagesHistory.ts +35 -1
- package/src/state-tracker/RoomsManager.ts +21 -4
- package/src/state-tracker/SpacesManager.ts +340 -313
- package/src/state-tracker/TopicHistoryWindow.ts +5 -5
- package/src/state-tracker/UsersManager.ts +3 -1
- package/tests/collections.test.ts +92 -0
- package/tests/state-reconnect.test.ts +257 -0
|
@@ -111,8 +111,8 @@ export abstract class TraversableRemoteCollection<
|
|
|
111
111
|
|
|
112
112
|
public abstract createMirror(): TraversableRemoteCollection<ItemT, EventMapT>;
|
|
113
113
|
|
|
114
|
-
public async resetToLatest(): Promise<void> {
|
|
115
|
-
if (this.internalState.ongoing || this.internalState.current === WindowState.LATEST) {
|
|
114
|
+
public async resetToLatest(force: boolean = false): Promise<void> {
|
|
115
|
+
if (this.internalState.ongoing || (! force && this.internalState.current === WindowState.LATEST)) {
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -203,7 +203,7 @@ export abstract class TraversableRemoteCollection<
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
public async jumpTo(id: string): Promise<void> {
|
|
206
|
-
if (this.internalState.ongoing || this._items.has(id)) {
|
|
206
|
+
if (this.internalState.ongoing || (this.state !== WindowState.LIVE && this._items.has(id))) {
|
|
207
207
|
return;
|
|
208
208
|
}
|
|
209
209
|
|
|
@@ -340,11 +340,11 @@ export class TopicHistoryWindow extends TraversableRemoteCollection<
|
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
public async resetToLatest(): Promise<void> {
|
|
343
|
+
public async resetToLatest(force: boolean = false): Promise<void> {
|
|
344
344
|
if (this.internalState.traverseLock) {
|
|
345
345
|
return;
|
|
346
346
|
}
|
|
347
|
-
return super.resetToLatest();
|
|
347
|
+
return super.resetToLatest(force);
|
|
348
348
|
}
|
|
349
349
|
|
|
350
350
|
public async fetchNext(): Promise<void> {
|
|
@@ -33,7 +33,9 @@ export class UsersManager {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
private handleSession(session: Session): void {
|
|
36
|
-
|
|
36
|
+
// Keep the "seen users" cache across reconnects so bound user lists do
|
|
37
|
+
// not blank out; just ensure our own user is present/updated. Stale
|
|
38
|
+
// entries are refreshed as member/message collections refetch.
|
|
37
39
|
this.handleUsers([session.user]);
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -759,4 +759,96 @@ describe('ObservableIndexedObjectCollection', () => {
|
|
|
759
759
|
expect(mirror.get('2')).toBeDefined();
|
|
760
760
|
expect(mirror.length).toBe(2);
|
|
761
761
|
});
|
|
762
|
+
|
|
763
|
+
test('reconcile - upserts new/changed items and deletes absent ones', () => {
|
|
764
|
+
const collection = new ObservableIndexedObjectCollection<TestItem>('id', [
|
|
765
|
+
createItem('1', 'first'),
|
|
766
|
+
createItem('2', 'second'),
|
|
767
|
+
createItem('3', 'third'),
|
|
768
|
+
]);
|
|
769
|
+
|
|
770
|
+
// Keep 1 (changed), keep 3 (unchanged), drop 2, add 4.
|
|
771
|
+
collection.reconcile(
|
|
772
|
+
createItem('1', 'first-renamed'),
|
|
773
|
+
createItem('3', 'third'),
|
|
774
|
+
createItem('4', 'fourth'),
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
expect(collection.length).toBe(3);
|
|
778
|
+
expect(collection.get('1')?.name).toBe('first-renamed');
|
|
779
|
+
expect(collection.get('2')).toBeUndefined();
|
|
780
|
+
expect(collection.get('3')?.name).toBe('third');
|
|
781
|
+
expect(collection.get('4')?.name).toBe('fourth');
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
test('reconcile - emits a single change event with set and deleted ids', () => {
|
|
785
|
+
const collection = new ObservableIndexedObjectCollection<TestItem>('id', [
|
|
786
|
+
createItem('1', 'first'),
|
|
787
|
+
createItem('2', 'second'),
|
|
788
|
+
]);
|
|
789
|
+
const handler = jest.fn();
|
|
790
|
+
collection.on('change', handler);
|
|
791
|
+
|
|
792
|
+
collection.reconcile(createItem('1', 'first'), createItem('3', 'third'));
|
|
793
|
+
|
|
794
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
795
|
+
expect(handler).toHaveBeenCalledWith({ setItems: ['1', '3'], deletedItems: ['2'] });
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
test('reconcile - never blanks: existing item stays present the whole time', () => {
|
|
799
|
+
const collection = new ObservableIndexedObjectCollection<TestItem>('id', [
|
|
800
|
+
createItem('1', 'first'),
|
|
801
|
+
createItem('2', 'second'),
|
|
802
|
+
]);
|
|
803
|
+
|
|
804
|
+
// A change listener that reads the collection while it is being
|
|
805
|
+
// reconciled must never observe an empty/partial intermediate state.
|
|
806
|
+
const seenLengths: number[] = [];
|
|
807
|
+
collection.on('change', () => seenLengths.push(collection.length));
|
|
808
|
+
|
|
809
|
+
collection.reconcile(createItem('1', 'first'), createItem('3', 'third'));
|
|
810
|
+
|
|
811
|
+
// Only the final state (2 items) is ever visible - no 0-length frame.
|
|
812
|
+
expect(seenLengths).toEqual([2]);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
test('reconcile - does not emit when nothing changes structurally on empty input', () => {
|
|
816
|
+
const collection = new ObservableIndexedObjectCollection<TestItem>('id');
|
|
817
|
+
const handler = jest.fn();
|
|
818
|
+
collection.on('change', handler);
|
|
819
|
+
|
|
820
|
+
collection.reconcile();
|
|
821
|
+
|
|
822
|
+
expect(handler).not.toHaveBeenCalled();
|
|
823
|
+
expect(collection.length).toBe(0);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
test('reconcile - reconciling to empty clears the collection', () => {
|
|
827
|
+
const collection = new ObservableIndexedObjectCollection<TestItem>('id', [
|
|
828
|
+
createItem('1', 'first'),
|
|
829
|
+
]);
|
|
830
|
+
const handler = jest.fn();
|
|
831
|
+
collection.on('change', handler);
|
|
832
|
+
|
|
833
|
+
collection.reconcile();
|
|
834
|
+
|
|
835
|
+
expect(collection.length).toBe(0);
|
|
836
|
+
expect(handler).toHaveBeenCalledWith({ setItems: [], deletedItems: ['1'] });
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
test('reconcile - preserves collection identity (mirror sees updates)', () => {
|
|
840
|
+
const collection = new ObservableIndexedObjectCollection<TestItem>('id', [
|
|
841
|
+
createItem('1', 'first'),
|
|
842
|
+
createItem('2', 'second'),
|
|
843
|
+
]);
|
|
844
|
+
const mirror = collection.createMirror();
|
|
845
|
+
|
|
846
|
+
collection.reconcile(createItem('2', 'second'), createItem('3', 'third'));
|
|
847
|
+
|
|
848
|
+
// The mirror shares the same underlying items, so it reflects the
|
|
849
|
+
// reconcile without being re-created.
|
|
850
|
+
expect(mirror.get('1')).toBeUndefined();
|
|
851
|
+
expect(mirror.get('3')?.name).toBe('third');
|
|
852
|
+
expect(mirror.length).toBe(2);
|
|
853
|
+
});
|
|
762
854
|
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {EventTarget} from "../src/EventTarget";
|
|
2
|
+
import {RoomsManager} from "../src/state-tracker/RoomsManager";
|
|
3
|
+
import {SpacesManager} from "../src/state-tracker/SpacesManager";
|
|
4
|
+
import {WindowState} from "../src/state-tracker/TopicHistoryWindow";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* These tests exercise the reconnect (second `Session`) reconciliation path of
|
|
8
|
+
* the state tracker: collections must keep their identity, surviving rooms and
|
|
9
|
+
* spaces must not be wiped and rebuilt, ephemeral message history must be
|
|
10
|
+
* preserved, and only the entities actually removed on the server may be
|
|
11
|
+
* dropped. See ChatStateTracker managers `handleSession`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
type Responder = (data: any) => any;
|
|
15
|
+
|
|
16
|
+
class FakeClient extends EventTarget {
|
|
17
|
+
public readonly sent: { type: string; data: any }[] = [];
|
|
18
|
+
private readonly responders: Record<string, Responder> = {};
|
|
19
|
+
|
|
20
|
+
public respondTo(type: string, responder: Responder): void {
|
|
21
|
+
this.responders[type] = responder;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public async send(type: string, data: any): Promise<{ data: any; error: any }> {
|
|
25
|
+
this.sent.push({ type, data });
|
|
26
|
+
const responder = this.responders[type];
|
|
27
|
+
return { data: responder ? responder(data) : undefined, error: null };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public countSent(type: string): number {
|
|
31
|
+
return this.sent.filter(entry => entry.type === type).length;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const flush = async (): Promise<void> => {
|
|
36
|
+
// Drain microtasks scheduled by the fire-and-forget resync chain.
|
|
37
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
38
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const createRoom = (id: string, overrides: any = {}): any => ({
|
|
42
|
+
id,
|
|
43
|
+
spaceId: null,
|
|
44
|
+
name: id,
|
|
45
|
+
description: '',
|
|
46
|
+
type: 'Text',
|
|
47
|
+
defaultTopic: { id: `topic-${id}`, messageCount: 0, lastMessage: null },
|
|
48
|
+
recipients: null,
|
|
49
|
+
flags: 0,
|
|
50
|
+
stream: null,
|
|
51
|
+
history: { mode: 'Full' },
|
|
52
|
+
...overrides,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const createSpace = (id: string, roles: any[] = []): any => ({
|
|
56
|
+
id,
|
|
57
|
+
name: id,
|
|
58
|
+
roles,
|
|
59
|
+
defaultRooms: [],
|
|
60
|
+
systemRoom: null,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const emitSession = (client: FakeClient, rooms: any[], spaces: any[] = []): void => {
|
|
64
|
+
client.emit('Session', {
|
|
65
|
+
serverVersion: '1.0.0',
|
|
66
|
+
protoVersion: '1.0.0',
|
|
67
|
+
user: { id: 'me' },
|
|
68
|
+
state: { rooms, spaces },
|
|
69
|
+
} as any);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const createTracker = () => {
|
|
73
|
+
const client = new FakeClient();
|
|
74
|
+
const tracker: any = { client, getMe: async () => ({ id: 'me' }) };
|
|
75
|
+
tracker.rooms = new RoomsManager(tracker);
|
|
76
|
+
tracker.spaces = new SpacesManager(tracker);
|
|
77
|
+
return { client, tracker };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
describe('reconnect - RoomsManager', () => {
|
|
81
|
+
test('preserves the rooms list identity and reconciles membership', async () => {
|
|
82
|
+
const { client, tracker } = createTracker();
|
|
83
|
+
|
|
84
|
+
emitSession(client, [createRoom('A'), createRoom('B')]);
|
|
85
|
+
const list = await tracker.rooms.get();
|
|
86
|
+
|
|
87
|
+
expect(list.items.map((r: any) => r.id).sort()).toEqual(['A', 'B']);
|
|
88
|
+
|
|
89
|
+
// Reconnect: B is gone, C joined, A survived (renamed).
|
|
90
|
+
emitSession(client, [createRoom('A', { name: 'A-renamed' }), createRoom('C')]);
|
|
91
|
+
const listAfter = await tracker.rooms.get();
|
|
92
|
+
|
|
93
|
+
expect(listAfter).toBe(list); // same object -> bindings intact
|
|
94
|
+
expect(listAfter.items.map((r: any) => r.id).sort()).toEqual(['A', 'C']);
|
|
95
|
+
expect(listAfter.get('A').name).toBe('A-renamed');
|
|
96
|
+
expect(listAfter.get('B')).toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('keeps a member collection object across reconnect and refetches it', async () => {
|
|
100
|
+
const { client, tracker } = createTracker();
|
|
101
|
+
client.respondTo('GetRoomMembers', (data: any) => ({
|
|
102
|
+
id: data.id,
|
|
103
|
+
members: [
|
|
104
|
+
{ user: { id: 'me' }, spaceMember: null, roles: null, customColor: null, customNick: null, extras: '' },
|
|
105
|
+
{ user: { id: 'u1' }, spaceMember: null, roles: null, customColor: null, customNick: null, extras: '' },
|
|
106
|
+
],
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
emitSession(client, [createRoom('A')]);
|
|
110
|
+
const membersBefore = await tracker.rooms.getMembers('A');
|
|
111
|
+
expect(membersBefore.items.map((m: any) => m.user.id).sort()).toEqual(['me', 'u1']);
|
|
112
|
+
expect(client.countSent('GetRoomMembers')).toBe(1);
|
|
113
|
+
|
|
114
|
+
// After reconnect u1 has left; the refetch must reconcile in place.
|
|
115
|
+
client.respondTo('GetRoomMembers', (data: any) => ({
|
|
116
|
+
id: data.id,
|
|
117
|
+
members: [
|
|
118
|
+
{ user: { id: 'me' }, spaceMember: null, roles: null, customColor: null, customNick: null, extras: '' },
|
|
119
|
+
],
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
emitSession(client, [createRoom('A')]);
|
|
123
|
+
const membersAfter = await tracker.rooms.getMembers('A');
|
|
124
|
+
|
|
125
|
+
expect(membersAfter).toBe(membersBefore); // same object -> no blank
|
|
126
|
+
expect(client.countSent('GetRoomMembers')).toBe(2); // guard was dropped -> refetched
|
|
127
|
+
expect(membersAfter.items.map((m: any) => m.user.id)).toEqual(['me']);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('reconnect - MessagesManager', () => {
|
|
132
|
+
const emitNewMessage = (client: FakeClient, roomId: string, topicId: string, id: string): void => {
|
|
133
|
+
client.emit('NewMessage', {
|
|
134
|
+
message: {
|
|
135
|
+
id,
|
|
136
|
+
type: 'Text',
|
|
137
|
+
content: '',
|
|
138
|
+
location: { roomId, topicId },
|
|
139
|
+
author: { user: { id: 'other' } },
|
|
140
|
+
},
|
|
141
|
+
} as any);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
test('preserves ephemeral live history across reconnect (no refetch, no loss)', async () => {
|
|
145
|
+
const { client, tracker } = createTracker();
|
|
146
|
+
|
|
147
|
+
emitSession(client, [createRoom('E', { history: { mode: 'Ephemeral' } })]);
|
|
148
|
+
|
|
149
|
+
const history = await tracker.rooms.messages.getRoomHistory('E');
|
|
150
|
+
const window = await history.getMessagesWindow('topic-E');
|
|
151
|
+
expect(window.state).toBe(WindowState.LIVE);
|
|
152
|
+
|
|
153
|
+
emitNewMessage(client, 'E', 'topic-E', 'm1');
|
|
154
|
+
emitNewMessage(client, 'E', 'topic-E', 'm2');
|
|
155
|
+
expect(window.items.map((m: any) => m.id)).toEqual(['m1', 'm2']);
|
|
156
|
+
|
|
157
|
+
client.sent.length = 0;
|
|
158
|
+
|
|
159
|
+
// Reconnect - the whole point: ephemeral context must survive.
|
|
160
|
+
emitSession(client, [createRoom('E', { history: { mode: 'Ephemeral' } })]);
|
|
161
|
+
await flush();
|
|
162
|
+
|
|
163
|
+
const historyAfter = await tracker.rooms.messages.getRoomHistory('E');
|
|
164
|
+
const windowAfter = await historyAfter.getMessagesWindow('topic-E');
|
|
165
|
+
|
|
166
|
+
expect(historyAfter).toBe(history); // history object preserved
|
|
167
|
+
expect(windowAfter).toBe(window); // window object preserved
|
|
168
|
+
expect(windowAfter.items.map((m: any) => m.id)).toEqual(['m1', 'm2']);
|
|
169
|
+
expect(client.countSent('GetMessages')).toBe(0); // never refetched
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('refreshes a LATEST window but leaves an untouched LIVE window alone', async () => {
|
|
173
|
+
const { client, tracker } = createTracker();
|
|
174
|
+
client.respondTo('GetMessages', () => ({ messages: [{ id: 'x1' }, { id: 'x2' }] }));
|
|
175
|
+
|
|
176
|
+
// R was opened (pulled to LATEST); L was only ever created (LIVE).
|
|
177
|
+
emitSession(client, [createRoom('R'), createRoom('L')]);
|
|
178
|
+
|
|
179
|
+
const rHistory = await tracker.rooms.messages.getRoomHistory('R');
|
|
180
|
+
const rWindow = await rHistory.getMessagesWindow('topic-R');
|
|
181
|
+
await rWindow.resetToLatest();
|
|
182
|
+
expect(rWindow.state).toBe(WindowState.LATEST);
|
|
183
|
+
|
|
184
|
+
const lHistory = await tracker.rooms.messages.getRoomHistory('L');
|
|
185
|
+
const lWindow = await lHistory.getMessagesWindow('topic-L');
|
|
186
|
+
expect(lWindow.state).toBe(WindowState.LIVE);
|
|
187
|
+
|
|
188
|
+
client.sent.length = 0;
|
|
189
|
+
|
|
190
|
+
emitSession(client, [createRoom('R'), createRoom('L')]);
|
|
191
|
+
await flush();
|
|
192
|
+
|
|
193
|
+
// LATEST window was refreshed with exactly one request; identity kept.
|
|
194
|
+
expect((await tracker.rooms.messages.getRoomHistory('R'))).toBe(rHistory);
|
|
195
|
+
expect(client.sent.filter(e => e.type === 'GetMessages'
|
|
196
|
+
&& e.data.location.roomId === 'R').length).toBe(1);
|
|
197
|
+
// LIVE window (never pulled by the app) was left untouched.
|
|
198
|
+
expect(client.sent.filter(e => e.type === 'GetMessages'
|
|
199
|
+
&& e.data.location.roomId === 'L').length).toBe(0);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('reconnect - SpacesManager', () => {
|
|
204
|
+
test('reconciles roles in place and drops only removed spaces', async () => {
|
|
205
|
+
const { client, tracker } = createTracker();
|
|
206
|
+
|
|
207
|
+
const s1Roles = [
|
|
208
|
+
{ id: 'r1', name: 'everyone', priority: 0, color: '#fff' },
|
|
209
|
+
{ id: 'r2', name: 'mod', priority: 1, color: '#fff' },
|
|
210
|
+
];
|
|
211
|
+
emitSession(client, [], [createSpace('S1', s1Roles), createSpace('S2')]);
|
|
212
|
+
|
|
213
|
+
const spaces = await tracker.spaces.get();
|
|
214
|
+
const rolesS1 = await tracker.spaces.getRoles('S1');
|
|
215
|
+
expect(spaces.items.map((s: any) => s.id).sort()).toEqual(['S1', 'S2']);
|
|
216
|
+
expect(rolesS1.items.map((r: any) => r.id).sort()).toEqual(['r1', 'r2']);
|
|
217
|
+
|
|
218
|
+
// Reconnect: S2 removed, S3 joined; S1 roles changed (r2 removed, r3 added).
|
|
219
|
+
const s1RolesNew = [
|
|
220
|
+
{ id: 'r1', name: 'everyone', priority: 0, color: '#fff' },
|
|
221
|
+
{ id: 'r3', name: 'vip', priority: 2, color: '#fff' },
|
|
222
|
+
];
|
|
223
|
+
emitSession(client, [], [createSpace('S1', s1RolesNew), createSpace('S3')]);
|
|
224
|
+
|
|
225
|
+
const spacesAfter = await tracker.spaces.get();
|
|
226
|
+
const rolesS1After = await tracker.spaces.getRoles('S1');
|
|
227
|
+
|
|
228
|
+
expect(spacesAfter).toBe(spaces);
|
|
229
|
+
expect(spacesAfter.items.map((s: any) => s.id).sort()).toEqual(['S1', 'S3']);
|
|
230
|
+
expect(rolesS1After).toBe(rolesS1); // roles collection identity preserved
|
|
231
|
+
expect(rolesS1After.items.map((r: any) => r.id).sort()).toEqual(['r1', 'r3']);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('keeps a space member collection across reconnect and refetches it', async () => {
|
|
235
|
+
const { client, tracker } = createTracker();
|
|
236
|
+
client.respondTo('GetSpaceMembers', (data: any) => ({
|
|
237
|
+
id: data.id,
|
|
238
|
+
members: [{ user: { id: 'me' } }, { user: { id: 'u1' } }],
|
|
239
|
+
}));
|
|
240
|
+
|
|
241
|
+
emitSession(client, [], [createSpace('S1')]);
|
|
242
|
+
const membersBefore = await tracker.spaces.getMembers('S1');
|
|
243
|
+
expect(client.countSent('GetSpaceMembers')).toBe(1);
|
|
244
|
+
|
|
245
|
+
client.respondTo('GetSpaceMembers', (data: any) => ({
|
|
246
|
+
id: data.id,
|
|
247
|
+
members: [{ user: { id: 'me' } }],
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
emitSession(client, [], [createSpace('S1')]);
|
|
251
|
+
const membersAfter = await tracker.spaces.getMembers('S1');
|
|
252
|
+
|
|
253
|
+
expect(membersAfter).toBe(membersBefore);
|
|
254
|
+
expect(client.countSent('GetSpaceMembers')).toBe(2);
|
|
255
|
+
expect(membersAfter.items.map((m: any) => m.user.id)).toEqual(['me']);
|
|
256
|
+
});
|
|
257
|
+
});
|