mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-06-08 15:21:53 +03:00
* Bump eslint-plugin-matrix-org to enable @typescript-eslint/consistent-type-imports rule * Re-lint after merge
478 lines
20 KiB
TypeScript
478 lines
20 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 MockedObject } from "jest-mock";
|
|
|
|
import { type MatrixClient } from "../../src/client";
|
|
import { EventTimelineSet } from "../../src/models/event-timeline-set";
|
|
import { Room } from "../../src/models/room";
|
|
import { EventTimeline } from "../../src/models/event-timeline";
|
|
import { TimelineIndex, TimelineWindow } from "../../src/timeline-window";
|
|
import { mkMessage } from "../test-utils/test-utils";
|
|
import { type MatrixEvent } from "../../src/models/event";
|
|
|
|
const ROOM_ID = "roomId";
|
|
const USER_ID = "userId";
|
|
const mockClient = {
|
|
getEventTimeline: jest.fn(),
|
|
paginateEventTimeline: jest.fn(),
|
|
supportsThreads: jest.fn(),
|
|
getUserId: jest.fn().mockReturnValue(USER_ID),
|
|
} as unknown as MockedObject<MatrixClient>;
|
|
|
|
/*
|
|
* create a timeline with a bunch (default 3) events.
|
|
* baseIndex is 1 by default.
|
|
*/
|
|
function createTimeline(numEvents = 3, baseIndex = 1): EventTimeline {
|
|
const room = new Room(ROOM_ID, mockClient, USER_ID);
|
|
const timelineSet = new EventTimelineSet(room);
|
|
jest.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet);
|
|
|
|
const timeline = new EventTimeline(timelineSet);
|
|
|
|
// add the events after the baseIndex first
|
|
addEventsToTimeline(timeline, numEvents - baseIndex, false);
|
|
|
|
// then add those before the baseIndex
|
|
addEventsToTimeline(timeline, baseIndex, true);
|
|
|
|
expect(timeline.getBaseIndex()).toEqual(baseIndex);
|
|
return timeline;
|
|
}
|
|
|
|
function addEventsToTimeline(timeline: EventTimeline, numEvents: number, toStartOfTimeline: boolean) {
|
|
for (let i = 0; i < numEvents; i++) {
|
|
timeline.addEvent(
|
|
mkMessage({
|
|
room: ROOM_ID,
|
|
user: USER_ID,
|
|
event: true,
|
|
}),
|
|
{ toStartOfTimeline, addToState: false },
|
|
);
|
|
}
|
|
}
|
|
|
|
function createEvents(numEvents: number): Array<MatrixEvent> {
|
|
const ret = [];
|
|
|
|
for (let i = 0; i < numEvents; i++) {
|
|
ret.push(
|
|
mkMessage({
|
|
room: ROOM_ID,
|
|
user: USER_ID,
|
|
event: true,
|
|
unsigned: { age: 1 },
|
|
}),
|
|
);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* create a pair of linked timelines
|
|
*/
|
|
function createLinkedTimelines(): [EventTimeline, EventTimeline] {
|
|
const tl1 = createTimeline();
|
|
const tl2 = createTimeline();
|
|
tl1.setNeighbouringTimeline(tl2, EventTimeline.FORWARDS);
|
|
tl2.setNeighbouringTimeline(tl1, EventTimeline.BACKWARDS);
|
|
return [tl1, tl2];
|
|
}
|
|
|
|
describe("TimelineIndex", function () {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
mockClient.getEventTimeline.mockResolvedValue(undefined);
|
|
});
|
|
|
|
describe("minIndex", function () {
|
|
it("should return the min index relative to BaseIndex", function () {
|
|
const timelineIndex = new TimelineIndex(createTimeline(), 0);
|
|
expect(timelineIndex.minIndex()).toEqual(-1);
|
|
});
|
|
});
|
|
|
|
describe("maxIndex", function () {
|
|
it("should return the max index relative to BaseIndex", function () {
|
|
const timelineIndex = new TimelineIndex(createTimeline(), 0);
|
|
expect(timelineIndex.maxIndex()).toEqual(2);
|
|
});
|
|
});
|
|
|
|
describe("advance", function () {
|
|
it("should advance up to the end of the timeline", function () {
|
|
const timelineIndex = new TimelineIndex(createTimeline(), 0);
|
|
const result = timelineIndex.advance(3);
|
|
expect(result).toEqual(2);
|
|
expect(timelineIndex.index).toEqual(2);
|
|
});
|
|
|
|
it("should retreat back to the start of the timeline", function () {
|
|
const timelineIndex = new TimelineIndex(createTimeline(), 0);
|
|
const result = timelineIndex.advance(-2);
|
|
expect(result).toEqual(-1);
|
|
expect(timelineIndex.index).toEqual(-1);
|
|
});
|
|
|
|
it("should advance into the next timeline", function () {
|
|
const timelines = createLinkedTimelines();
|
|
const tl1 = timelines[0];
|
|
const tl2 = timelines[1];
|
|
|
|
// initialise the index pointing at the end of the first timeline
|
|
const timelineIndex = new TimelineIndex(tl1, 2);
|
|
|
|
const result = timelineIndex.advance(1);
|
|
expect(result).toEqual(1);
|
|
expect(timelineIndex.timeline).toBe(tl2);
|
|
|
|
// we expect the index to be the zero (ie, the same as the
|
|
// BaseIndex), because the BaseIndex points at the second event,
|
|
// and we've advanced past the first.
|
|
expect(timelineIndex.index).toEqual(0);
|
|
});
|
|
|
|
it("should retreat into the previous timeline", function () {
|
|
const timelines = createLinkedTimelines();
|
|
const tl1 = timelines[0];
|
|
const tl2 = timelines[1];
|
|
|
|
// initialise the index pointing at the start of the second
|
|
// timeline
|
|
const timelineIndex = new TimelineIndex(tl2, -1);
|
|
|
|
const result = timelineIndex.advance(-1);
|
|
expect(result).toEqual(-1);
|
|
expect(timelineIndex.timeline).toBe(tl1);
|
|
expect(timelineIndex.index).toEqual(1);
|
|
});
|
|
});
|
|
|
|
describe("retreat", function () {
|
|
it("should retreat up to the start of the timeline", function () {
|
|
const timelineIndex = new TimelineIndex(createTimeline(), 0);
|
|
const result = timelineIndex.retreat(2);
|
|
expect(result).toEqual(1);
|
|
expect(timelineIndex.index).toEqual(-1);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("TimelineWindow", function () {
|
|
/**
|
|
* create a dummy eventTimelineSet and client, and a TimelineWindow
|
|
* attached to them.
|
|
*/
|
|
function createWindow(
|
|
timeline: EventTimeline,
|
|
opts?: {
|
|
windowLimit?: number;
|
|
},
|
|
): [TimelineWindow, EventTimelineSet] {
|
|
const timelineSet = { getTimelineForEvent: () => null } as unknown as EventTimelineSet;
|
|
mockClient.getEventTimeline.mockResolvedValue(timeline);
|
|
|
|
return [new TimelineWindow(mockClient, timelineSet, opts), timelineSet];
|
|
}
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
mockClient.getEventTimeline.mockResolvedValue(undefined);
|
|
mockClient.paginateEventTimeline.mockResolvedValue(false);
|
|
});
|
|
|
|
describe("load", function () {
|
|
it("should initialise from the live timeline", async function () {
|
|
const liveTimeline = createTimeline();
|
|
const room = new Room(ROOM_ID, mockClient, USER_ID);
|
|
const timelineSet = new EventTimelineSet(room);
|
|
jest.spyOn(timelineSet, "getLiveTimeline").mockReturnValue(liveTimeline);
|
|
|
|
const timelineWindow = new TimelineWindow(mockClient, timelineSet);
|
|
await timelineWindow.load(undefined, 2);
|
|
|
|
expect(timelineSet.getLiveTimeline).toHaveBeenCalled();
|
|
|
|
const expectedEvents = liveTimeline.getEvents().slice(1);
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
|
});
|
|
|
|
it("should initialise from a specific event", async function () {
|
|
const timeline = createTimeline();
|
|
const eventId = timeline.getEvents()[1].getId();
|
|
|
|
const timelineSet = { getTimelineForEvent: () => null } as unknown as EventTimelineSet;
|
|
mockClient.getEventTimeline.mockResolvedValue(timeline);
|
|
|
|
const timelineWindow = new TimelineWindow(mockClient, timelineSet);
|
|
await timelineWindow.load(eventId, 3);
|
|
expect(mockClient.getEventTimeline).toHaveBeenCalledWith(timelineSet, eventId);
|
|
const expectedEvents = timeline.getEvents();
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
|
});
|
|
|
|
it("canPaginate should return false until load has returned", async function () {
|
|
const timeline = createTimeline();
|
|
timeline.setPaginationToken("toktok1", EventTimeline.BACKWARDS);
|
|
timeline.setPaginationToken("toktok2", EventTimeline.FORWARDS);
|
|
|
|
const eventId = timeline.getEvents()[1].getId();
|
|
|
|
const timelineSet = { getTimelineForEvent: () => null } as unknown as EventTimelineSet;
|
|
mockClient.getEventTimeline.mockResolvedValue(timeline);
|
|
|
|
const timelineWindow = new TimelineWindow(mockClient, timelineSet);
|
|
|
|
const timelineWindowLoadPromise = timelineWindow.load(eventId, 3);
|
|
|
|
// cannot paginate before load is complete
|
|
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(false);
|
|
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(false);
|
|
|
|
// wait for load
|
|
await timelineWindowLoadPromise;
|
|
const expectedEvents = timeline.getEvents();
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
|
|
|
// can paginate now
|
|
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(true);
|
|
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("pagination", function () {
|
|
it("should be able to advance across the initial timeline", async function () {
|
|
const timeline = createTimeline();
|
|
const eventId = timeline.getEvents()[1].getId();
|
|
const [timelineWindow] = createWindow(timeline);
|
|
|
|
await timelineWindow.load(eventId, 1);
|
|
|
|
const expectedEvents = [timeline.getEvents()[1]];
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
|
|
|
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(true);
|
|
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(true);
|
|
|
|
expect(await timelineWindow.paginate(EventTimeline.FORWARDS, 2)).toBe(true);
|
|
const expectedEventsAfterPagination = timeline.getEvents().slice(1);
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEventsAfterPagination);
|
|
|
|
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(true);
|
|
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(false);
|
|
|
|
// cant paginate forward anymore
|
|
expect(await timelineWindow.paginate(EventTimeline.FORWARDS, 2)).toBe(false);
|
|
|
|
// paginate back again
|
|
expect(await timelineWindow.paginate(EventTimeline.BACKWARDS, 2)).toBe(true);
|
|
|
|
const expectedEvents3 = timeline.getEvents();
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents3);
|
|
|
|
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(false);
|
|
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(false);
|
|
expect(await timelineWindow.paginate(EventTimeline.BACKWARDS, 2)).toBe(false);
|
|
});
|
|
|
|
it("should advance into next timeline", async function () {
|
|
const tls = createLinkedTimelines();
|
|
const eventId = tls[0].getEvents()[1].getId();
|
|
const [timelineWindow] = createWindow(tls[0], { windowLimit: 5 });
|
|
|
|
await timelineWindow.load(eventId, 3);
|
|
const expectedEvents = tls[0].getEvents();
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
|
|
|
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(false);
|
|
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(true);
|
|
|
|
expect(await timelineWindow.paginate(EventTimeline.FORWARDS, 2)).toBe(true);
|
|
const expectedEvents2 = tls[0].getEvents().concat(tls[1].getEvents().slice(0, 2));
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents2);
|
|
|
|
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(false);
|
|
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(true);
|
|
|
|
expect(await timelineWindow.paginate(EventTimeline.FORWARDS, 2)).toBe(true);
|
|
|
|
// the windowLimit should have made us drop an event from
|
|
// tls[0]
|
|
const expectedEvents3 = tls[0].getEvents().slice(1).concat(tls[1].getEvents());
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents3);
|
|
|
|
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(true);
|
|
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(false);
|
|
expect(await timelineWindow.paginate(EventTimeline.FORWARDS, 2)).toBe(false);
|
|
});
|
|
|
|
it("should retreat into previous timeline", async function () {
|
|
const tls = createLinkedTimelines();
|
|
const eventId = tls[1].getEvents()[1].getId();
|
|
const [timelineWindow] = createWindow(tls[1], { windowLimit: 5 });
|
|
|
|
await timelineWindow.load(eventId, 3);
|
|
|
|
const expectedEvents = tls[1].getEvents();
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
|
|
|
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(true);
|
|
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(false);
|
|
|
|
expect(await timelineWindow.paginate(EventTimeline.BACKWARDS, 2)).toBe(true);
|
|
const expectedEvents2 = tls[0].getEvents().slice(1, 3).concat(tls[1].getEvents());
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents2);
|
|
|
|
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(true);
|
|
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(false);
|
|
|
|
expect(await timelineWindow.paginate(EventTimeline.BACKWARDS, 2)).toBe(true);
|
|
// the windowLimit should have made us drop an event from
|
|
// tls[1]
|
|
const expectedEvents3 = tls[0].getEvents().concat(tls[1].getEvents().slice(0, 2));
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents3);
|
|
|
|
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(false);
|
|
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(true);
|
|
expect(await timelineWindow.paginate(EventTimeline.BACKWARDS, 2)).toBe(false);
|
|
});
|
|
|
|
it("should make forward pagination requests", async function () {
|
|
const timeline = createTimeline();
|
|
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
|
|
|
|
const [timelineWindow] = createWindow(timeline, { windowLimit: 5 });
|
|
const eventId = timeline.getEvents()[1].getId();
|
|
|
|
mockClient.paginateEventTimeline.mockImplementation(async (_t, _opts) => {
|
|
addEventsToTimeline(timeline, 3, false);
|
|
return true;
|
|
});
|
|
|
|
await timelineWindow.load(eventId, 3);
|
|
const expectedEvents = timeline.getEvents();
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
|
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(false);
|
|
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(true);
|
|
|
|
expect(await timelineWindow.paginate(EventTimeline.FORWARDS, 2)).toBe(true);
|
|
expect(mockClient.paginateEventTimeline).toHaveBeenCalledWith(timeline, { backwards: false, limit: 2 });
|
|
const expectedEvents2 = timeline.getEvents().slice(0, 5);
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents2);
|
|
});
|
|
|
|
it("should make backward pagination requests", async function () {
|
|
const timeline = createTimeline();
|
|
timeline.setPaginationToken("toktok", EventTimeline.BACKWARDS);
|
|
|
|
const [timelineWindow] = createWindow(timeline, { windowLimit: 5 });
|
|
const eventId = timeline.getEvents()[1].getId();
|
|
|
|
mockClient.paginateEventTimeline.mockImplementation(async (_t, _opts) => {
|
|
addEventsToTimeline(timeline, 3, true);
|
|
return true;
|
|
});
|
|
|
|
await timelineWindow.load(eventId, 3);
|
|
const expectedEvents = timeline.getEvents();
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
|
|
|
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(true);
|
|
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(false);
|
|
|
|
expect(await timelineWindow.paginate(EventTimeline.BACKWARDS, 2)).toBe(true);
|
|
expect(mockClient.paginateEventTimeline).toHaveBeenCalledWith(timeline, { backwards: true, limit: 2 });
|
|
|
|
const expectedEvents2 = timeline.getEvents().slice(1, 6);
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents2);
|
|
});
|
|
|
|
it("should limit the number of unsuccessful pagination requests", async function () {
|
|
const timeline = createTimeline();
|
|
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
|
|
|
|
const [timelineWindow] = createWindow(timeline, { windowLimit: 5 });
|
|
const eventId = timeline.getEvents()[1].getId();
|
|
|
|
mockClient.paginateEventTimeline.mockImplementation(async (_t, _opts) => {
|
|
return true;
|
|
});
|
|
|
|
await timelineWindow.load(eventId, 3);
|
|
const expectedEvents = timeline.getEvents();
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
|
|
|
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(false);
|
|
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(true);
|
|
expect(await timelineWindow.paginate(EventTimeline.FORWARDS, 2, true, 3)).toBe(false);
|
|
|
|
expect(mockClient.paginateEventTimeline).toHaveBeenCalledWith(timeline, { backwards: false, limit: 2 });
|
|
|
|
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(3);
|
|
const expectedEvents2 = timeline.getEvents().slice(0, 3);
|
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents2);
|
|
|
|
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)).toBe(false);
|
|
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(true);
|
|
});
|
|
});
|
|
|
|
function idsOf(events: Array<MatrixEvent>): Array<string> {
|
|
return events.map((e) => (e ? (e.getId() ?? "MISSING_ID") : "MISSING_EVENT"));
|
|
}
|
|
|
|
describe("removing events", () => {
|
|
it("should shorten if removing an event within the window makes it overflow", function () {
|
|
// Given a room with events in two timelines
|
|
const room = new Room(ROOM_ID, mockClient, USER_ID, { timelineSupport: true });
|
|
const timelineSet = room.getUnfilteredTimelineSet();
|
|
const liveTimeline = room.getLiveTimeline();
|
|
const oldTimeline = room.addTimeline();
|
|
liveTimeline.setNeighbouringTimeline(oldTimeline, EventTimeline.BACKWARDS);
|
|
oldTimeline.setNeighbouringTimeline(liveTimeline, EventTimeline.FORWARDS);
|
|
|
|
const oldEvents = createEvents(5);
|
|
const liveEvents = createEvents(5);
|
|
const [, , e3, e4, e5] = oldEvents;
|
|
const [, e7, e8, e9, e10] = liveEvents;
|
|
room.addLiveEvents(liveEvents, { addToState: false });
|
|
room.addEventsToTimeline(oldEvents, true, false, oldTimeline);
|
|
|
|
// And 2 windows over the timelines in this room
|
|
const oldWindow = new TimelineWindow(mockClient, timelineSet);
|
|
oldWindow.load(e5.getId(), 6);
|
|
expect(idsOf(oldWindow.getEvents())).toEqual(idsOf([e5, e4, e3]));
|
|
|
|
const newWindow = new TimelineWindow(mockClient, timelineSet);
|
|
newWindow.load(e9.getId(), 4);
|
|
expect(idsOf(newWindow.getEvents())).toEqual(idsOf([e7, e8, e9, e10]));
|
|
|
|
// When I remove an event
|
|
room.removeEvent(e8.getId()!);
|
|
|
|
// Then the affected timeline is shortened (because it would have
|
|
// been too long with the removed event gone)
|
|
expect(idsOf(newWindow.getEvents())).toEqual(idsOf([e7, e9, e10]));
|
|
|
|
// And the unaffected one is not
|
|
expect(idsOf(oldWindow.getEvents())).toEqual(idsOf([e5, e4, e3]));
|
|
});
|
|
});
|
|
});
|