You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-28 17:02:01 +03:00
* Bump eslint-plugin-matrix-org to enable @typescript-eslint/consistent-type-imports rule * Re-lint after merge
873 lines
37 KiB
TypeScript
873 lines
37 KiB
TypeScript
/*
|
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
import type HttpBackend from "matrix-mock-request";
|
|
import * as utils from "../test-utils/test-utils";
|
|
import { EventStatus } from "../../src/models/event";
|
|
import {
|
|
MatrixError,
|
|
ClientEvent,
|
|
type IEvent,
|
|
type MatrixClient,
|
|
RoomEvent,
|
|
type ISyncResponse,
|
|
type IMinimalEvent,
|
|
type IRoomEvent,
|
|
type Room,
|
|
} from "../../src";
|
|
import { TestClient } from "../TestClient";
|
|
import { KnownMembership } from "../../src/@types/membership";
|
|
|
|
describe("MatrixClient room timelines", function () {
|
|
const userId = "@alice:localhost";
|
|
const userName = "Alice";
|
|
const accessToken = "aseukfgwef";
|
|
const roomId = "!foo:bar";
|
|
const otherUserId = "@bob:localhost";
|
|
let client: MatrixClient | undefined;
|
|
let httpBackend: HttpBackend | undefined;
|
|
|
|
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
|
room: roomId,
|
|
mship: KnownMembership.Join,
|
|
user: userId,
|
|
name: userName,
|
|
});
|
|
const ROOM_NAME_EVENT = utils.mkEvent({
|
|
type: "m.room.name",
|
|
room: roomId,
|
|
user: otherUserId,
|
|
content: {
|
|
name: "Old room name",
|
|
},
|
|
});
|
|
let NEXT_SYNC_DATA: Partial<ISyncResponse>;
|
|
const SYNC_DATA = {
|
|
next_batch: "s_5_3",
|
|
rooms: {
|
|
join: {
|
|
"!foo:bar": {
|
|
// roomId
|
|
timeline: {
|
|
events: [
|
|
utils.mkMessage({
|
|
room: roomId,
|
|
user: otherUserId,
|
|
msg: "hello",
|
|
}),
|
|
],
|
|
prev_batch: "f_1_1",
|
|
},
|
|
state: {
|
|
events: [
|
|
ROOM_NAME_EVENT,
|
|
utils.mkMembership({
|
|
room: roomId,
|
|
mship: KnownMembership.Join,
|
|
user: otherUserId,
|
|
name: "Bob",
|
|
}),
|
|
USER_MEMBERSHIP_EVENT,
|
|
utils.mkEvent({
|
|
type: "m.room.create",
|
|
room: roomId,
|
|
user: userId,
|
|
content: {},
|
|
}),
|
|
],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
function setNextSyncData(events: Partial<IEvent>[] = []) {
|
|
NEXT_SYNC_DATA = {
|
|
next_batch: "n",
|
|
presence: { events: [] },
|
|
rooms: {
|
|
invite: {},
|
|
join: {
|
|
"!foo:bar": {
|
|
timeline: { events: [] },
|
|
state: { events: [] },
|
|
ephemeral: { events: [] },
|
|
},
|
|
},
|
|
leave: {},
|
|
} as unknown as ISyncResponse["rooms"],
|
|
};
|
|
events.forEach(function (e) {
|
|
if (e.room_id !== roomId) {
|
|
throw new Error("setNextSyncData only works with one room id");
|
|
}
|
|
if (e.state_key) {
|
|
// push the current
|
|
NEXT_SYNC_DATA.rooms!.join[roomId].timeline.events.push(e as unknown as IRoomEvent);
|
|
} else if (["m.typing", "m.receipt"].indexOf(e.type!) !== -1) {
|
|
NEXT_SYNC_DATA.rooms!.join[roomId].ephemeral.events.push(e as unknown as IMinimalEvent);
|
|
} else {
|
|
NEXT_SYNC_DATA.rooms!.join[roomId].timeline.events.push(e as unknown as IRoomEvent);
|
|
}
|
|
});
|
|
}
|
|
|
|
const setupTestClient = (): [MatrixClient, HttpBackend] => {
|
|
// these tests should work with or without timelineSupport
|
|
const testClient = new TestClient(userId, "DEVICE", accessToken, undefined, { timelineSupport: true });
|
|
const httpBackend = testClient.httpBackend;
|
|
const client = testClient.client;
|
|
|
|
setNextSyncData();
|
|
httpBackend!.when("GET", "/versions").respond(200, {});
|
|
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
|
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
|
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
|
httpBackend!.when("GET", "/sync").respond(200, function () {
|
|
return NEXT_SYNC_DATA;
|
|
});
|
|
client!.startClient();
|
|
|
|
return [client!, httpBackend];
|
|
};
|
|
|
|
beforeEach(async function () {
|
|
[client!, httpBackend] = setupTestClient();
|
|
await httpBackend.flush("/versions");
|
|
await httpBackend.flush("/pushrules");
|
|
await httpBackend.flush("/filter");
|
|
});
|
|
|
|
afterEach(function () {
|
|
httpBackend!.verifyNoOutstandingExpectation();
|
|
client!.stopClient();
|
|
return httpBackend!.stop();
|
|
});
|
|
|
|
describe("local echo events", function () {
|
|
it(
|
|
"should be added immediately after calling MatrixClient.sendEvent " +
|
|
"with EventStatus.SENDING and the right event.sender",
|
|
async () => {
|
|
const wasMessageAddedPromise = new Promise((resolve) => {
|
|
client!.on(ClientEvent.Sync, async (state) => {
|
|
if (state !== "PREPARED") {
|
|
return;
|
|
}
|
|
const room = client!.getRoom(roomId)!;
|
|
expect(room.timeline.length).toEqual(1);
|
|
|
|
client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
|
// check it was added
|
|
expect(room.timeline.length).toEqual(2);
|
|
// check status
|
|
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
|
|
// check member
|
|
const member = room.timeline[1].sender;
|
|
expect(member?.userId).toEqual(userId);
|
|
expect(member?.name).toEqual(userName);
|
|
|
|
await httpBackend!.flush("/sync", 1);
|
|
resolve(null);
|
|
});
|
|
});
|
|
await httpBackend!.flush("/sync", 1);
|
|
await wasMessageAddedPromise;
|
|
},
|
|
);
|
|
|
|
it(
|
|
"should be updated correctly when the send request finishes " +
|
|
"BEFORE the event comes down the event stream",
|
|
async () => {
|
|
const eventId = "$foo:bar";
|
|
httpBackend!.when("PUT", "/txn1").respond(200, {
|
|
event_id: eventId,
|
|
});
|
|
|
|
const ev = utils.mkMessage({
|
|
msg: "I am a fish",
|
|
user: userId,
|
|
room: roomId,
|
|
});
|
|
ev.event_id = eventId;
|
|
ev.unsigned = { transaction_id: "txn1" };
|
|
setNextSyncData([ev]);
|
|
|
|
const wasMessageAddedPromise = new Promise((resolve) => {
|
|
client!.on(ClientEvent.Sync, function (state) {
|
|
if (state !== "PREPARED") {
|
|
return;
|
|
}
|
|
const room = client!.getRoom(roomId)!;
|
|
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(async () => {
|
|
expect(room.timeline[1].getId()).toEqual(eventId);
|
|
await httpBackend!.flush("/sync", 1);
|
|
expect(room.timeline[1].getId()).toEqual(eventId);
|
|
resolve(null);
|
|
});
|
|
httpBackend!.flush("/txn1", 1);
|
|
});
|
|
});
|
|
await httpBackend!.flush("/sync", 1);
|
|
await wasMessageAddedPromise;
|
|
},
|
|
);
|
|
|
|
it(
|
|
"should be updated correctly when the send request finishes " +
|
|
"AFTER the event comes down the event stream",
|
|
async () => {
|
|
const eventId = "$foo:bar";
|
|
httpBackend!.when("PUT", "/txn1").respond(200, {
|
|
event_id: eventId,
|
|
});
|
|
|
|
const ev = utils.mkMessage({
|
|
msg: "I am a fish",
|
|
user: userId,
|
|
room: roomId,
|
|
});
|
|
ev.event_id = eventId;
|
|
ev.unsigned = { transaction_id: "txn1" };
|
|
setNextSyncData([ev]);
|
|
|
|
const wasMessageAddedPromise = new Promise((resolve) => {
|
|
client!.on(ClientEvent.Sync, async (state) => {
|
|
if (state !== "PREPARED") {
|
|
return;
|
|
}
|
|
const room = client!.getRoom(roomId)!;
|
|
const messageSendPromise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
|
await httpBackend!.flush("/sync", 1);
|
|
expect(room.timeline.length).toEqual(2);
|
|
httpBackend!.flush("/txn1", 1);
|
|
await messageSendPromise;
|
|
expect(room.timeline.length).toEqual(2);
|
|
expect(room.timeline[1].getId()).toEqual(eventId);
|
|
resolve(null);
|
|
});
|
|
});
|
|
await httpBackend!.flush("/sync", 1);
|
|
await wasMessageAddedPromise;
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("paginated events", function () {
|
|
let sbEvents: Partial<IEvent>[];
|
|
const sbEndTok = "pagin_end";
|
|
|
|
beforeEach(function () {
|
|
sbEvents = [];
|
|
httpBackend!.when("GET", "/messages").respond(200, function () {
|
|
return {
|
|
chunk: sbEvents,
|
|
start: "pagin_start",
|
|
end: sbEndTok,
|
|
};
|
|
});
|
|
});
|
|
|
|
it("should set Room.oldState.paginationToken to null at the start of the timeline.", async () => {
|
|
const didPaginatePromise = new Promise((resolve) => {
|
|
client!.on(ClientEvent.Sync, async (state) => {
|
|
if (state !== "PREPARED") {
|
|
return;
|
|
}
|
|
const room = client!.getRoom(roomId)!;
|
|
expect(room.timeline.length).toEqual(1);
|
|
|
|
await Promise.all([client!.scrollback(room), httpBackend!.flush("/messages", 1)]);
|
|
expect(room.timeline.length).toEqual(1);
|
|
expect(room.oldState.paginationToken).toBe(null);
|
|
|
|
// still have a sync to flush
|
|
await httpBackend!.flush("/sync", 1);
|
|
resolve(null);
|
|
});
|
|
});
|
|
await httpBackend!.flush("/sync", 1);
|
|
await didPaginatePromise;
|
|
});
|
|
|
|
it("should set the right event.sender values", async () => {
|
|
// We're aiming for an eventual timeline of:
|
|
//
|
|
// 'Old Alice' joined the room
|
|
// <Old Alice> I'm old alice
|
|
// @alice:localhost changed their name from 'Old Alice' to 'Alice'
|
|
// <Alice> I'm alice
|
|
// ------^ /messages results above this point, /sync result below
|
|
// <Bob> hello
|
|
|
|
// make an m.room.member event for alice's join
|
|
const joinMshipEvent = utils.mkMembership({
|
|
mship: KnownMembership.Join,
|
|
user: userId,
|
|
room: roomId,
|
|
name: "Old Alice",
|
|
url: undefined,
|
|
});
|
|
|
|
// make an m.room.member event with prev_content for alice's nick
|
|
// change
|
|
const oldMshipEvent = utils.mkMembership({
|
|
mship: KnownMembership.Join,
|
|
user: userId,
|
|
room: roomId,
|
|
name: userName,
|
|
url: "mxc://some/url",
|
|
});
|
|
oldMshipEvent.unsigned!.prev_content = {
|
|
displayname: "Old Alice",
|
|
avatar_url: undefined,
|
|
membership: KnownMembership.Join,
|
|
};
|
|
|
|
// set the list of events to return on scrollback (/messages)
|
|
// N.B. synapse returns /messages in reverse chronological order
|
|
sbEvents = [
|
|
utils.mkMessage({
|
|
user: userId,
|
|
room: roomId,
|
|
msg: "I'm alice",
|
|
}),
|
|
oldMshipEvent,
|
|
utils.mkMessage({
|
|
user: userId,
|
|
room: roomId,
|
|
msg: "I'm old alice",
|
|
}),
|
|
joinMshipEvent,
|
|
];
|
|
|
|
const didPaginatePromise = new Promise((resolve) => {
|
|
client!.on(ClientEvent.Sync, async (state) => {
|
|
if (state !== "PREPARED") {
|
|
return;
|
|
}
|
|
const room = client!.getRoom(roomId)!;
|
|
// sync response
|
|
expect(room.timeline.length).toEqual(1);
|
|
|
|
await Promise.all([client!.scrollback(room), httpBackend!.flush("/messages", 1)]);
|
|
|
|
expect(room.timeline.length).toEqual(5);
|
|
const joinMsg = room.timeline[0];
|
|
expect(joinMsg.sender?.name).toEqual("Old Alice");
|
|
const oldMsg = room.timeline[1];
|
|
expect(oldMsg.sender?.name).toEqual("Old Alice");
|
|
const newMsg = room.timeline[3];
|
|
expect(newMsg.sender?.name).toEqual(userName);
|
|
|
|
// still have a sync to flush
|
|
await httpBackend!.flush("/sync", 1);
|
|
resolve(null);
|
|
});
|
|
});
|
|
await httpBackend!.flush("/sync", 1);
|
|
await didPaginatePromise;
|
|
});
|
|
|
|
it("should add it them to the right place in the timeline", async () => {
|
|
// set the list of events to return on scrollback
|
|
sbEvents = [
|
|
utils.mkMessage({
|
|
user: userId,
|
|
room: roomId,
|
|
msg: "I am new",
|
|
}),
|
|
utils.mkMessage({
|
|
user: userId,
|
|
room: roomId,
|
|
msg: "I am old",
|
|
}),
|
|
];
|
|
|
|
const didPaginatePromise = new Promise((resolve) => {
|
|
client!.on(ClientEvent.Sync, async (state) => {
|
|
if (state !== "PREPARED") {
|
|
return;
|
|
}
|
|
const room = client!.getRoom(roomId)!;
|
|
expect(room.timeline.length).toEqual(1);
|
|
|
|
await Promise.all([client!.scrollback(room), httpBackend!.flush("/messages", 1)]);
|
|
|
|
expect(room.timeline.length).toEqual(3);
|
|
expect(room.timeline[0].event).toEqual(sbEvents[1]);
|
|
expect(room.timeline[1].event).toEqual(sbEvents[0]);
|
|
|
|
// still have a sync to flush
|
|
await httpBackend!.flush("/sync", 1);
|
|
resolve(null);
|
|
});
|
|
});
|
|
await httpBackend!.flush("/sync", 1);
|
|
await didPaginatePromise;
|
|
});
|
|
|
|
it("should use 'end' as the next pagination token", async () => {
|
|
// set the list of events to return on scrollback
|
|
sbEvents = [
|
|
utils.mkMessage({
|
|
user: userId,
|
|
room: roomId,
|
|
msg: "I am new",
|
|
}),
|
|
];
|
|
|
|
const didPaginatePromise = new Promise((resolve) => {
|
|
client!.on(ClientEvent.Sync, async (state) => {
|
|
if (state !== "PREPARED") {
|
|
return;
|
|
}
|
|
const room = client!.getRoom(roomId)!;
|
|
expect(room.oldState.paginationToken).toBeTruthy();
|
|
|
|
await Promise.all([client!.scrollback(room, 1), httpBackend!.flush("/messages", 1)]);
|
|
expect(room.oldState.paginationToken).toEqual(sbEndTok);
|
|
|
|
// still have a sync to flush
|
|
await httpBackend!.flush("/sync", 1);
|
|
resolve(null);
|
|
});
|
|
});
|
|
await httpBackend!.flush("/sync", 1);
|
|
await didPaginatePromise;
|
|
});
|
|
});
|
|
|
|
describe("new events", function () {
|
|
it("should be added to the right place in the timeline", function () {
|
|
const eventData = [
|
|
utils.mkMessage({ user: userId, room: roomId }),
|
|
utils.mkMessage({ user: userId, room: roomId }),
|
|
];
|
|
setNextSyncData(eventData);
|
|
|
|
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(() => {
|
|
const room = client!.getRoom(roomId)!;
|
|
|
|
let index = 0;
|
|
client!.on(RoomEvent.Timeline, function (event, rm, toStart) {
|
|
expect(toStart).toBe(false);
|
|
expect(rm).toEqual(room);
|
|
expect(event.event).toEqual(eventData[index]);
|
|
index += 1;
|
|
});
|
|
|
|
httpBackend!.flush("/messages", 1);
|
|
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(function () {
|
|
expect(index).toEqual(2);
|
|
expect(room.timeline.length).toEqual(3);
|
|
expect(room.timeline[2].event).toEqual(eventData[1]);
|
|
expect(room.timeline[1].event).toEqual(eventData[0]);
|
|
});
|
|
});
|
|
});
|
|
|
|
it("should set the right event.sender values", function () {
|
|
const eventData = [
|
|
utils.mkMessage({ user: userId, room: roomId }),
|
|
utils.mkMembership({
|
|
user: userId,
|
|
room: roomId,
|
|
mship: KnownMembership.Join,
|
|
name: "New Name",
|
|
}),
|
|
utils.mkMessage({ user: userId, room: roomId }),
|
|
];
|
|
setNextSyncData(eventData);
|
|
|
|
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(() => {
|
|
const room = client!.getRoom(roomId)!;
|
|
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(function () {
|
|
const preNameEvent = room.timeline[room.timeline.length - 3];
|
|
const postNameEvent = room.timeline[room.timeline.length - 1];
|
|
expect(preNameEvent.sender?.name).toEqual(userName);
|
|
expect(postNameEvent.sender?.name).toEqual("New Name");
|
|
});
|
|
});
|
|
});
|
|
|
|
it("should set the right room.name", function () {
|
|
const secondRoomNameEvent = utils.mkEvent({
|
|
user: userId,
|
|
room: roomId,
|
|
type: "m.room.name",
|
|
content: {
|
|
name: "Room 2",
|
|
},
|
|
});
|
|
setNextSyncData([secondRoomNameEvent]);
|
|
|
|
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(() => {
|
|
const room = client!.getRoom(roomId)!;
|
|
let nameEmitCount = 0;
|
|
client!.on(RoomEvent.Name, function (rm) {
|
|
nameEmitCount += 1;
|
|
});
|
|
|
|
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)])
|
|
.then(function () {
|
|
expect(nameEmitCount).toEqual(1);
|
|
expect(room.name).toEqual("Room 2");
|
|
// do another round
|
|
const thirdRoomNameEvent = utils.mkEvent({
|
|
user: userId,
|
|
room: roomId,
|
|
type: "m.room.name",
|
|
content: {
|
|
name: "Room 3",
|
|
},
|
|
});
|
|
setNextSyncData([thirdRoomNameEvent]);
|
|
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
|
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]);
|
|
})
|
|
.then(function () {
|
|
expect(nameEmitCount).toEqual(2);
|
|
expect(room.name).toEqual("Room 3");
|
|
});
|
|
});
|
|
});
|
|
|
|
it("should set the right room members", function () {
|
|
const userC = "@cee:bar";
|
|
const userD = "@dee:bar";
|
|
const eventData = [
|
|
utils.mkMembership({
|
|
user: userC,
|
|
room: roomId,
|
|
mship: KnownMembership.Join,
|
|
name: "C",
|
|
}),
|
|
utils.mkMembership({
|
|
user: userC,
|
|
room: roomId,
|
|
mship: KnownMembership.Invite,
|
|
skey: userD,
|
|
}),
|
|
];
|
|
setNextSyncData(eventData);
|
|
|
|
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(() => {
|
|
const room = client!.getRoom(roomId)!;
|
|
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(function () {
|
|
expect(room.currentState.getMembers().length).toEqual(4);
|
|
expect(room.currentState.getMember(userC)!.name).toEqual("C");
|
|
expect(room.currentState.getMember(userC)!.membership).toEqual(KnownMembership.Join);
|
|
expect(room.currentState.getMember(userD)!.name).toEqual(userD);
|
|
expect(room.currentState.getMember(userD)!.membership).toEqual(KnownMembership.Invite);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("gappy sync", function () {
|
|
it("should copy the last known state to the new timeline", function () {
|
|
const eventData = [utils.mkMessage({ user: userId, room: roomId })];
|
|
setNextSyncData(eventData);
|
|
NEXT_SYNC_DATA.rooms!.join[roomId].timeline.limited = true;
|
|
|
|
return Promise.all([
|
|
httpBackend!.flush("/versions", 1),
|
|
httpBackend!.flush("/sync", 1),
|
|
utils.syncPromise(client!),
|
|
]).then(() => {
|
|
const room = client!.getRoom(roomId)!;
|
|
|
|
httpBackend!.flush("/messages", 1);
|
|
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(function () {
|
|
expect(room.timeline.length).toEqual(1);
|
|
expect(room.timeline[0].event).toEqual(eventData[0]);
|
|
expect(room.currentState.getMembers().length).toEqual(2);
|
|
expect(room.currentState.getMember(userId)!.name).toEqual(userName);
|
|
expect(room.currentState.getMember(userId)!.membership).toEqual(KnownMembership.Join);
|
|
expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob");
|
|
expect(room.currentState.getMember(otherUserId)!.membership).toEqual(KnownMembership.Join);
|
|
});
|
|
});
|
|
});
|
|
|
|
it("should emit a `RoomEvent.TimelineReset` event when the sync response is `limited`", function () {
|
|
const eventData = [utils.mkMessage({ user: userId, room: roomId })];
|
|
setNextSyncData(eventData);
|
|
NEXT_SYNC_DATA.rooms!.join[roomId].timeline.limited = true;
|
|
|
|
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(() => {
|
|
const room = client!.getRoom(roomId)!;
|
|
|
|
let emitCount = 0;
|
|
client!.on(RoomEvent.TimelineReset, function (emitRoom) {
|
|
expect(emitRoom).toEqual(room);
|
|
emitCount++;
|
|
});
|
|
|
|
httpBackend!.flush("/messages", 1);
|
|
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(function () {
|
|
expect(emitCount).toEqual(1);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Refresh live timeline", () => {
|
|
const initialSyncEventData = [
|
|
utils.mkMessage({ user: userId, room: roomId }),
|
|
utils.mkMessage({ user: userId, room: roomId }),
|
|
utils.mkMessage({ user: userId, room: roomId }),
|
|
];
|
|
|
|
const contextUrl =
|
|
`/rooms/${encodeURIComponent(roomId)}/context/` +
|
|
`${encodeURIComponent(initialSyncEventData[2].event_id!)}`;
|
|
const contextResponse = {
|
|
start: "start_token",
|
|
events_before: [initialSyncEventData[1], initialSyncEventData[0]],
|
|
event: initialSyncEventData[2],
|
|
events_after: [],
|
|
state: [USER_MEMBERSHIP_EVENT],
|
|
end: "end_token",
|
|
};
|
|
|
|
let room: Room;
|
|
beforeEach(async () => {
|
|
setNextSyncData(initialSyncEventData);
|
|
|
|
// Create a room from the sync
|
|
await Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 1)]);
|
|
|
|
// Get the room after the first sync so the room is created
|
|
room = client!.getRoom(roomId)!;
|
|
expect(room).toBeTruthy();
|
|
});
|
|
|
|
it("should clear and refresh messages in timeline", async () => {
|
|
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
|
|
// to construct a new timeline from.
|
|
httpBackend!.when("GET", contextUrl).respond(200, function () {
|
|
// The timeline should be cleared at this point in the refresh
|
|
expect(room.timeline.length).toEqual(0);
|
|
|
|
return contextResponse;
|
|
});
|
|
|
|
// Refresh the timeline.
|
|
await Promise.all([room.refreshLiveTimeline(), httpBackend!.flushAllExpected()]);
|
|
|
|
// Make sure the message are visible
|
|
const resultantEventsInTimeline = room.getUnfilteredTimelineSet().getLiveTimeline().getEvents();
|
|
const resultantEventIdsInTimeline = resultantEventsInTimeline.map((event) => event.getId());
|
|
expect(resultantEventIdsInTimeline).toEqual([
|
|
initialSyncEventData[0].event_id,
|
|
initialSyncEventData[1].event_id,
|
|
initialSyncEventData[2].event_id,
|
|
]);
|
|
});
|
|
|
|
it("Perfectly merges timelines if a sync finishes while refreshing the timeline", async () => {
|
|
// `/context` request for `refreshLiveTimeline()` ->
|
|
// `getEventTimeline()` to construct a new timeline from.
|
|
//
|
|
// We only resolve this request after we detect that the timeline
|
|
// was reset(when it goes blank) and force a sync to happen in the
|
|
// middle of all of this refresh timeline logic. We want to make
|
|
// sure the sync pagination still works as expected after messing
|
|
// the refresh timline logic messes with the pagination tokens.
|
|
httpBackend!.when("GET", contextUrl).respond(200, () => {
|
|
// Now finally return and make the `/context` request respond
|
|
return contextResponse;
|
|
});
|
|
|
|
// Wait for the timeline to reset(when it goes blank) which means
|
|
// it's in the middle of the refrsh logic right before the
|
|
// `getEventTimeline()` -> `/context`. Then simulate a racey `/sync`
|
|
// to happen in the middle of all of this refresh timeline logic. We
|
|
// want to make sure the sync pagination still works as expected
|
|
// after messing the refresh timline logic messes with the
|
|
// pagination tokens.
|
|
//
|
|
// We define this here so the event listener is in place before we
|
|
// call `room.refreshLiveTimeline()`.
|
|
const racingSyncEventData = [utils.mkMessage({ user: userId, room: roomId })];
|
|
const waitForRaceySyncAfterResetPromise = new Promise<void>((resolve, reject) => {
|
|
let eventFired = false;
|
|
// Throw a more descriptive error if this part of the test times out.
|
|
const failTimeout = setTimeout(() => {
|
|
if (eventFired) {
|
|
reject(
|
|
new Error(
|
|
"TestError: `RoomEvent.TimelineReset` fired but we timed out trying to make" +
|
|
"a `/sync` happen in time.",
|
|
),
|
|
);
|
|
} else {
|
|
reject(new Error("TestError: Timed out while waiting for `RoomEvent.TimelineReset` to fire."));
|
|
}
|
|
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Jest? */);
|
|
|
|
room.on(RoomEvent.TimelineReset, async () => {
|
|
try {
|
|
eventFired = true;
|
|
|
|
// The timeline should be cleared at this point in the refresh
|
|
expect(room.getUnfilteredTimelineSet().getLiveTimeline().getEvents().length).toEqual(0);
|
|
|
|
// Then make a `/sync` happen by sending a message and seeing that it
|
|
// shows up (simulate a /sync naturally racing with us).
|
|
setNextSyncData(racingSyncEventData);
|
|
httpBackend!.when("GET", "/sync").respond(200, function () {
|
|
return NEXT_SYNC_DATA;
|
|
});
|
|
await Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!, 1)]);
|
|
// Make sure the timeline has the racey sync data
|
|
const afterRaceySyncTimelineEvents = room
|
|
.getUnfilteredTimelineSet()
|
|
.getLiveTimeline()
|
|
.getEvents();
|
|
const afterRaceySyncTimelineEventIds = afterRaceySyncTimelineEvents.map((event) =>
|
|
event.getId(),
|
|
);
|
|
expect(afterRaceySyncTimelineEventIds).toEqual([racingSyncEventData[0].event_id]);
|
|
|
|
clearTimeout(failTimeout);
|
|
resolve();
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Refresh the timeline. Just start the function, we will wait for
|
|
// it to finish after the racey sync.
|
|
const refreshLiveTimelinePromise = room.refreshLiveTimeline();
|
|
|
|
await waitForRaceySyncAfterResetPromise;
|
|
|
|
await Promise.all([
|
|
refreshLiveTimelinePromise,
|
|
// Then flush the remaining `/context` to left the refresh logic complete
|
|
httpBackend!.flushAllExpected(),
|
|
]);
|
|
|
|
// Make sure sync pagination still works by seeing a new message show up
|
|
// after refreshing the timeline.
|
|
const afterRefreshEventData = [utils.mkMessage({ user: userId, room: roomId })];
|
|
setNextSyncData(afterRefreshEventData);
|
|
httpBackend!.when("GET", "/sync").respond(200, function () {
|
|
return NEXT_SYNC_DATA;
|
|
});
|
|
await Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 1)]);
|
|
|
|
// Make sure the timeline includes the the events from the `/sync`
|
|
// that raced and beat us in the middle of everything and the
|
|
// `/sync` after the refresh. Since the `/sync` beat us to create
|
|
// the timeline, `initialSyncEventData` won't be visible unless we
|
|
// paginate backwards with `/messages`.
|
|
const resultantEventsInTimeline = room.getUnfilteredTimelineSet().getLiveTimeline().getEvents();
|
|
const resultantEventIdsInTimeline = resultantEventsInTimeline.map((event) => event.getId());
|
|
expect(resultantEventIdsInTimeline).toEqual([
|
|
racingSyncEventData[0].event_id,
|
|
afterRefreshEventData[0].event_id,
|
|
]);
|
|
});
|
|
|
|
it("Timeline recovers after `/context` request to generate new timeline fails", async () => {
|
|
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
|
|
// to construct a new timeline from.
|
|
httpBackend!
|
|
.when("GET", contextUrl)
|
|
.check(() => {
|
|
// The timeline should be cleared at this point in the refresh
|
|
expect(room.timeline.length).toEqual(0);
|
|
})
|
|
.respond(
|
|
500,
|
|
new MatrixError({
|
|
errcode: "TEST_FAKE_ERROR",
|
|
error:
|
|
"We purposely intercepted this /context request to make it fail " +
|
|
"in order to test whether the refresh timeline code is resilient",
|
|
}),
|
|
);
|
|
|
|
// Refresh the timeline and expect it to fail
|
|
const settledFailedRefreshPromises = await Promise.allSettled([
|
|
room.refreshLiveTimeline(),
|
|
httpBackend!.flushAllExpected(),
|
|
]);
|
|
// We only expect `TEST_FAKE_ERROR` here. Anything else is
|
|
// unexpected and should fail the test.
|
|
if (settledFailedRefreshPromises[0].status === "fulfilled") {
|
|
throw new Error("Expected the /context request to fail with a 500");
|
|
} else if (settledFailedRefreshPromises[0].reason.errcode !== "TEST_FAKE_ERROR") {
|
|
throw settledFailedRefreshPromises[0].reason;
|
|
}
|
|
|
|
// The timeline will be empty after we refresh the timeline and fail
|
|
// to construct a new timeline.
|
|
expect(room.timeline.length).toEqual(0);
|
|
|
|
// `/messages` request for `refreshLiveTimeline()` ->
|
|
// `getLatestTimeline()` to construct a new timeline from.
|
|
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`).respond(200, function () {
|
|
return {
|
|
chunk: [
|
|
{
|
|
// The latest message in the room
|
|
event_id: initialSyncEventData[2].event_id,
|
|
},
|
|
],
|
|
};
|
|
});
|
|
// `/context` request for `refreshLiveTimeline()` ->
|
|
// `getLatestTimeline()` -> `getEventTimeline()` to construct a new
|
|
// timeline from.
|
|
httpBackend!.when("GET", contextUrl).respond(200, function () {
|
|
// The timeline should be cleared at this point in the refresh
|
|
expect(room.timeline.length).toEqual(0);
|
|
|
|
return contextResponse;
|
|
});
|
|
|
|
// Refresh the timeline again but this time it should pass
|
|
await Promise.all([room.refreshLiveTimeline(), httpBackend!.flushAllExpected()]);
|
|
|
|
// Make sure sync pagination still works by seeing a new message show up
|
|
// after refreshing the timeline.
|
|
const afterRefreshEventData = [utils.mkMessage({ user: userId, room: roomId })];
|
|
setNextSyncData(afterRefreshEventData);
|
|
httpBackend!.when("GET", "/sync").respond(200, function () {
|
|
return NEXT_SYNC_DATA;
|
|
});
|
|
await Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 1)]);
|
|
|
|
// Make sure the message are visible
|
|
const resultantEventsInTimeline = room.getUnfilteredTimelineSet().getLiveTimeline().getEvents();
|
|
const resultantEventIdsInTimeline = resultantEventsInTimeline.map((event) => event.getId());
|
|
expect(resultantEventIdsInTimeline).toEqual([
|
|
initialSyncEventData[0].event_id,
|
|
initialSyncEventData[1].event_id,
|
|
initialSyncEventData[2].event_id,
|
|
afterRefreshEventData[0].event_id,
|
|
]);
|
|
});
|
|
});
|
|
});
|