Refactor EventPreview to shared MVVM (#33646)
* Refactor EventPreview to shared MVVM * Fix EventPreviewView export formatting * Snapshots images for stories * Deduplicate event preview formatting * Handle event preview update failures * Coalesce event preview updates * Wait for event preview test expectations
This commit is contained in:
+19
-33
@@ -40,15 +40,13 @@ exports[`<PinnedMessageBanner /> should display display a poll event 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_EventPreview mx_PinnedMessageBanner_message"
|
||||
class="mx_EventPreview _eventPreview_w0s50_8 mx_PinnedMessageBanner_message"
|
||||
data-testid="banner-message"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="mx_EventPreview_prefix"
|
||||
>
|
||||
<strong>
|
||||
Poll:
|
||||
</span>
|
||||
</strong>
|
||||
Alice?
|
||||
</span>
|
||||
</span>
|
||||
@@ -119,9 +117,8 @@ exports[`<PinnedMessageBanner /> should display the last message when the pinned
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="mx_EventPreview mx_PinnedMessageBanner_message"
|
||||
class="mx_EventPreview _eventPreview_w0s50_8 mx_PinnedMessageBanner_message"
|
||||
data-testid="banner-message"
|
||||
title="Third pinned message"
|
||||
>
|
||||
Third pinned message
|
||||
</span>
|
||||
@@ -180,15 +177,13 @@ exports[`<PinnedMessageBanner /> should display the m.audio event type 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_EventPreview mx_PinnedMessageBanner_message"
|
||||
class="mx_EventPreview _eventPreview_w0s50_8 mx_PinnedMessageBanner_message"
|
||||
data-testid="banner-message"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="mx_EventPreview_prefix"
|
||||
>
|
||||
<strong>
|
||||
Audio:
|
||||
</span>
|
||||
</strong>
|
||||
Message with m.audio type
|
||||
</span>
|
||||
</span>
|
||||
@@ -238,15 +233,13 @@ exports[`<PinnedMessageBanner /> should display the m.file event type 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_EventPreview mx_PinnedMessageBanner_message"
|
||||
class="mx_EventPreview _eventPreview_w0s50_8 mx_PinnedMessageBanner_message"
|
||||
data-testid="banner-message"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="mx_EventPreview_prefix"
|
||||
>
|
||||
<strong>
|
||||
File:
|
||||
</span>
|
||||
</strong>
|
||||
Message with m.file type
|
||||
</span>
|
||||
</span>
|
||||
@@ -296,15 +289,13 @@ exports[`<PinnedMessageBanner /> should display the m.image event type 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_EventPreview mx_PinnedMessageBanner_message"
|
||||
class="mx_EventPreview _eventPreview_w0s50_8 mx_PinnedMessageBanner_message"
|
||||
data-testid="banner-message"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="mx_EventPreview_prefix"
|
||||
>
|
||||
<strong>
|
||||
Image:
|
||||
</span>
|
||||
</strong>
|
||||
Message with m.image type
|
||||
</span>
|
||||
</span>
|
||||
@@ -354,15 +345,13 @@ exports[`<PinnedMessageBanner /> should display the m.video event type 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_EventPreview mx_PinnedMessageBanner_message"
|
||||
class="mx_EventPreview _eventPreview_w0s50_8 mx_PinnedMessageBanner_message"
|
||||
data-testid="banner-message"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="mx_EventPreview_prefix"
|
||||
>
|
||||
<strong>
|
||||
Video:
|
||||
</span>
|
||||
</strong>
|
||||
Message with m.video type
|
||||
</span>
|
||||
</span>
|
||||
@@ -429,9 +418,8 @@ exports[`<PinnedMessageBanner /> should render 2 pinned event 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="mx_EventPreview mx_PinnedMessageBanner_message"
|
||||
class="mx_EventPreview _eventPreview_w0s50_8 mx_PinnedMessageBanner_message"
|
||||
data-testid="banner-message"
|
||||
title="Second pinned message"
|
||||
>
|
||||
Second pinned message
|
||||
</span>
|
||||
@@ -511,9 +499,8 @@ exports[`<PinnedMessageBanner /> should render 4 pinned event 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="mx_EventPreview mx_PinnedMessageBanner_message"
|
||||
class="mx_EventPreview _eventPreview_w0s50_8 mx_PinnedMessageBanner_message"
|
||||
data-testid="banner-message"
|
||||
title="Fourth pinned message"
|
||||
>
|
||||
Fourth pinned message
|
||||
</span>
|
||||
@@ -572,9 +559,8 @@ exports[`<PinnedMessageBanner /> should render a single pinned event 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_EventPreview mx_PinnedMessageBanner_message"
|
||||
class="mx_EventPreview _eventPreview_w0s50_8 mx_PinnedMessageBanner_message"
|
||||
data-testid="banner-message"
|
||||
title="First pinned message"
|
||||
>
|
||||
First pinned message
|
||||
</span>
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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 React from "react";
|
||||
import { render, screen, waitFor } from "jest-matrix-react";
|
||||
import { EventType, type MatrixClient, MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { EventPreviewViewModel } from "../../../../../../src/viewmodels/room/timeline/event-tile/EventPreviewViewModel";
|
||||
import { mkEvent, stubClient } from "../../../../../test-utils";
|
||||
|
||||
describe("EventPreviewViewModel", () => {
|
||||
const roomId = "!room:example.com";
|
||||
const userId = "@alice:example.com";
|
||||
|
||||
let cli: MatrixClient;
|
||||
let decryptEventIfNeeded: jest.SpiedFunction<MatrixClient["decryptEventIfNeeded"]>;
|
||||
|
||||
const makeMessageEvent = ({
|
||||
body = "Hello",
|
||||
msgtype = MsgType.Text,
|
||||
}: {
|
||||
body?: string;
|
||||
msgtype?: MsgType;
|
||||
} = {}): MatrixEvent =>
|
||||
mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
room: roomId,
|
||||
user: userId,
|
||||
content: {
|
||||
body,
|
||||
msgtype,
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cli = stubClient();
|
||||
decryptEventIfNeeded = jest.spyOn(cli, "decryptEventIfNeeded").mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("generates a text preview and tooltip", async () => {
|
||||
const mxEvent = makeMessageEvent({ body: "Text preview" });
|
||||
const vm = new EventPreviewViewModel({ cli, mxEvent });
|
||||
|
||||
await waitFor(() => expect(vm.getSnapshot().previewContent).toBe("Text preview"));
|
||||
|
||||
expect(decryptEventIfNeeded).toHaveBeenCalledWith(mxEvent);
|
||||
expect(vm.getSnapshot()).toMatchObject({
|
||||
isVisible: true,
|
||||
previewContent: "Text preview",
|
||||
previewTooltip: "Text preview",
|
||||
});
|
||||
});
|
||||
|
||||
it("generates prefixed media preview content", async () => {
|
||||
const mxEvent = makeMessageEvent({ body: "clip.mp4", msgtype: MsgType.Video });
|
||||
const vm = new EventPreviewViewModel({ cli, mxEvent });
|
||||
|
||||
await waitFor(() => expect(vm.getSnapshot().previewContent).toBeDefined());
|
||||
|
||||
render(<>{vm.getSnapshot().previewContent}</>);
|
||||
|
||||
expect(screen.getByText("Video:")).toBeInTheDocument();
|
||||
expect(screen.getByText("Video:").tagName).toBe("STRONG");
|
||||
expect(screen.getByText("clip.mp4")).toBeInTheDocument();
|
||||
expect(vm.getSnapshot().previewTooltip).toBeUndefined();
|
||||
});
|
||||
|
||||
it("updates the preview when the event is replaced", async () => {
|
||||
const mxEvent = makeMessageEvent({ body: "Original" });
|
||||
const vm = new EventPreviewViewModel({ cli, mxEvent });
|
||||
|
||||
await waitFor(() => expect(vm.getSnapshot().previewContent).toBe("Original"));
|
||||
|
||||
const replacementEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
room_id: roomId,
|
||||
sender: userId,
|
||||
content: {
|
||||
"body": "Edited",
|
||||
"msgtype": MsgType.Text,
|
||||
"m.new_content": {
|
||||
body: "Edited",
|
||||
msgtype: MsgType.Text,
|
||||
},
|
||||
"m.relates_to": {
|
||||
rel_type: "m.replace",
|
||||
event_id: mxEvent.getId(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mxEvent.makeReplaced(replacementEvent);
|
||||
|
||||
await waitFor(() => expect(vm.getSnapshot().previewContent).toBe("Edited"));
|
||||
});
|
||||
|
||||
it("skips unchanged setter inputs and updates for a different event", async () => {
|
||||
const originalEvent = makeMessageEvent({ body: "Original" });
|
||||
const updatedEvent = makeMessageEvent({ body: "Updated" });
|
||||
const vm = new EventPreviewViewModel({ cli, mxEvent: originalEvent });
|
||||
const listener = jest.fn();
|
||||
|
||||
await waitFor(() => expect(vm.getSnapshot().previewContent).toBe("Original"));
|
||||
decryptEventIfNeeded.mockClear();
|
||||
vm.subscribe(listener);
|
||||
|
||||
vm.setEvent(originalEvent);
|
||||
vm.setClient(cli);
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
expect(decryptEventIfNeeded).not.toHaveBeenCalled();
|
||||
|
||||
vm.setEvent(updatedEvent);
|
||||
|
||||
await waitFor(() => expect(vm.getSnapshot().previewContent).toBe("Updated"));
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("removes event listeners when disposed", async () => {
|
||||
const mxEvent = makeMessageEvent({ body: "Original" });
|
||||
const vm = new EventPreviewViewModel({ cli, mxEvent });
|
||||
const listener = jest.fn();
|
||||
|
||||
await waitFor(() => expect(vm.getSnapshot().previewContent).toBe("Original"));
|
||||
decryptEventIfNeeded.mockClear();
|
||||
vm.subscribe(listener);
|
||||
vm.dispose();
|
||||
|
||||
const replacementEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
room_id: roomId,
|
||||
sender: userId,
|
||||
content: {
|
||||
"body": "Edited",
|
||||
"msgtype": MsgType.Text,
|
||||
"m.new_content": {
|
||||
body: "Edited",
|
||||
msgtype: MsgType.Text,
|
||||
},
|
||||
"m.relates_to": {
|
||||
rel_type: "m.replace",
|
||||
event_id: mxEvent.getId(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mxEvent.makeReplaced(replacementEvent);
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
expect(decryptEventIfNeeded).not.toHaveBeenCalled();
|
||||
expect(vm.getSnapshot().previewContent).toBe("Original");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user