/* * 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 => ({ 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: "Hello", emojiBodyElements: undefined, className: "mx_EventTile_body", }); const vm = new EventContentBodyViewModel(defaultProps()); const snapshot = vm.getSnapshot(); expect(snapshot.formattedBody).toBe("Hello"); 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); 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); }); });