element-web/test/components/structures/TimelinePanel-test.tsx
Robin 87e2274ae7
Enable pagination for overlay timelines (#10757)
* Update @types/jest to 29.2.6

This adds the correct types for the contexts field on mock objects, which I'll need shortly

* Enable pagination for overlay timelines
2023-05-12 16:27:41 +00:00

870 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Copyright 2022 - 2023 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 { render, waitFor, screen } from "@testing-library/react";
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
import {
EventTimelineSet,
EventType,
MatrixClient,
MatrixEvent,
PendingEventOrdering,
Room,
RoomEvent,
RoomMember,
RoomState,
TimelineWindow,
} from "matrix-js-sdk/src/matrix";
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
import {
FeatureSupport,
Thread,
THREAD_RELATION_TYPE,
ThreadEvent,
ThreadFilterType,
} from "matrix-js-sdk/src/models/thread";
import React, { createRef } from "react";
import { mocked } from "jest-mock";
import { forEachRight } from "lodash";
import TimelinePanel from "../../../src/components/structures/TimelinePanel";
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { isCallEvent } from "../../../src/components/structures/LegacyCallEventGrouper";
import { flushPromises, mkMembership, mkRoom, stubClient } from "../../test-utils";
import { mkThread } from "../../test-utils/threads";
import { createMessageEventContent } from "../../test-utils/events";
import ScrollPanel from "../../../src/components/structures/ScrollPanel";
// ScrollPanel calls this, but jsdom doesn't mock it for us
HTMLDivElement.prototype.scrollBy = () => {};
const newReceipt = (eventId: string, userId: string, readTs: number, fullyReadTs: number): MatrixEvent => {
const receiptContent = {
[eventId]: {
[ReceiptType.Read]: { [userId]: { ts: readTs } },
[ReceiptType.ReadPrivate]: { [userId]: { ts: readTs } },
[ReceiptType.FullyRead]: { [userId]: { ts: fullyReadTs } },
},
};
return new MatrixEvent({ content: receiptContent, type: EventType.Receipt });
};
const mkTimeline = (room: Room, events: MatrixEvent[]): [EventTimeline, EventTimelineSet] => {
const timelineSet = {
room: room as Room,
getLiveTimeline: () => timeline,
getTimelineForEvent: () => timeline,
getPendingEvents: () => [],
} as unknown as EventTimelineSet;
const timeline = new EventTimeline(timelineSet);
events.forEach((event) => timeline.addEvent(event, { toStartOfTimeline: false }));
return [timeline, timelineSet];
};
const getProps = (room: Room, events: MatrixEvent[]): TimelinePanel["props"] => {
const [, timelineSet] = mkTimeline(room, events);
return {
timelineSet,
manageReadReceipts: true,
sendReadReceiptOnLoad: true,
};
};
const mockEvents = (room: Room, count = 2): MatrixEvent[] => {
const events: MatrixEvent[] = [];
for (let index = 0; index < count; index++) {
events.push(
new MatrixEvent({
room_id: room.roomId,
event_id: `${room.roomId}_event_${index}`,
type: EventType.RoomMessage,
sender: "userId",
content: createMessageEventContent("`Event${index}`"),
}),
);
}
return events;
};
const setupTestData = (): [MatrixClient, Room, MatrixEvent[]] => {
const client = MatrixClientPeg.get();
const room = mkRoom(client, "roomId");
const events = mockEvents(room);
return [client, room, events];
};
const setupOverlayTestData = (client: MatrixClient, mainEvents: MatrixEvent[]): [Room, MatrixEvent[]] => {
const virtualRoom = mkRoom(client, "virtualRoomId");
const overlayEvents = mockEvents(virtualRoom, 5);
// Set the event order that we'll be looking for in the timeline
overlayEvents[0].localTimestamp = 1000;
mainEvents[0].localTimestamp = 2000;
overlayEvents[1].localTimestamp = 3000;
overlayEvents[2].localTimestamp = 4000;
overlayEvents[3].localTimestamp = 5000;
mainEvents[1].localTimestamp = 6000;
overlayEvents[4].localTimestamp = 7000;
return [virtualRoom, overlayEvents];
};
const expectEvents = (container: HTMLElement, events: MatrixEvent[]): void => {
const eventTiles = container.querySelectorAll(".mx_EventTile");
const eventTileIds = [...eventTiles].map((tileElement) => tileElement.getAttribute("data-event-id"));
expect(eventTileIds).toEqual(events.map((ev) => ev.getId()));
};
const withScrollPanelMountSpy = async (
continuation: (mountSpy: jest.SpyInstance<void, []>) => Promise<void>,
): Promise<void> => {
const mountSpy = jest.spyOn(ScrollPanel.prototype, "componentDidMount");
try {
await continuation(mountSpy);
} finally {
mountSpy.mockRestore();
}
};
const setupPagination = (
client: MatrixClient,
timeline: EventTimeline,
previousPage: MatrixEvent[] | null,
nextPage: MatrixEvent[] | null,
): void => {
timeline.setPaginationToken(previousPage === null ? null : "start", EventTimeline.BACKWARDS);
timeline.setPaginationToken(nextPage === null ? null : "end", EventTimeline.FORWARDS);
mocked(client).paginateEventTimeline.mockImplementation(async (tl, { backwards }) => {
if (tl === timeline) {
if (backwards) {
forEachRight(previousPage ?? [], (event) => tl.addEvent(event, { toStartOfTimeline: true }));
} else {
(nextPage ?? []).forEach((event) => tl.addEvent(event, { toStartOfTimeline: false }));
}
// Prevent any further pagination attempts in this direction
tl.setPaginationToken(null, backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS);
return true;
} else {
return false;
}
});
};
describe("TimelinePanel", () => {
beforeEach(() => {
stubClient();
});
describe("read receipts and markers", () => {
it("should forget the read marker when asked to", () => {
const cli = MatrixClientPeg.get();
const readMarkersSent: string[] = [];
// Track calls to setRoomReadMarkers
cli.setRoomReadMarkers = (_roomId, rmEventId, _a, _b) => {
readMarkersSent.push(rmEventId);
return Promise.resolve({});
};
const ev0 = new MatrixEvent({
event_id: "ev0",
sender: "@u2:m.org",
origin_server_ts: 111,
type: EventType.RoomMessage,
content: createMessageEventContent("hello 1"),
});
const ev1 = new MatrixEvent({
event_id: "ev1",
sender: "@u2:m.org",
origin_server_ts: 222,
type: EventType.RoomMessage,
content: createMessageEventContent("hello 2"),
});
const roomId = "#room:example.com";
const userId = cli.credentials.userId!;
const room = new Room(roomId, cli, userId, { pendingEventOrdering: PendingEventOrdering.Detached });
// Create a TimelinePanel with ev0 already present
const timelineSet = new EventTimelineSet(room, {});
timelineSet.addLiveEvent(ev0);
const ref = createRef<TimelinePanel>();
render(
<TimelinePanel
timelineSet={timelineSet}
manageReadMarkers={true}
manageReadReceipts={true}
eventId={ev0.getId()}
ref={ref}
/>,
);
const timelinePanel = ref.current!;
// An event arrived, and we read it
timelineSet.addLiveEvent(ev1);
room.addEphemeralEvents([newReceipt("ev1", userId, 222, 220)]);
// Sanity: We have not sent any read marker yet
expect(readMarkersSent).toEqual([]);
// This is what we are testing: forget the read marker - this should
// update our read marker to match the latest receipt we sent
timelinePanel.forgetReadMarker();
// We sent off a read marker for the new event
expect(readMarkersSent).toEqual(["ev1"]);
});
});
it("should scroll event into view when props.eventId changes", () => {
const client = MatrixClientPeg.get();
const room = mkRoom(client, "roomId");
const events = mockEvents(room);
const props = {
...getProps(room, events),
onEventScrolledIntoView: jest.fn(),
};
const { rerender } = render(<TimelinePanel {...props} />);
expect(props.onEventScrolledIntoView).toHaveBeenCalledWith(undefined);
props.eventId = events[1].getId();
rerender(<TimelinePanel {...props} />);
expect(props.onEventScrolledIntoView).toHaveBeenCalledWith(events[1].getId());
});
it("paginates", async () => {
const [client, room, events] = setupTestData();
const eventsPage1 = events.slice(0, 1);
const eventsPage2 = events.slice(1, 2);
// Start with only page 2 of the main events in the window
const [timeline, timelineSet] = mkTimeline(room, eventsPage2);
setupPagination(client, timeline, eventsPage1, null);
await withScrollPanelMountSpy(async (mountSpy) => {
const { container } = render(<TimelinePanel {...getProps(room, events)} timelineSet={timelineSet} />);
await waitFor(() => expectEvents(container, [events[1]]));
// ScrollPanel has no chance of working in jsdom, so we've no choice
// but to do some shady stuff to trigger the fill callback by hand
const scrollPanel = mountSpy.mock.contexts[0] as ScrollPanel;
scrollPanel.props.onFillRequest!(true);
await waitFor(() => expectEvents(container, [events[0], events[1]]));
});
});
it("unpaginates", async () => {
const [, room, events] = setupTestData();
await withScrollPanelMountSpy(async (mountSpy) => {
const { container } = render(<TimelinePanel {...getProps(room, events)} />);
await waitFor(() => expectEvents(container, [events[0], events[1]]));
// ScrollPanel has no chance of working in jsdom, so we've no choice
// but to do some shady stuff to trigger the unfill callback by hand
const scrollPanel = mountSpy.mock.contexts[0] as ScrollPanel;
scrollPanel.props.onUnfillRequest!(true, events[0].getId()!);
await waitFor(() => expectEvents(container, [events[1]]));
});
});
describe("onRoomTimeline", () => {
it("ignores events for other timelines", () => {
const [client, room, events] = setupTestData();
const otherTimelineSet = { room: room as Room } as EventTimelineSet;
const otherTimeline = new EventTimeline(otherTimelineSet);
const props = {
...getProps(room, events),
onEventScrolledIntoView: jest.fn(),
};
const paginateSpy = jest.spyOn(TimelineWindow.prototype, "paginate").mockClear();
render(<TimelinePanel {...props} />);
const event = new MatrixEvent({ type: RoomEvent.Timeline });
const data = { timeline: otherTimeline, liveEvent: true };
client.emit(RoomEvent.Timeline, event, room, false, false, data);
expect(paginateSpy).not.toHaveBeenCalled();
});
it("ignores timeline updates without a live event", () => {
const [client, room, events] = setupTestData();
const props = getProps(room, events);
const paginateSpy = jest.spyOn(TimelineWindow.prototype, "paginate").mockClear();
render(<TimelinePanel {...props} />);
const event = new MatrixEvent({ type: RoomEvent.Timeline });
const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: false };
client.emit(RoomEvent.Timeline, event, room, false, false, data);
expect(paginateSpy).not.toHaveBeenCalled();
});
it("ignores timeline where toStartOfTimeline is true", () => {
const [client, room, events] = setupTestData();
const props = getProps(room, events);
const paginateSpy = jest.spyOn(TimelineWindow.prototype, "paginate").mockClear();
render(<TimelinePanel {...props} />);
const event = new MatrixEvent({ type: RoomEvent.Timeline });
const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: false };
const toStartOfTimeline = true;
client.emit(RoomEvent.Timeline, event, room, toStartOfTimeline, false, data);
expect(paginateSpy).not.toHaveBeenCalled();
});
it("advances the timeline window", () => {
const [client, room, events] = setupTestData();
const props = getProps(room, events);
const paginateSpy = jest.spyOn(TimelineWindow.prototype, "paginate").mockClear();
render(<TimelinePanel {...props} />);
const event = new MatrixEvent({ type: RoomEvent.Timeline });
const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: true };
client.emit(RoomEvent.Timeline, event, room, false, false, data);
expect(paginateSpy).toHaveBeenCalledWith(EventTimeline.FORWARDS, 1, false);
});
it("advances the overlay timeline window", async () => {
const [client, room, events] = setupTestData();
const virtualRoom = mkRoom(client, "virtualRoomId");
const virtualEvents = mockEvents(virtualRoom);
const { timelineSet: overlayTimelineSet } = getProps(virtualRoom, virtualEvents);
const props = {
...getProps(room, events),
overlayTimelineSet,
};
const paginateSpy = jest.spyOn(TimelineWindow.prototype, "paginate").mockClear();
render(<TimelinePanel {...props} />);
await flushPromises();
const event = new MatrixEvent({ type: RoomEvent.Timeline });
const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: true };
client.emit(RoomEvent.Timeline, event, room, false, false, data);
await flushPromises();
expect(paginateSpy).toHaveBeenCalledTimes(2);
});
});
describe("with overlayTimeline", () => {
it("renders merged timeline", async () => {
const [client, room, events] = setupTestData();
const virtualRoom = mkRoom(client, "virtualRoomId");
const virtualCallInvite = new MatrixEvent({
type: "m.call.invite",
room_id: virtualRoom.roomId,
event_id: `virtualCallEvent1`,
});
const virtualCallMetaEvent = new MatrixEvent({
type: "org.matrix.call.sdp_stream_metadata_changed",
room_id: virtualRoom.roomId,
event_id: `virtualCallEvent2`,
});
const virtualEvents = [virtualCallInvite, ...mockEvents(virtualRoom), virtualCallMetaEvent];
const { timelineSet: overlayTimelineSet } = getProps(virtualRoom, virtualEvents);
const { container } = render(
<TimelinePanel
{...getProps(room, events)}
overlayTimelineSet={overlayTimelineSet}
overlayTimelineSetFilter={isCallEvent}
/>,
);
await waitFor(() =>
expectEvents(container, [
// main timeline events are included
events[0],
events[1],
// virtual timeline call event is included
virtualCallInvite,
// virtual call event has no tile renderer => not rendered
]),
);
});
it.each([
["when it starts with no overlay events", true],
["to get enough overlay events", false],
])("expands the initial window %s", async (_s, startWithEmptyOverlayWindow) => {
const [client, room, events] = setupTestData();
const [virtualRoom, overlayEvents] = setupOverlayTestData(client, events);
let overlayEventsPage1: MatrixEvent[];
let overlayEventsPage2: MatrixEvent[];
let overlayEventsPage3: MatrixEvent[];
if (startWithEmptyOverlayWindow) {
overlayEventsPage1 = overlayEvents.slice(0, 3);
overlayEventsPage2 = [];
overlayEventsPage3 = overlayEvents.slice(3, 5);
} else {
overlayEventsPage1 = overlayEvents.slice(0, 2);
overlayEventsPage2 = overlayEvents.slice(2, 3);
overlayEventsPage3 = overlayEvents.slice(3, 5);
}
// Start with only page 2 of the overlay events in the window
const [overlayTimeline, overlayTimelineSet] = mkTimeline(virtualRoom, overlayEventsPage2);
setupPagination(client, overlayTimeline, overlayEventsPage1, overlayEventsPage3);
const { container } = render(
<TimelinePanel {...getProps(room, events)} overlayTimelineSet={overlayTimelineSet} />,
);
await waitFor(() =>
expectEvents(container, [
overlayEvents[0],
events[0],
overlayEvents[1],
overlayEvents[2],
overlayEvents[3],
events[1],
overlayEvents[4],
]),
);
});
it("extends overlay window beyond main window at the start of the timeline", async () => {
const [client, room, events] = setupTestData();
const [virtualRoom, overlayEvents] = setupOverlayTestData(client, events);
// Delete event 0 so the TimelinePanel will still leave some stuff
// unloaded for us to test with
events.shift();
const overlayEventsPage1 = overlayEvents.slice(0, 2);
const overlayEventsPage2 = overlayEvents.slice(2, 5);
// Start with only page 2 of the overlay events in the window
const [overlayTimeline, overlayTimelineSet] = mkTimeline(virtualRoom, overlayEventsPage2);
setupPagination(client, overlayTimeline, overlayEventsPage1, null);
const { container } = render(
<TimelinePanel {...getProps(room, events)} overlayTimelineSet={overlayTimelineSet} />,
);
await waitFor(() =>
expectEvents(container, [
// These first two are the newly loaded events
overlayEvents[0],
overlayEvents[1],
overlayEvents[2],
overlayEvents[3],
events[0],
overlayEvents[4],
]),
);
});
it("extends overlay window beyond main window at the end of the timeline", async () => {
const [client, room, events] = setupTestData();
const [virtualRoom, overlayEvents] = setupOverlayTestData(client, events);
// Delete event 1 so the TimelinePanel will still leave some stuff
// unloaded for us to test with
events.pop();
const overlayEventsPage1 = overlayEvents.slice(0, 2);
const overlayEventsPage2 = overlayEvents.slice(2, 5);
// Start with only page 1 of the overlay events in the window
const [overlayTimeline, overlayTimelineSet] = mkTimeline(virtualRoom, overlayEventsPage1);
setupPagination(client, overlayTimeline, null, overlayEventsPage2);
const { container } = render(
<TimelinePanel {...getProps(room, events)} overlayTimelineSet={overlayTimelineSet} />,
);
await waitFor(() =>
expectEvents(container, [
overlayEvents[0],
events[0],
overlayEvents[1],
// These are the newly loaded events
overlayEvents[2],
overlayEvents[3],
overlayEvents[4],
]),
);
});
it("paginates", async () => {
const [client, room, events] = setupTestData();
const [virtualRoom, overlayEvents] = setupOverlayTestData(client, events);
const eventsPage1 = events.slice(0, 1);
const eventsPage2 = events.slice(1, 2);
// Start with only page 1 of the main events in the window
const [timeline, timelineSet] = mkTimeline(room, eventsPage1);
const [, overlayTimelineSet] = mkTimeline(virtualRoom, overlayEvents);
setupPagination(client, timeline, null, eventsPage2);
await withScrollPanelMountSpy(async (mountSpy) => {
const { container } = render(
<TimelinePanel
{...getProps(room, events)}
timelineSet={timelineSet}
overlayTimelineSet={overlayTimelineSet}
/>,
);
await waitFor(() => expectEvents(container, [overlayEvents[0], events[0]]));
// ScrollPanel has no chance of working in jsdom, so we've no choice
// but to do some shady stuff to trigger the fill callback by hand
const scrollPanel = mountSpy.mock.contexts[0] as ScrollPanel;
scrollPanel.props.onFillRequest!(false);
await waitFor(() =>
expectEvents(container, [
overlayEvents[0],
events[0],
overlayEvents[1],
overlayEvents[2],
overlayEvents[3],
events[1],
overlayEvents[4],
]),
);
});
});
it.each([
["down", "main", true, false],
["down", "overlay", true, true],
["up", "main", false, false],
["up", "overlay", false, true],
])("unpaginates %s to an event from the %s timeline", async (_s1, _s2, backwards, fromOverlay) => {
const [client, room, events] = setupTestData();
const [virtualRoom, overlayEvents] = setupOverlayTestData(client, events);
let marker: MatrixEvent;
let expectedEvents: MatrixEvent[];
if (backwards) {
if (fromOverlay) {
marker = overlayEvents[1];
// Overlay events 01 and event 0 should be unpaginated
// Overlay events 23 should be hidden since they're at the edge of the window
expectedEvents = [events[1], overlayEvents[4]];
} else {
marker = events[0];
// Overlay event 0 and event 0 should be unpaginated
// Overlay events 13 should be hidden since they're at the edge of the window
expectedEvents = [events[1], overlayEvents[4]];
}
} else {
if (fromOverlay) {
marker = overlayEvents[4];
// Only the last overlay event should be unpaginated
expectedEvents = [
overlayEvents[0],
events[0],
overlayEvents[1],
overlayEvents[2],
overlayEvents[3],
events[1],
];
} else {
// Get rid of overlay event 4 so we can test the case where no overlay events get unpaginated
overlayEvents.pop();
marker = events[1];
// Only event 1 should be unpaginated
// Overlay events 12 should be hidden since they're at the edge of the window
expectedEvents = [overlayEvents[0], events[0]];
}
}
const [, overlayTimelineSet] = mkTimeline(virtualRoom, overlayEvents);
await withScrollPanelMountSpy(async (mountSpy) => {
const { container } = render(
<TimelinePanel {...getProps(room, events)} overlayTimelineSet={overlayTimelineSet} />,
);
await waitFor(() =>
expectEvents(container, [
overlayEvents[0],
events[0],
overlayEvents[1],
overlayEvents[2],
overlayEvents[3],
events[1],
...(!backwards && !fromOverlay ? [] : [overlayEvents[4]]),
]),
);
// ScrollPanel has no chance of working in jsdom, so we've no choice
// but to do some shady stuff to trigger the unfill callback by hand
const scrollPanel = mountSpy.mock.contexts[0] as ScrollPanel;
scrollPanel.props.onUnfillRequest!(backwards, marker.getId()!);
await waitFor(() => expectEvents(container, expectedEvents));
});
});
});
describe("when a thread updates", () => {
let client: MatrixClient;
let room: Room;
let allThreads: EventTimelineSet;
let root: MatrixEvent;
let reply1: MatrixEvent;
let reply2: MatrixEvent;
beforeEach(() => {
client = MatrixClientPeg.get();
Thread.hasServerSideSupport = FeatureSupport.Stable;
room = new Room("roomId", client, "userId");
allThreads = new EventTimelineSet(
room,
{
pendingEvents: false,
},
undefined,
undefined,
ThreadFilterType.All,
);
const timeline = new EventTimeline(allThreads);
allThreads.getLiveTimeline = () => timeline;
allThreads.getTimelineForEvent = () => timeline;
reply1 = new MatrixEvent({
room_id: room.roomId,
event_id: "event_reply_1",
type: EventType.RoomMessage,
sender: "userId",
content: createMessageEventContent("ReplyEvent1"),
});
reply2 = new MatrixEvent({
room_id: room.roomId,
event_id: "event_reply_2",
type: EventType.RoomMessage,
sender: "userId",
content: createMessageEventContent("ReplyEvent2"),
});
root = new MatrixEvent({
room_id: room.roomId,
event_id: "event_root_1",
type: EventType.RoomMessage,
sender: "userId",
content: createMessageEventContent("RootEvent"),
});
const eventMap: { [key: string]: MatrixEvent } = {
[root.getId()!]: root,
[reply1.getId()!]: reply1,
[reply2.getId()!]: reply2,
};
room.findEventById = (eventId: string) => eventMap[eventId];
client.fetchRoomEvent = async (roomId: string, eventId: string) =>
roomId === room.roomId ? eventMap[eventId]?.event : {};
});
it("updates thread previews", async () => {
root.setUnsigned({
"m.relations": {
[THREAD_RELATION_TYPE.name]: {
latest_event: reply1.event,
count: 1,
current_user_participated: true,
},
},
});
const thread = room.createThread(root.getId()!, root, [], true);
// So that we do not have to mock the thread loading
thread.initialEventsFetched = true;
// @ts-ignore
thread.fetchEditsWhereNeeded = () => Promise.resolve();
await thread.addEvent(reply1, true);
await allThreads.getLiveTimeline().addEvent(thread.rootEvent!, { toStartOfTimeline: true });
const replyToEvent = jest.spyOn(thread, "replyToEvent", "get");
const dom = render(
<MatrixClientContext.Provider value={client}>
<TimelinePanel timelineSet={allThreads} manageReadReceipts sendReadReceiptOnLoad />
</MatrixClientContext.Provider>,
);
await dom.findByText("RootEvent");
await dom.findByText("ReplyEvent1");
expect(replyToEvent).toHaveBeenCalled();
root.setUnsigned({
"m.relations": {
[THREAD_RELATION_TYPE.name]: {
latest_event: reply2.event,
count: 2,
current_user_participated: true,
},
},
});
replyToEvent.mockClear();
await thread.addEvent(reply2, false, true);
await dom.findByText("RootEvent");
await dom.findByText("ReplyEvent2");
expect(replyToEvent).toHaveBeenCalled();
});
it("ignores thread updates for unknown threads", async () => {
root.setUnsigned({
"m.relations": {
[THREAD_RELATION_TYPE.name]: {
latest_event: reply1.event,
count: 1,
current_user_participated: true,
},
},
});
const realThread = room.createThread(root.getId()!, root, [], true);
// So that we do not have to mock the thread loading
realThread.initialEventsFetched = true;
// @ts-ignore
realThread.fetchEditsWhereNeeded = () => Promise.resolve();
await realThread.addEvent(reply1, true);
await allThreads.getLiveTimeline().addEvent(realThread.rootEvent!, { toStartOfTimeline: true });
const replyToEvent = jest.spyOn(realThread, "replyToEvent", "get");
// @ts-ignore
const fakeThread1: Thread = {
id: undefined!,
get roomId(): string {
return room.roomId;
},
};
const fakeRoom = new Room("thisroomdoesnotexist", client, "userId");
// @ts-ignore
const fakeThread2: Thread = {
id: root.getId()!,
get roomId(): string {
return fakeRoom.roomId;
},
};
const dom = render(
<MatrixClientContext.Provider value={client}>
<TimelinePanel timelineSet={allThreads} manageReadReceipts sendReadReceiptOnLoad />
</MatrixClientContext.Provider>,
);
await dom.findByText("RootEvent");
await dom.findByText("ReplyEvent1");
expect(replyToEvent).toHaveBeenCalled();
replyToEvent.mockClear();
room.emit(ThreadEvent.Update, fakeThread1);
room.emit(ThreadEvent.Update, fakeThread2);
await dom.findByText("ReplyEvent1");
expect(replyToEvent).not.toHaveBeenCalled();
replyToEvent.mockClear();
});
});
it("renders when the last message is an undecryptable thread root", async () => {
const client = MatrixClientPeg.get();
client.isRoomEncrypted = () => true;
client.supportsThreads = () => true;
client.decryptEventIfNeeded = () => Promise.resolve();
const authorId = client.getUserId()!;
const room = new Room("roomId", client, authorId, {
lazyLoadMembers: false,
pendingEventOrdering: PendingEventOrdering.Detached,
});
const events = mockEvents(room);
const timelineSet = room.getUnfilteredTimelineSet();
const { rootEvent } = mkThread({
room,
client,
authorId,
participantUserIds: [authorId],
});
events.push(rootEvent);
events.forEach((event) => timelineSet.getLiveTimeline().addEvent(event, { toStartOfTimeline: true }));
const roomMembership = mkMembership({
mship: "join",
prevMship: "join",
user: authorId,
room: room.roomId,
event: true,
skey: "123",
});
events.push(roomMembership);
const member = new RoomMember(room.roomId, authorId);
member.membership = "join";
const roomState = new RoomState(room.roomId);
jest.spyOn(roomState, "getMember").mockReturnValue(member);
jest.spyOn(timelineSet.getLiveTimeline(), "getState").mockReturnValue(roomState);
timelineSet.addEventToTimeline(roomMembership, timelineSet.getLiveTimeline(), { toStartOfTimeline: false });
for (const event of events) {
jest.spyOn(event, "isDecryptionFailure").mockReturnValue(true);
jest.spyOn(event, "shouldAttemptDecryption").mockReturnValue(false);
}
const { container } = render(
<MatrixClientContext.Provider value={client}>
<TimelinePanel timelineSet={timelineSet} manageReadReceipts={true} sendReadReceiptOnLoad={true} />
</MatrixClientContext.Provider>,
);
await waitFor(() => expect(screen.queryByRole("progressbar")).toBeNull());
await waitFor(() => expect(container.querySelector(".mx_RoomView_MessageList")).not.toBeEmptyDOMElement());
});
});