Files
blap/apps/web/test/viewmodels/event-tiles/EventTileThreadState-test.ts
T
rbondesson 03360eb260 Refactor EventTile using the MVVM pattern - #5a (#33587)
* Extract EventTile receipt state

* Extract EventTile thread state

* Extract EventTile reaction relation state

* Extract EventTile reply-chain state

* Keep reply chain collapse wiring in EventTile

* Reuse message event eligibility for receipts
2026-05-22 18:25:32 +00:00

165 lines
5.0 KiB
TypeScript

/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { EventType, type MatrixEvent, MsgType, type Thread } from "matrix-js-sdk/src/matrix";
import { mkEvent } from "../../test-utils";
import { TimelineRenderingType } from "../../../src/contexts/RoomContext";
import {
getEventTileThread,
getEventTileThreadState,
type EventTileThreadLookup,
type EventTileThreadStateInput,
} from "../../../src/viewmodels/room/timeline/event-tile/EventTileThreadState";
const roomId = "!room:example.org";
const userId = "@alice:example.org";
function makeEvent({ id = "$event" }: { id?: string } = {}): MatrixEvent {
return mkEvent({
event: true,
id,
type: EventType.RoomMessage,
room: roomId,
user: userId,
content: {
msgtype: MsgType.Text,
body: "Hello",
},
});
}
function makeThread({
id = "$event",
replyToEvent,
}: {
id?: string;
replyToEvent?: MatrixEvent;
} = {}): Thread {
return {
id,
length: 2,
replyToEvent,
} as Thread;
}
function makeInput(overrides: Partial<EventTileThreadStateInput> = {}): EventTileThreadStateInput {
return {
mxEvent: makeEvent(),
thread: null,
timelineRenderingType: TimelineRenderingType.Room,
...overrides,
};
}
function setThreadRootId(mxEvent: MatrixEvent, threadRootId: string): void {
Object.defineProperty(mxEvent, "threadRootId", {
configurable: true,
value: threadRootId,
});
}
describe("EventTileThreadState", () => {
it("uses the event thread when it is already available", () => {
const mxEvent = makeEvent();
const thread = makeThread();
jest.spyOn(mxEvent, "getThread").mockReturnValue(thread);
const room: EventTileThreadLookup = {
findThreadForEvent: jest.fn(),
};
expect(getEventTileThread(mxEvent, room)).toBe(thread);
expect(room.findThreadForEvent).not.toHaveBeenCalled();
});
it("falls back to the room thread lookup", () => {
const mxEvent = makeEvent();
const thread = makeThread();
jest.spyOn(mxEvent, "getThread").mockReturnValue(undefined);
const room: EventTileThreadLookup = {
findThreadForEvent: jest.fn().mockReturnValue(thread),
};
expect(getEventTileThread(mxEvent, room)).toBe(thread);
expect(room.findThreadForEvent).toHaveBeenCalledWith(mxEvent);
});
it("returns null when no thread can be found", () => {
const mxEvent = makeEvent();
jest.spyOn(mxEvent, "getThread").mockReturnValue(undefined);
expect(getEventTileThread(mxEvent, null)).toBeNull();
});
it("shows the thread summary for thread root events", () => {
const mxEvent = makeEvent({ id: "$thread-root" });
const thread = makeThread({ id: "$thread-root" });
const state = getEventTileThreadState(makeInput({ mxEvent, thread }));
expect(state.shouldShowThreadSummary).toBe(true);
expect(state.shouldShowThreadPanelSummary).toBe(true);
expect(state.searchThreadInfo.kind).toBe("none");
});
it("derives the thread panel timestamp from the latest reply", () => {
const replyToEvent = makeEvent({ id: "$reply" });
jest.spyOn(replyToEvent, "getTs").mockReturnValue(123);
const state = getEventTileThreadState(
makeInput({
thread: makeThread({ replyToEvent }),
}),
);
expect(state.threadReplyEventTs).toBe(123);
});
it("shows linked search thread info for thread replies with a highlight link", () => {
const mxEvent = makeEvent();
setThreadRootId(mxEvent, "$thread-root");
const state = getEventTileThreadState(
makeInput({
mxEvent,
timelineRenderingType: TimelineRenderingType.Search,
highlightLink: "https://example.org/thread",
}),
);
expect(state.searchThreadInfo).toEqual({
kind: "link",
href: "https://example.org/thread",
});
});
it("shows text search thread info for thread replies without a highlight link", () => {
const mxEvent = makeEvent();
setThreadRootId(mxEvent, "$thread-root");
const state = getEventTileThreadState(
makeInput({
mxEvent,
timelineRenderingType: TimelineRenderingType.Search,
}),
);
expect(state.searchThreadInfo).toEqual({
kind: "text",
});
});
it("does not show search thread info outside search timelines", () => {
const mxEvent = makeEvent();
setThreadRootId(mxEvent, "$thread-root");
const state = getEventTileThreadState(makeInput({ mxEvent }));
expect(state.searchThreadInfo.kind).toBe("none");
});
});