Files
ThreadNet-Web/test/unit-tests/HtmlUtils-test.tsx
T
Will HuntandGitHub 75d9898dff Global configuration flag for media previews (#29582)
* Modify useMediaVisible to take a room.

* Add initial support for a account data level key.

* Update controls.

* Update settings

* Lint and fixes

* make some tests go happy

* lint

* i18n

* update preferences

* prettier

* Update settings tab.

* update screenshot

* Update docs

* Rewrite controller

* Rewrite tons of tests

* Rewrite RoomAvatar to be a functional component

This is so we can use hooks to determine the setting state.

* lint

* lint

* Tidy up comments

* Apply media visible hook to inline images.

* Move conditionals.

* copyright all the things

* Review changes

* Update html utils to properly discard media.

* Types fix

* Fixing tests that break settings getValue expectations

* Fix logic around media preview calculation

* Fix room header tests

* Fixup tests for timelinePanel

* Clear settings in matrixchat

* Update tests to use SettingsStore where possible.

* fix bug

* revert changes to client.ts

* copyright years

* Add header

* Add a test for MediaPreviewAccountSettingsTab

* Mark initMatrixClient as optional

* Improve on types

* Ensure we do not set the account data twice.

* lint

* Review changes

* Ensure we include the client on rendered messages.

* Fix test

* update labels

* clean designs

* update settings tab

* update snapshot

* copyright

* prevent mutation
2025-04-22 09:37:47 +00:00

270 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
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 OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen } from "jest-matrix-react";
import parse from "html-react-parser";
import { bodyToHtml, bodyToNode, formatEmojis, topicToHtml } from "../../src/HtmlUtils";
import SettingsStore from "../../src/settings/SettingsStore";
import { getMockClientWithEventEmitter } from "../test-utils";
import { SettingLevel } from "../../src/settings/SettingLevel";
import SdkConfig from "../../src/SdkConfig";
describe("topicToHtml", () => {
afterEach(() => {
SettingsStore.reset();
});
function getContent() {
return screen.getByRole("contentinfo").children[0].innerHTML;
}
it("converts plain text topic to HTML", () => {
render(<div role="contentinfo">{topicToHtml("pizza", undefined, null, false)}</div>);
expect(getContent()).toEqual("pizza");
});
it("converts plain text topic with emoji to HTML", () => {
render(<div role="contentinfo">{topicToHtml("pizza 🍕", undefined, null, false)}</div>);
expect(getContent()).toEqual('pizza <span class="mx_Emoji" title=":pizza:">🍕</span>');
});
it("converts literal HTML topic to HTML", async () => {
SettingsStore.setValue("feature_html_topic", null, SettingLevel.DEVICE, true);
render(<div role="contentinfo">{topicToHtml("<b>pizza</b>", undefined, null, false)}</div>);
expect(getContent()).toEqual("&lt;b&gt;pizza&lt;/b&gt;");
});
it("converts true HTML topic to HTML", async () => {
SettingsStore.setValue("feature_html_topic", null, SettingLevel.DEVICE, true);
render(<div role="contentinfo">{topicToHtml("**pizza**", "<b>pizza</b>", null, false)}</div>);
expect(getContent()).toEqual("<b>pizza</b>");
});
it("converts true HTML topic with emoji to HTML", async () => {
SettingsStore.setValue("feature_html_topic", null, SettingLevel.DEVICE, true);
render(<div role="contentinfo">{topicToHtml("**pizza** 🍕", "<b>pizza</b> 🍕", null, false)}</div>);
expect(getContent()).toEqual('<b>pizza</b> <span class="mx_Emoji" title=":pizza:">🍕</span>');
});
});
describe("bodyToHtml", () => {
it("should apply highlights to HTML messages", () => {
const html = bodyToHtml(
{
body: "test **foo** bar",
msgtype: "m.text",
formatted_body: "test <b>foo</b> bar",
format: "org.matrix.custom.html",
},
["test"],
);
expect(html).toMatchInlineSnapshot(`"<span class="mx_EventTile_searchHighlight">test</span> <b>foo</b> bar"`);
});
it("should apply highlights to plaintext messages", () => {
const html = bodyToHtml(
{
body: "test foo bar",
msgtype: "m.text",
},
["test"],
);
expect(html).toMatchInlineSnapshot(`"<span class="mx_EventTile_searchHighlight">test</span> foo bar"`);
});
it("should not respect HTML tags in plaintext message highlighting", () => {
const html = bodyToHtml(
{
body: "test foo <b>bar",
msgtype: "m.text",
},
["test"],
);
expect(html).toMatchInlineSnapshot(`"<span class="mx_EventTile_searchHighlight">test</span> foo &lt;b&gt;bar"`);
});
it("does not mistake characters in text presentation mode for emoji", () => {
const { asFragment } = render(
<span className="mx_EventTile_body translate" dir="auto">
{parse(bodyToHtml({ body: "↔ ❗︎", msgtype: "m.text" }, [], {}))}
</span>,
);
expect(asFragment()).toMatchSnapshot();
});
describe("feature_latex_maths", () => {
beforeEach(() => {
SettingsStore.setValue("feature_latex_maths", null, SettingLevel.DEVICE, true);
});
afterEach(() => {
SettingsStore.reset();
SdkConfig.reset();
});
it("should render inline katex", () => {
const html = bodyToHtml(
{
body: "hello \\xi world",
msgtype: "m.text",
formatted_body: 'hello <span data-mx-maths="\\xi"><code>\\xi</code></span> world',
format: "org.matrix.custom.html",
},
[],
);
expect(html).toMatchSnapshot();
});
it("should render block katex", () => {
const html = bodyToHtml(
{
body: "hello \\xi world",
msgtype: "m.text",
formatted_body: '<p>hello</p><div data-mx-maths="\\xi"><code>\\xi</code></div><p>world</p>',
format: "org.matrix.custom.html",
},
[],
);
expect(html).toMatchSnapshot();
});
it("should not mangle code blocks", () => {
const html = bodyToHtml(
{
body: "hello \\xi world",
msgtype: "m.text",
formatted_body: "<p>hello</p><pre><code>$\\xi$</code></pre><p>world</p>",
format: "org.matrix.custom.html",
},
[],
);
expect(html).toMatchSnapshot();
});
it("should not mangle divs", () => {
const html = bodyToHtml(
{
body: "hello world",
msgtype: "m.text",
formatted_body: "<p>hello</p><div>world</div>",
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}:`);
}
});
});
describe("bodyToNode", () => {
it("generates big emoji for emoji made of multiple characters", () => {
const { className, emojiBodyElements } = bodyToNode(
{
body: "👨‍👩‍👧‍👦 ↔️ 🇮🇸",
msgtype: "m.text",
},
[],
{
stripReplyFallback: true,
},
);
const { asFragment } = render(
<span className={className} dir="auto">
{emojiBodyElements}
</span>,
);
expect(asFragment()).toMatchSnapshot();
});
it("should generate big emoji for an emoji-only reply to a message", () => {
const { className, formattedBody } = bodyToNode(
{
"body": "> <@sender1:server> Test\n\n🥰",
"format": "org.matrix.custom.html",
"formatted_body":
'<mx-reply><blockquote><a href="https://matrix.to/#/!roomId:server/$eventId">In reply to</a> <a href="https://matrix.to/#/@sender1:server">@sender1:server</a><br>Test</blockquote></mx-reply>🥰',
"m.relates_to": {
"m.in_reply_to": {
event_id: "$eventId",
},
},
"msgtype": "m.text",
},
[],
{
stripReplyFallback: true,
},
);
const { asFragment } = render(
<span className={className} dir="auto" dangerouslySetInnerHTML={{ __html: formattedBody! }} />,
);
expect(asFragment()).toMatchSnapshot();
});
it.each([[true], [false]])("should handle inline media when mediaIsVisible is %s", (mediaIsVisible) => {
const cli = getMockClientWithEventEmitter({
mxcUrlToHttp: jest.fn().mockReturnValue("https://example.org/img"),
});
const { className, formattedBody } = bodyToNode(
{
"body": "![foo](mxc://going/knowwhere) Hello there",
"format": "org.matrix.custom.html",
"formatted_body": `<img src="mxc://going/knowwhere">foo</img> Hello there`,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$eventId",
},
},
"msgtype": "m.text",
},
[],
{
mediaIsVisible,
},
);
const { asFragment } = render(
<span className={className} dir="auto" dangerouslySetInnerHTML={{ __html: formattedBody! }} />,
);
expect(asFragment()).toMatchSnapshot();
// We do not want to download untrusted media.
// eslint-disable-next-line no-restricted-properties
expect(cli.mxcUrlToHttp).toHaveBeenCalledTimes(mediaIsVisible ? 1 : 0);
});
afterEach(() => {
jest.resetAllMocks();
});
});