element-web/apps/web/test/viewmodels/message-body/EventContentBodyViewModel-test.tsx
Florian Duros 1025d60001
Avoid excessive re-render when setting same reference/value in snapshot (#32918)
* perf: avoid excessive re-render when setting same value in snapshot

* test: update tests
2026-03-25 15:23:11 +00:00

374 lines
13 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 { MsgType, PushRuleKind, type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix";
import { type JSX } from "react";
import {
EventContentBodyViewModel,
type EventContentBodyViewModelProps,
} from "../../../src/viewmodels/message-body/EventContentBodyViewModel";
import { stubClient, mkStubRoom, mkEvent } from "../../test-utils";
import { bodyToNode } from "../../../src/HtmlUtils";
import {
combineRenderers,
mentionPillRenderer,
keywordPillRenderer,
ambiguousLinkTooltipRenderer,
spoilerRenderer,
codeBlockRenderer,
} from "../../../src/renderer";
import PlatformPeg from "../../../src/PlatformPeg";
import type BasePlatform from "../../../src/BasePlatform";
import SettingsStore from "../../../src/settings/SettingsStore";
jest.mock("../../../src/HtmlUtils", () => ({
...jest.requireActual("../../../src/HtmlUtils"),
bodyToNode: jest.fn(),
}));
jest.mock("../../../src/renderer", () => ({
combineRenderers: jest.fn(),
mentionPillRenderer: jest.fn(),
keywordPillRenderer: jest.fn(),
ambiguousLinkTooltipRenderer: jest.fn(),
codeBlockRenderer: jest.fn(),
spoilerRenderer: jest.fn(),
}));
jest.mock("../../../src/PlatformPeg", () => ({
__esModule: true,
default: {
get: jest.fn(),
},
}));
const mockedBodyToNode = jest.mocked(bodyToNode);
const mockedCombineRenderers = jest.mocked(combineRenderers);
const mockedPlatformPeg = jest.mocked(PlatformPeg);
describe("EventContentBodyViewModel", () => {
const defaultContent = {
body: "Hello world",
msgtype: MsgType.Text,
};
const defaultProps = (overrides: Partial<EventContentBodyViewModelProps> = {}): EventContentBodyViewModelProps => ({
client: null,
content: defaultContent,
linkify: false,
as: "span",
...overrides,
});
beforeEach(() => {
mockedBodyToNode.mockReset();
mockedCombineRenderers.mockReset();
mockedPlatformPeg.get.mockReset();
mockedPlatformPeg.get.mockReturnValue(null);
});
it("passes render options to bodyToNode", () => {
const replacer = jest.fn();
mockedCombineRenderers.mockReturnValue(() => replacer);
mockedBodyToNode.mockReturnValue({
strippedBody: "Hello world",
formattedBody: undefined,
emojiBodyElements: undefined,
className: "mx_EventTile_body",
});
const vm = new EventContentBodyViewModel(
defaultProps({
linkify: true,
stripReply: true,
enableBigEmoji: true,
mediaIsVisible: false,
}),
);
const snapshot = vm.getSnapshot();
expect(mockedBodyToNode).toHaveBeenCalledWith(defaultContent, undefined, {
disableBigEmoji: false,
stripReplyFallback: true,
mediaIsVisible: false,
linkify: true,
});
expect(snapshot.body).toBe("Hello world");
expect(snapshot.replacer).toBe(replacer);
expect(snapshot.className).toContain("mx_EventTile_body");
});
it("initializes setting-backed options from SettingsStore when omitted", () => {
const replacer = jest.fn();
const createReplacerFromOptions = jest.fn().mockReturnValue(replacer);
mockedCombineRenderers.mockReturnValue(createReplacerFromOptions);
mockedBodyToNode.mockReturnValue({
strippedBody: "Hello world",
formattedBody: undefined,
emojiBodyElements: undefined,
className: "mx_EventTile_body",
});
const getValueSpy = jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => {
if (settingName === "TextualBody.enableBigEmoji") return false;
if (settingName === "Pill.shouldShowPillAvatar") return false;
return true;
});
new EventContentBodyViewModel(defaultProps());
expect(getValueSpy).toHaveBeenCalledWith("TextualBody.enableBigEmoji");
expect(getValueSpy).toHaveBeenCalledWith("Pill.shouldShowPillAvatar");
expect(mockedBodyToNode).toHaveBeenCalledWith(
defaultContent,
undefined,
expect.objectContaining({ disableBigEmoji: true }),
);
expect(mockedCombineRenderers).toHaveBeenCalledWith();
expect(createReplacerFromOptions).toHaveBeenCalledWith(
expect.objectContaining({ shouldShowPillAvatar: false }),
);
getValueSpy.mockRestore();
});
it("uses the injected client to resolve the room for renderer context", () => {
const replacer = jest.fn();
const createReplacerFromOptions = jest.fn().mockReturnValue(replacer);
mockedCombineRenderers.mockReturnValue(createReplacerFromOptions);
mockedBodyToNode.mockReturnValue({
strippedBody: "Hello world",
formattedBody: undefined,
emojiBodyElements: undefined,
className: "mx_EventTile_body",
});
const client = stubClient();
const mxEvent = mkEvent({
type: "m.room.message",
room: "!room:example.org",
user: "@user:example.org",
content: defaultContent,
event: true,
});
const room = mkStubRoom("!room:example.org", "Room", client) as Room;
const getRoomSpy = jest.spyOn(client, "getRoom").mockReturnValue(room);
new EventContentBodyViewModel(defaultProps({ mxEvent, client }));
expect(getRoomSpy).toHaveBeenCalledWith("!room:example.org");
expect(createReplacerFromOptions).toHaveBeenCalledWith(expect.objectContaining({ room }));
});
it("forces disableBigEmoji for emote events", () => {
const replacer = jest.fn();
mockedCombineRenderers.mockReturnValue(() => replacer);
mockedBodyToNode.mockReturnValue({
strippedBody: "Emote",
formattedBody: undefined,
emojiBodyElements: undefined,
className: "mx_EventTile_body",
});
new EventContentBodyViewModel(
defaultProps({
content: {
body: "Emote",
msgtype: MsgType.Emote,
},
enableBigEmoji: true,
}),
);
expect(mockedBodyToNode).toHaveBeenCalledWith(
{ body: "Emote", msgtype: MsgType.Emote },
undefined,
expect.objectContaining({ disableBigEmoji: true }),
);
});
it("uses parse when formattedBody is provided", () => {
const replacer = jest.fn();
mockedCombineRenderers.mockReturnValue(() => replacer);
mockedBodyToNode.mockReturnValue({
strippedBody: "Hello world",
formattedBody: "<b>Hello</b>",
emojiBodyElements: undefined,
className: "mx_EventTile_body",
});
const vm = new EventContentBodyViewModel(defaultProps());
const snapshot = vm.getSnapshot();
expect(snapshot.formattedBody).toBe("<b>Hello</b>");
expect(snapshot.body).toBe("Hello world");
expect(snapshot.replacer).toBe(replacer);
});
it("uses emojiBodyElements when provided", () => {
const replacer = jest.fn();
mockedCombineRenderers.mockReturnValue(() => replacer);
const emojiElements = ["emoji"] as unknown as JSX.Element[];
mockedBodyToNode.mockReturnValue({
strippedBody: "ignored",
formattedBody: undefined,
emojiBodyElements: emojiElements,
className: "mx_EventTile_body",
});
const vm = new EventContentBodyViewModel(defaultProps());
expect(vm.getSnapshot().body).toBe(emojiElements);
expect(vm.getSnapshot().replacer).toBe(replacer);
});
it("sets dir to auto for div elements even when includeDir is false", () => {
const replacer = jest.fn();
mockedCombineRenderers.mockReturnValue(() => replacer);
mockedBodyToNode.mockReturnValue({
strippedBody: "Hello world",
formattedBody: undefined,
emojiBodyElements: undefined,
className: "mx_EventTile_body",
});
const vm = new EventContentBodyViewModel(defaultProps({ as: "div", includeDir: false }));
expect(vm.getSnapshot().dir).toBe("auto");
});
it("omits dir when includeDir is false on span elements", () => {
const replacer = jest.fn();
mockedCombineRenderers.mockReturnValue(() => replacer);
mockedBodyToNode.mockReturnValue({
strippedBody: "Hello world",
formattedBody: undefined,
emojiBodyElements: undefined,
className: "mx_EventTile_body",
});
const vm = new EventContentBodyViewModel(defaultProps({ as: "span", includeDir: false }));
expect(vm.getSnapshot().dir).toBeUndefined();
});
it("updates snapshot when setEventContent changes content", () => {
const replacer = jest.fn();
mockedCombineRenderers.mockReturnValue(() => replacer);
mockedBodyToNode.mockReturnValue({
strippedBody: "Initial",
formattedBody: undefined,
emojiBodyElements: undefined,
className: "mx_EventTile_body",
});
const vm = new EventContentBodyViewModel(defaultProps());
expect(vm.getSnapshot().body).toBe("Initial");
mockedBodyToNode.mockReturnValue({
strippedBody: "Updated",
formattedBody: undefined,
emojiBodyElements: undefined,
className: "mx_EventTile_body",
});
vm.setEventContent(undefined, { body: "Updated", msgtype: MsgType.Text });
expect(vm.getSnapshot().body).toBe("Updated");
});
it("doesn't emit updates when setters are called with unchanged values", () => {
const replacer = jest.fn();
mockedCombineRenderers.mockReturnValue(() => replacer);
mockedBodyToNode.mockReturnValue({
strippedBody: "Initial",
formattedBody: undefined,
emojiBodyElements: undefined,
className: "mx_EventTile_body",
});
const vm = new EventContentBodyViewModel(defaultProps());
const previousSnapshot = vm.getSnapshot();
const subscriber = jest.fn();
vm.subscribe(subscriber);
vm.setEventContent(undefined, defaultContent);
vm.setAs("span");
expect(subscriber).toHaveBeenCalledTimes(0);
expect(vm.getSnapshot()).toEqual(previousSnapshot);
});
it("includes renderers based on options and platform capabilities", () => {
const replacer = jest.fn();
mockedCombineRenderers.mockReturnValue(() => replacer);
mockedBodyToNode.mockReturnValue({
strippedBody: "Hello world",
formattedBody: undefined,
emojiBodyElements: undefined,
className: "mx_EventTile_body",
});
mockedPlatformPeg.get.mockReturnValue({ needsUrlTooltips: () => true } as unknown as BasePlatform);
const client = stubClient();
const mxEvent = mkEvent({
type: "m.room.message",
room: "!room:example.org",
user: "@user:example.org",
content: defaultContent,
event: true,
});
jest.spyOn(mxEvent, "getPushDetails").mockReturnValue({
rule: {
enabled: true,
kind: PushRuleKind.ContentSpecific,
pattern: "Hello",
},
} as unknown as ReturnType<MatrixEvent["getPushDetails"]>);
jest.spyOn(client, "getRoom").mockReturnValue(mkStubRoom("!room:example.org", "Room", client) as Room);
new EventContentBodyViewModel(
defaultProps({
renderMentionPills: true,
renderKeywordPills: true,
renderTooltipsForAmbiguousLinks: true,
renderSpoilers: true,
renderCodeBlocks: true,
mxEvent,
}),
);
expect(mockedCombineRenderers).toHaveBeenCalledWith(
mentionPillRenderer,
keywordPillRenderer,
ambiguousLinkTooltipRenderer,
spoilerRenderer,
codeBlockRenderer,
);
});
it("skips tooltip renderer when platform does not need URL tooltips", () => {
const replacer = jest.fn();
mockedCombineRenderers.mockReturnValue(() => replacer);
mockedBodyToNode.mockReturnValue({
strippedBody: "Hello world",
formattedBody: undefined,
emojiBodyElements: undefined,
className: "mx_EventTile_body",
});
mockedPlatformPeg.get.mockReturnValue({ needsUrlTooltips: () => false } as unknown as BasePlatform);
new EventContentBodyViewModel(
defaultProps({
renderMentionPills: true,
renderTooltipsForAmbiguousLinks: true,
}),
);
expect(mockedCombineRenderers).toHaveBeenCalledWith(mentionPillRenderer);
});
});