Files
ThreadNet-Web/apps/web/test/unit-tests/HtmlUtils-test.tsx
T
sorB 3da363517f
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
feat: show call participants in room list (Discord-style)
2026-05-10 14:25:35 +02:00

322 lines
12 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", () => {
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 () => {
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 () => {
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 () => {
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("should linkify and hightlight parts of links in plaintext message highlighting", () => {
getMockClientWithEventEmitter({});
const html = bodyToHtml(
{
body: "foo http://link.example/test/path bar",
msgtype: "m.text",
},
["test"],
{
linkify: true,
},
);
expect(html).toMatchInlineSnapshot(
`"foo <a href="http://link.example/test/path" target="_blank" rel="noreferrer noopener" data-linkified="true">http://link.example/<span class="mx_EventTile_searchHighlight">test</span>/path</a> bar"`,
);
});
it("should hightlight parts of links in HTML message highlighting", () => {
const html = bodyToHtml(
{
body: "foo http://link.example/test/path bar",
msgtype: "m.text",
formatted_body: 'foo <a href="http://link.example/test/path">http://link.example/test/path</a> bar',
format: "org.matrix.custom.html",
},
["test"],
{
linkify: true,
},
);
expect(html).toMatchInlineSnapshot(
`"foo <a href="http://link.example/test/path" target="_blank" rel="noreferrer noopener">http://link.example/<span class="mx_EventTile_searchHighlight">test</span>/path</a> bar"`,
);
});
it("should ignore data-linkified in incoming links but should be applied to linkified links", () => {
getMockClientWithEventEmitter({});
const html = bodyToHtml(
{
body: "foo http://link.example/test/path bar",
msgtype: "m.text",
formatted_body:
'foo <a data-linkfied="true" href="http://link.example/test/path">http://link.example/test/path</a> bar with https://example.org',
format: "org.matrix.custom.html",
},
[],
{
linkify: true,
},
);
expect(html).toMatchInlineSnapshot(
`"foo <a href="http://link.example/test/path" target="_blank" rel="noreferrer noopener">http://link.example/test/path</a> bar with <a href="https://example.org" target="_blank" rel="noreferrer noopener" data-linkified="true">https://example.org</a>"`,
);
});
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();
});
});