/* Copyright 2024 New Vector Ltd. Copyright 2022 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import React, { ReactElement } from "react"; import { mocked } from "jest-mock"; import { render, screen } from "@testing-library/react"; import { IContent } from "matrix-js-sdk/src/matrix"; import { bodyToSpan, formatEmojis, topicToHtml } from "../src/HtmlUtils"; import SettingsStore from "../src/settings/SettingsStore"; jest.mock("../src/settings/SettingsStore"); const enableHtmlTopicFeature = () => { mocked(SettingsStore).getValue.mockImplementation((arg): any => { return arg === "feature_html_topic"; }); }; describe("topicToHtml", () => { function getContent() { return screen.getByRole("contentinfo").children[0].innerHTML; } it("converts plain text topic to HTML", () => { render(
{topicToHtml("pizza", undefined, null, false)}
); expect(getContent()).toEqual("pizza"); }); it("converts plain text topic with emoji to HTML", () => { render(
{topicToHtml("pizza ๐Ÿ•", undefined, null, false)}
); expect(getContent()).toEqual('pizza ๐Ÿ•'); }); it("converts literal HTML topic to HTML", async () => { enableHtmlTopicFeature(); render(
{topicToHtml("pizza", undefined, null, false)}
); expect(getContent()).toEqual("<b>pizza</b>"); }); it("converts true HTML topic to HTML", async () => { enableHtmlTopicFeature(); render(
{topicToHtml("**pizza**", "pizza", null, false)}
); expect(getContent()).toEqual("pizza"); }); it("converts true HTML topic with emoji to HTML", async () => { enableHtmlTopicFeature(); render(
{topicToHtml("**pizza** ๐Ÿ•", "pizza ๐Ÿ•", null, false)}
); expect(getContent()).toEqual('pizza ๐Ÿ•'); }); }); describe("bodyToHtml", () => { function getHtml(content: IContent, highlights?: string[]): string { return (bodyToSpan(content, highlights, {}) as ReactElement).props.dangerouslySetInnerHTML.__html; } it("should apply highlights to HTML messages", () => { const html = getHtml( { body: "test **foo** bar", msgtype: "m.text", formatted_body: "test foo bar", format: "org.matrix.custom.html", }, ["test"], ); expect(html).toMatchInlineSnapshot(`"test foo bar"`); }); it("should apply highlights to plaintext messages", () => { const html = getHtml( { body: "test foo bar", msgtype: "m.text", }, ["test"], ); expect(html).toMatchInlineSnapshot(`"test foo bar"`); }); it("should not respect HTML tags in plaintext message highlighting", () => { const html = getHtml( { body: "test foo bar", msgtype: "m.text", }, ["test"], ); expect(html).toMatchInlineSnapshot(`"test foo <b>bar"`); }); it("generates big emoji for emoji made of multiple characters", () => { const { asFragment } = render(bodyToSpan({ body: "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ โ†”๏ธ ๐Ÿ‡ฎ๐Ÿ‡ธ", msgtype: "m.text" }, [], {}) as ReactElement); expect(asFragment()).toMatchSnapshot(); }); it("should generate big emoji for an emoji-only reply to a message", () => { const { asFragment } = render( bodyToSpan( { "body": "> <@sender1:server> Test\n\n๐Ÿฅฐ", "format": "org.matrix.custom.html", "formatted_body": '
In reply to @sender1:server
Test
๐Ÿฅฐ', "m.relates_to": { "m.in_reply_to": { event_id: "$eventId", }, }, "msgtype": "m.text", }, [], { stripReplyFallback: true, }, ) as ReactElement, ); expect(asFragment()).toMatchSnapshot(); }); it("does not mistake characters in text presentation mode for emoji", () => { const { asFragment } = render(bodyToSpan({ body: "โ†” โ—๏ธŽ", msgtype: "m.text" }, [], {}) as ReactElement); expect(asFragment()).toMatchSnapshot(); }); describe("feature_latex_maths", () => { beforeEach(() => { jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => feature === "feature_latex_maths"); }); it("should render inline katex", () => { const html = getHtml({ body: "hello \\xi world", msgtype: "m.text", formatted_body: 'hello \\xi world', format: "org.matrix.custom.html", }); expect(html).toMatchSnapshot(); }); it("should render block katex", () => { const html = getHtml({ body: "hello \\xi world", msgtype: "m.text", formatted_body: '

hello

\\xi

world

', format: "org.matrix.custom.html", }); expect(html).toMatchSnapshot(); }); it("should not mangle code blocks", () => { const html = getHtml({ body: "hello \\xi world", msgtype: "m.text", formatted_body: "

hello

$\\xi$

world

", format: "org.matrix.custom.html", }); expect(html).toMatchSnapshot(); }); it("should not mangle divs", () => { const html = getHtml({ body: "hello world", msgtype: "m.text", formatted_body: "

hello

world
", format: "org.matrix.custom.html", }); expect(html).toMatchSnapshot(); }); }); }); describe("formatEmojis", () => { it.each([ ["๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ", [["๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ", "flag-england"]]], ["๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ", [["๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ", "flag-scotland"]]], ["๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ", [["๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ", "flag-wales"]]], ])("%s emoji", (emoji, expectations) => { const res = formatEmojis(emoji, false); expect(res).toHaveLength(expectations.length); for (let i = 0; i < res.length; i++) { const [emoji, title] = expectations[i]; expect(res[i].props.children).toEqual(emoji); expect(res[i].props.title).toEqual(`:${title}:`); } }); });