1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-23 17:02:25 +03:00
Files
matrix-js-sdk/spec/unit/timeline-window.spec.ts
David Baker 5bcd26e506 Support MSC4222 state_after (#4487)
* WIP support for state_after

* Fix sliding sync sdk / embedded tests

* Allow both state & state_after to be undefined

Since it must have allowed state to be undefined previously: the test
had it as such.

* Fix limited sync handling

* Need to use state_after being undefined

if state can be undefined anyway

* Make sliding sync sdk tests pass

* Remove deprecated interfaces & backwards-compat code

* Remove useless assignment

* Use updates unstable prefix

* Clarify docs

* Remove additional semi-backwards compatible overload

* Update unstable prefixes

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add test for MSC4222 behaviour

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tidy

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add comments to explain why things work as they are.

* Fix sync accumulator for state_after sync handling

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Revert "Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)"

This reverts commit 957329b218.

* Fix Sync Accumulator toJSON putting start timeline state in state_after field

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add test case

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Timo <toger5@hotmail.de>
2024-11-27 11:40:41 +00:00

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 { MockedObject } from "jest-mock";
import { 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 { 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]));
});
});
});