Migrate more tests to vitest (#34349)
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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 { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import React from "react";
|
||||
|
||||
import { formatList, formatCount, formatCountLong } from "./FormattingUtils";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
|
||||
vi.mock("../dispatcher/dispatcher");
|
||||
|
||||
describe("FormattingUtils", () => {
|
||||
describe("formatCount", () => {
|
||||
it.each([
|
||||
{ count: 999, expectedCount: "999" },
|
||||
{ count: 9999, expectedCount: "10K" },
|
||||
{ count: 99999, expectedCount: "100K" },
|
||||
{ count: 999999, expectedCount: "1M" },
|
||||
{ count: 9999999, expectedCount: "10M" },
|
||||
{ count: 99999999, expectedCount: "100M" },
|
||||
{ count: 999999999, expectedCount: "1B" },
|
||||
{ count: 9999999999, expectedCount: "10B" },
|
||||
])("formats $count as $expectedCount", ({ count, expectedCount }) => {
|
||||
expect(formatCount(count)).toBe(expectedCount);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatCountLong", () => {
|
||||
it("formats numbers according to the locale", () => {
|
||||
expect(formatCountLong(1000)).toBe("1,000");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatList", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.spyOn(SettingsStore, "getValue").mockReturnValue("en-GB");
|
||||
});
|
||||
|
||||
it("should return empty string when given empty list", () => {
|
||||
expect(formatList([])).toEqual("");
|
||||
});
|
||||
|
||||
it("should return only item when given list of length 1", () => {
|
||||
expect(formatList(["abc"])).toEqual("abc");
|
||||
});
|
||||
|
||||
it("should return expected sentence in English without item limit", () => {
|
||||
expect(formatList(["abc", "def", "ghi"])).toEqual("abc, def and ghi");
|
||||
});
|
||||
|
||||
it("should return expected sentence in German without item limit", () => {
|
||||
vi.spyOn(SettingsStore, "getValue").mockReturnValue("de");
|
||||
expect(formatList(["abc", "def", "ghi"])).toEqual("abc, def und ghi");
|
||||
});
|
||||
|
||||
it("should return expected sentence in English with item limit", () => {
|
||||
expect(formatList(["abc", "def", "ghi", "jkl"], 2)).toEqual("abc, def and 2 others");
|
||||
expect(formatList(["abc", "def", "ghi", "jkl"], 3)).toEqual("abc, def, ghi and one other");
|
||||
});
|
||||
|
||||
it("should return expected sentence in English with item limit and includeCount", () => {
|
||||
expect(formatList(["abc", "def", "ghi", "jkl"], 3, true)).toEqual("abc, def and 2 others");
|
||||
expect(formatList(["abc", "def", "ghi", "jkl"], 4, true)).toEqual("abc, def, ghi and jkl");
|
||||
});
|
||||
|
||||
it("should return expected sentence in ReactNode when given 2 React children", () => {
|
||||
expect(formatList([<span key="a">a</span>, <span key="b">b</span>])).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should return expected sentence in ReactNode when given more React children", () => {
|
||||
expect(
|
||||
formatList([
|
||||
<span key="a">a</span>,
|
||||
<span key="b">b</span>,
|
||||
<span key="c">c</span>,
|
||||
<span key="d">d</span>,
|
||||
]),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should return expected sentence in ReactNode when using itemLimit", () => {
|
||||
expect(
|
||||
formatList(
|
||||
[<span key="a">a</span>, <span key="b">b</span>, <span key="c">c</span>, <span key="d">d</span>],
|
||||
2,
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render } from "test-utils-rtl";
|
||||
|
||||
import type { IContent } from "matrix-js-sdk/src/matrix";
|
||||
import type React from "react";
|
||||
import { editBodyDiffToHtml } from "./MessageDiffUtils";
|
||||
|
||||
describe("editBodyDiffToHtml", () => {
|
||||
function buildContent(message: string): IContent {
|
||||
return {
|
||||
body: message,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: message,
|
||||
msgtype: "m.text",
|
||||
};
|
||||
}
|
||||
|
||||
function renderDiff(before: string, after: string) {
|
||||
const node = editBodyDiffToHtml(buildContent(before), buildContent(after));
|
||||
|
||||
return render(node as React.ReactElement);
|
||||
}
|
||||
|
||||
it.each([
|
||||
["simple word changes", "hello", "world"],
|
||||
["central word changes", "beginning middle end", "beginning :smile: end"],
|
||||
["text deletions", "<b>hello</b> world", "<b>hello</b>"],
|
||||
["text additions", "<b>hello</b>", "<b>hello</b> world"],
|
||||
["block element additions", "hello", "hello <p>world</p>"],
|
||||
["inline element additions", "hello", "hello <q>world</q>"],
|
||||
["block element deletions", `hi <blockquote>there</blockquote>`, "hi"],
|
||||
["inline element deletions", `hi <em>there</em>`, "hi"],
|
||||
["element replacements", `hi <i>there</i>`, "hi <em>there</em>"],
|
||||
["attribute modifications", `<a href="#hi">hi</a>`, `<a href="#bye">hi</a>`],
|
||||
["attribute deletions", `<a href="#hi">hi</a>`, `<a>hi</a>`],
|
||||
["attribute additions", `<a>hi</a>`, `<a href="#/room/!123">hi</a>`],
|
||||
["handles empty tags", `<a>hi</a>`, `<a><h1></h1></a> hi`],
|
||||
])("renders %s", (_label, before, after) => {
|
||||
const { container } = renderDiff(before, after);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// see https://github.com/fiduswriter/diffDOM/issues/90
|
||||
// fixed in diff-dom in 4.2.2+
|
||||
it("deduplicates diff steps", () => {
|
||||
const { container } = renderDiff("<div><em>foo</em> bar baz</div>", "<div><em>foo</em> bar bay</div>");
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("handles non-html input", () => {
|
||||
const before: IContent = {
|
||||
body: "who knows what's going on <strong>here</strong>",
|
||||
format: "org.exotic.encoding",
|
||||
formatted_body: "who knows what's going on <strong>here</strong>",
|
||||
msgtype: "m.text",
|
||||
};
|
||||
|
||||
const after: IContent = {
|
||||
...before,
|
||||
body: "who knows what's going on <strong>there</strong>",
|
||||
formatted_body: "who knows what's going on <strong>there</strong>",
|
||||
};
|
||||
|
||||
const { container } = render(editBodyDiffToHtml(before, after) as React.ReactElement);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// see https://github.com/vector-im/element-web/issues/23665
|
||||
it("handles complex transformations", () => {
|
||||
const { container } = renderDiff(
|
||||
'<span data-mx-maths="{☃️}^\\infty"><code>{☃️}^\\infty</code></span>',
|
||||
'<span data-mx-maths="{😃}^\\infty"><code>{😃}^\\infty</code></span>',
|
||||
);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`FormattingUtils > formatList > should return expected sentence in ReactNode when given 2 React children 1`] = `
|
||||
<React.Fragment>
|
||||
<React.Fragment>
|
||||
<span>
|
||||
a
|
||||
</span>
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
and
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
<span>
|
||||
b
|
||||
</span>
|
||||
</React.Fragment>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`FormattingUtils > formatList > should return expected sentence in ReactNode when given more React children 1`] = `
|
||||
<React.Fragment>
|
||||
<React.Fragment>
|
||||
<span>
|
||||
a
|
||||
</span>
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
,
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
<span>
|
||||
b
|
||||
</span>
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
,
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
<span>
|
||||
c
|
||||
</span>
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
and
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
<span>
|
||||
d
|
||||
</span>
|
||||
</React.Fragment>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`FormattingUtils > formatList > should return expected sentence in ReactNode when using itemLimit 1`] = `
|
||||
<span>
|
||||
<React.Fragment>
|
||||
<React.Fragment>
|
||||
<span>
|
||||
a
|
||||
</span>
|
||||
,
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
<span>
|
||||
b
|
||||
</span>
|
||||
</React.Fragment>
|
||||
</React.Fragment>
|
||||
and 2 others
|
||||
</span>
|
||||
`;
|
||||
@@ -0,0 +1,500 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`editBodyDiffToHtml > deduplicates diff steps 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<div>
|
||||
<em>
|
||||
foo
|
||||
</em>
|
||||
<span>
|
||||
bar ba
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
z
|
||||
</span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
y
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`editBodyDiffToHtml > handles complex transformations 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
<span
|
||||
data-mx-maths="{<span class='mx_Emoji' title=':snowman:'>☃️</span>}^\\infty"
|
||||
>
|
||||
<code>
|
||||
{
|
||||
<span
|
||||
class="mx_Emoji"
|
||||
title=":snowman:"
|
||||
>
|
||||
☃️
|
||||
</span>
|
||||
}^\\infty
|
||||
</code>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
<span
|
||||
data-mx-maths="{<span class='mx_Emoji' title=':smiley:'>😃</span>}^\\infty"
|
||||
>
|
||||
<code>
|
||||
{
|
||||
<span
|
||||
class="mx_Emoji"
|
||||
title=":snowman:"
|
||||
>
|
||||
☃️
|
||||
</span>
|
||||
}^\\infty
|
||||
</code>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`editBodyDiffToHtml > handles non-html input 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<span>
|
||||
who knows what's going on <strong>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
t
|
||||
</span>
|
||||
here</strong>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`editBodyDiffToHtml > renders attribute additions 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
hi
|
||||
</a>
|
||||
</span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
<a
|
||||
href="undefined"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
hi
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
<span
|
||||
target="undefined"
|
||||
>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
hi
|
||||
</a>
|
||||
</span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
<a
|
||||
href="undefined"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
hi
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`editBodyDiffToHtml > renders attribute deletions 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
<a
|
||||
href="#hi"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
hi
|
||||
</a>
|
||||
</span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
hi
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
<a
|
||||
href="#hi"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
hi
|
||||
</a>
|
||||
</span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
hi
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`editBodyDiffToHtml > renders attribute modifications 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
<a
|
||||
href="#hi"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
hi
|
||||
</a>
|
||||
</span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
<a
|
||||
href="#bye"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
hi
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`editBodyDiffToHtml > renders block element additions 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<span>
|
||||
hello
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
<p>
|
||||
world
|
||||
</p>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`editBodyDiffToHtml > renders block element deletions 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<span>
|
||||
hi
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
<blockquote>
|
||||
there
|
||||
</blockquote>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`editBodyDiffToHtml > renders central word changes 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<span>
|
||||
beginning
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
:s
|
||||
</span>
|
||||
mi
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
dd
|
||||
</span>
|
||||
le
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
:
|
||||
</span>
|
||||
end
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`editBodyDiffToHtml > renders element replacements 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
hi
|
||||
<span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
<i>
|
||||
there
|
||||
</i>
|
||||
</span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
<em>
|
||||
there
|
||||
</em>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`editBodyDiffToHtml > renders handles empty tags 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
hi
|
||||
</span>
|
||||
<div
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
<h1 />
|
||||
</div>
|
||||
</span>
|
||||
</a>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
hi
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`editBodyDiffToHtml > renders inline element additions 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<span>
|
||||
hello
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
world
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`editBodyDiffToHtml > renders inline element deletions 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<span>
|
||||
hi
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
<em>
|
||||
there
|
||||
</em>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`editBodyDiffToHtml > renders simple word changes 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
hello
|
||||
</span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
world
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`editBodyDiffToHtml > renders text additions 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<b>
|
||||
hello
|
||||
</b>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
world
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`editBodyDiffToHtml > renders text deletions 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<b>
|
||||
hello
|
||||
</b>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
world
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,32 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`createVoiceMessageContent > should create a voice message content 1`] = `
|
||||
{
|
||||
"body": "Voice message",
|
||||
"file": {},
|
||||
"info": {
|
||||
"duration": 23000,
|
||||
"mimetype": "ogg/opus",
|
||||
"size": 42000,
|
||||
},
|
||||
"msgtype": "m.audio",
|
||||
"org.matrix.msc1767.audio": {
|
||||
"duration": 23000,
|
||||
"waveform": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
],
|
||||
},
|
||||
"org.matrix.msc1767.file": {
|
||||
"file": {},
|
||||
"mimetype": "ogg/opus",
|
||||
"name": "Voice message.ogg",
|
||||
"size": 42000,
|
||||
"url": "mxc://example.com/file",
|
||||
},
|
||||
"org.matrix.msc1767.text": "Voice message",
|
||||
"org.matrix.msc3245.voice": {},
|
||||
"url": "mxc://example.com/file",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
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 { describe, it, expect } from "vitest";
|
||||
import { type EncryptedFile } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { createVoiceMessageContent } from "./createVoiceMessageContent";
|
||||
|
||||
describe("createVoiceMessageContent", () => {
|
||||
it("should create a voice message content", () => {
|
||||
expect(
|
||||
createVoiceMessageContent(
|
||||
"mxc://example.com/file",
|
||||
"ogg/opus",
|
||||
23000,
|
||||
42000,
|
||||
{} as unknown as EncryptedFile,
|
||||
[1, 2, 3],
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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 { describe, it, expect } from "vitest";
|
||||
|
||||
import { PermalinkParts } from "./PermalinkConstructor";
|
||||
import MatrixSchemePermalinkConstructor from "./MatrixSchemePermalinkConstructor";
|
||||
|
||||
describe("MatrixSchemePermalinkConstructor", () => {
|
||||
const peramlinkConstructor = new MatrixSchemePermalinkConstructor();
|
||||
|
||||
describe("parsePermalink", () => {
|
||||
it("should strip ?action=chat from user links", () => {
|
||||
expect(peramlinkConstructor.parsePermalink("matrix:u/user:example.com?action=chat")).toEqual(
|
||||
new PermalinkParts(null, null, "@user:example.com", null),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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 { describe, it, expect } from "vitest";
|
||||
|
||||
import MatrixToPermalinkConstructor from "./MatrixToPermalinkConstructor";
|
||||
import { PermalinkParts } from "./PermalinkConstructor";
|
||||
|
||||
describe("MatrixToPermalinkConstructor", () => {
|
||||
const peramlinkConstructor = new MatrixToPermalinkConstructor();
|
||||
|
||||
describe("parsePermalink", () => {
|
||||
it.each([
|
||||
["empty URL", ""],
|
||||
["something that is not an URL", "hello"],
|
||||
["should raise an error for a non-matrix.to URL", "https://example.com/#/@user:example.com"],
|
||||
])("should raise an error for %s", (name: string, url: string) => {
|
||||
expect(() => peramlinkConstructor.parsePermalink(url)).toThrow(
|
||||
new Error("Does not appear to be a permalink"),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["(https)", "https://matrix.to/#/@user:example.com"],
|
||||
["(http)", "http://matrix.to/#/@user:example.com"],
|
||||
["without protocol", "matrix.to/#/@user:example.com"],
|
||||
])("should parse an MXID %s", (name: string, url: string) => {
|
||||
expect(peramlinkConstructor.parsePermalink(url)).toEqual(
|
||||
new PermalinkParts(null, null, "@user:example.com", null),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("forRoom", () => {
|
||||
it("constructs a link given a room ID and via servers", () => {
|
||||
expect(peramlinkConstructor.forRoom("!myroom:example.com", ["one.example.com", "two.example.com"])).toEqual(
|
||||
"https://matrix.to/#/!myroom:example.com?via=one.example.com&via=two.example.com",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("forEvent", () => {
|
||||
it("constructs a link given an event ID, room ID and via servers", () => {
|
||||
expect(
|
||||
peramlinkConstructor.forEvent("!myroom:example.com", "$event4", ["one.example.com", "two.example.com"]),
|
||||
).toEqual("https://matrix.to/#/!myroom:example.com/$event4?via=one.example.com&via=two.example.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,459 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019-2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018 New Vector 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 { vi, describe, it, expect, afterAll, beforeEach } from "vitest";
|
||||
import { getMockClientWithEventEmitter } from "test-utils/client";
|
||||
|
||||
import { type EventEmitter } from "events";
|
||||
import { Room, RoomMember, EventType, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import { PermalinkParts } from "./PermalinkConstructor";
|
||||
import { makeRoomPermalink, makeUserPermalink, parsePermalink, RoomPermalinkCreator } from "./Permalinks";
|
||||
import { type IConfigOptions } from "../../IConfigOptions";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
|
||||
describe("Permalinks", function () {
|
||||
const userId = "@test:example.com";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
getUserId: vi.fn().mockReturnValue(userId),
|
||||
getRoom: vi.fn(),
|
||||
});
|
||||
mockClient.credentials = { userId };
|
||||
|
||||
const makeMemberWithPL = (roomId: Room["roomId"], userId: string, powerLevel: number): RoomMember => {
|
||||
const member = new RoomMember(roomId, userId);
|
||||
member.powerLevel = powerLevel;
|
||||
return member;
|
||||
};
|
||||
|
||||
function mockRoom(
|
||||
roomId: Room["roomId"],
|
||||
members: RoomMember[],
|
||||
serverACLContent?: { deny?: string[]; allow?: string[] },
|
||||
): Room {
|
||||
members.forEach((m) => (m.membership = KnownMembership.Join));
|
||||
const powerLevelsUsers = members.reduce<Record<string, number>>((pl, member) => {
|
||||
if (Number.isFinite(member.powerLevel)) {
|
||||
pl[member.userId] = member.powerLevel;
|
||||
}
|
||||
return pl;
|
||||
}, {});
|
||||
|
||||
const room = new Room(roomId, mockClient, userId);
|
||||
|
||||
const powerLevels = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
room_id: roomId,
|
||||
state_key: "",
|
||||
content: {
|
||||
users: powerLevelsUsers,
|
||||
users_default: 0,
|
||||
},
|
||||
});
|
||||
const serverACL = serverACLContent
|
||||
? new MatrixEvent({
|
||||
type: EventType.RoomServerAcl,
|
||||
room_id: roomId,
|
||||
state_key: "",
|
||||
content: serverACLContent,
|
||||
})
|
||||
: undefined;
|
||||
const stateEvents = serverACL ? [powerLevels, serverACL] : [powerLevels];
|
||||
room.currentState.setStateEvents(stateEvents);
|
||||
|
||||
vi.spyOn(room, "getCanonicalAlias").mockReturnValue(null);
|
||||
vi.spyOn(room, "getJoinedMembers").mockReturnValue(members);
|
||||
vi.spyOn(room, "getMember").mockImplementation((userId) => members.find((m) => m.userId === userId) || null);
|
||||
|
||||
return room;
|
||||
}
|
||||
beforeEach(function () {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.spyOn(MatrixClientPeg, "get").mockRestore();
|
||||
});
|
||||
|
||||
it("should not clean up listeners even if start was called multiple times", () => {
|
||||
const room = mockRoom("!fake:example.org", []);
|
||||
const getListenerCount = (emitter: EventEmitter) =>
|
||||
emitter
|
||||
.eventNames()
|
||||
.map((e) => emitter.listenerCount(e))
|
||||
.reduce((a, b) => a + b, 0);
|
||||
const listenerCountBefore = getListenerCount(room.currentState);
|
||||
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.start();
|
||||
creator.start();
|
||||
creator.start();
|
||||
creator.start();
|
||||
expect(getListenerCount(room.currentState)).toBeGreaterThan(listenerCountBefore);
|
||||
|
||||
creator.stop();
|
||||
expect(getListenerCount(room.currentState)).toBe(listenerCountBefore);
|
||||
});
|
||||
|
||||
it("should pick no candidate servers when the room has no members", function () {
|
||||
const room = mockRoom("!fake:example.org", []);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator.serverCandidates).toBeTruthy();
|
||||
expect(creator.serverCandidates!.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should gracefully handle invalid MXIDs", () => {
|
||||
const roomId = "!fake:example.org";
|
||||
const alice50 = makeMemberWithPL(roomId, "@alice:pl-50:org", 50);
|
||||
const room = mockRoom(roomId, [alice50]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator.serverCandidates).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should pick a candidate server for the highest power level user in the room", function () {
|
||||
const roomId = "!fake:example.org";
|
||||
const alice50 = makeMemberWithPL(roomId, "@alice:pl_50", 50);
|
||||
const alice75 = makeMemberWithPL(roomId, "@alice:pl_75", 75);
|
||||
const alice95 = makeMemberWithPL(roomId, "@alice:pl_95", 95);
|
||||
const room = mockRoom("!fake:example.org", [alice50, alice75, alice95]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator.serverCandidates).toBeTruthy();
|
||||
expect(creator.serverCandidates!.length).toBe(3);
|
||||
expect(creator.serverCandidates![0]).toBe("pl_95");
|
||||
// we don't check the 2nd and 3rd servers because that is done by the next test
|
||||
});
|
||||
|
||||
it("should change candidate server when highest power level user leaves the room", function () {
|
||||
const roomId = "!fake:example.org";
|
||||
const member95 = makeMemberWithPL(roomId, "@alice:pl_95", 95);
|
||||
|
||||
const room = mockRoom(roomId, [
|
||||
makeMemberWithPL(roomId, "@alice:pl_50", 50),
|
||||
makeMemberWithPL(roomId, "@alice:pl_75", 75),
|
||||
member95,
|
||||
]);
|
||||
const creator = new RoomPermalinkCreator(room, null);
|
||||
creator.load();
|
||||
expect(creator.serverCandidates![0]).toBe("pl_95");
|
||||
member95.membership = KnownMembership.Leave;
|
||||
// @ts-ignore illegal private property
|
||||
creator.onRoomStateUpdate();
|
||||
expect(creator.serverCandidates![0]).toBe("pl_75");
|
||||
member95.membership = KnownMembership.Join;
|
||||
// @ts-ignore illegal private property
|
||||
creator.onRoomStateUpdate();
|
||||
expect(creator.serverCandidates![0]).toBe("pl_95");
|
||||
});
|
||||
|
||||
it("should pick candidate servers based on user population", function () {
|
||||
const roomId = "!fake:example.org";
|
||||
const room = mockRoom(roomId, [
|
||||
makeMemberWithPL(roomId, "@alice:first", 0),
|
||||
makeMemberWithPL(roomId, "@bob:first", 0),
|
||||
makeMemberWithPL(roomId, "@charlie:first", 0),
|
||||
makeMemberWithPL(roomId, "@alice:second", 0),
|
||||
makeMemberWithPL(roomId, "@bob:second", 0),
|
||||
makeMemberWithPL(roomId, "@charlie:third", 0),
|
||||
]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator.serverCandidates).toBeTruthy();
|
||||
expect(creator.serverCandidates!.length).toBe(3);
|
||||
expect(creator.serverCandidates![0]).toBe("first");
|
||||
expect(creator.serverCandidates![1]).toBe("second");
|
||||
expect(creator.serverCandidates![2]).toBe("third");
|
||||
});
|
||||
|
||||
it("should pick prefer candidate servers with higher power levels", function () {
|
||||
const roomId = "!fake:example.org";
|
||||
const room = mockRoom(roomId, [
|
||||
makeMemberWithPL(roomId, "@alice:first", 100),
|
||||
makeMemberWithPL(roomId, "@alice:second", 0),
|
||||
makeMemberWithPL(roomId, "@bob:second", 0),
|
||||
makeMemberWithPL(roomId, "@charlie:third", 0),
|
||||
]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator.serverCandidates!.length).toBe(3);
|
||||
expect(creator.serverCandidates![0]).toBe("first");
|
||||
expect(creator.serverCandidates![1]).toBe("second");
|
||||
expect(creator.serverCandidates![2]).toBe("third");
|
||||
});
|
||||
|
||||
it("should pick a maximum of 3 candidate servers", function () {
|
||||
const roomId = "!fake:example.org";
|
||||
const room = mockRoom(roomId, [
|
||||
makeMemberWithPL(roomId, "@alice:alpha", 100),
|
||||
makeMemberWithPL(roomId, "@alice:bravo", 0),
|
||||
makeMemberWithPL(roomId, "@alice:charlie", 0),
|
||||
makeMemberWithPL(roomId, "@alice:delta", 0),
|
||||
makeMemberWithPL(roomId, "@alice:echo", 0),
|
||||
]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator.serverCandidates).toBeTruthy();
|
||||
expect(creator.serverCandidates!.length).toBe(3);
|
||||
});
|
||||
|
||||
it("should not consider IPv4 hosts", function () {
|
||||
const roomId = "!fake:example.org";
|
||||
const room = mockRoom(roomId, [makeMemberWithPL(roomId, "@alice:127.0.0.1", 100)]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator.serverCandidates).toBeTruthy();
|
||||
expect(creator.serverCandidates!.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should not consider IPv6 hosts", function () {
|
||||
const roomId = "!fake:example.org";
|
||||
const room = mockRoom(roomId, [makeMemberWithPL(roomId, "@alice:[::1]", 100)]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator.serverCandidates).toBeTruthy();
|
||||
expect(creator.serverCandidates!.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should not consider IPv4 hostnames with ports", function () {
|
||||
const roomId = "!fake:example.org";
|
||||
const room = mockRoom(roomId, [makeMemberWithPL(roomId, "@alice:127.0.0.1:8448", 100)]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator.serverCandidates).toBeTruthy();
|
||||
expect(creator.serverCandidates!.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should not consider IPv6 hostnames with ports", function () {
|
||||
const roomId = "!fake:example.org";
|
||||
const room = mockRoom(roomId, [makeMemberWithPL(roomId, "@alice:[::1]:8448", 100)]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator.serverCandidates).toBeTruthy();
|
||||
expect(creator.serverCandidates!.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should work with hostnames with ports", function () {
|
||||
const roomId = "!fake:example.org";
|
||||
const room = mockRoom(roomId, [makeMemberWithPL(roomId, "@alice:example.org:8448", 100)]);
|
||||
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator.serverCandidates).toBeTruthy();
|
||||
expect(creator.serverCandidates!.length).toBe(1);
|
||||
expect(creator.serverCandidates![0]).toBe("example.org:8448");
|
||||
});
|
||||
|
||||
it("should not consider servers explicitly denied by ACLs", function () {
|
||||
const roomId = "!fake:example.org";
|
||||
const room = mockRoom(
|
||||
roomId,
|
||||
[
|
||||
makeMemberWithPL(roomId, "@alice:evilcorp.com", 100),
|
||||
makeMemberWithPL(roomId, "@bob:chat.evilcorp.com", 0),
|
||||
],
|
||||
{
|
||||
deny: ["evilcorp.com", "*.evilcorp.com"],
|
||||
allow: ["*"],
|
||||
},
|
||||
);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator.serverCandidates).toBeTruthy();
|
||||
expect(creator.serverCandidates!.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should not consider servers not allowed by ACLs", function () {
|
||||
const roomId = "!fake:example.org";
|
||||
const room = mockRoom(
|
||||
roomId,
|
||||
[
|
||||
makeMemberWithPL(roomId, "@alice:evilcorp.com", 100),
|
||||
makeMemberWithPL(roomId, "@bob:chat.evilcorp.com", 0),
|
||||
],
|
||||
{
|
||||
deny: [],
|
||||
allow: [], // implies "ban everyone"
|
||||
},
|
||||
);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator.serverCandidates).toBeTruthy();
|
||||
expect(creator.serverCandidates!.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should consider servers not explicitly banned by ACLs", function () {
|
||||
const roomId = "!fake:example.org";
|
||||
const room = mockRoom(
|
||||
roomId,
|
||||
[
|
||||
makeMemberWithPL(roomId, "@alice:evilcorp.com", 100),
|
||||
makeMemberWithPL(roomId, "@bob:chat.evilcorp.com", 0),
|
||||
],
|
||||
{
|
||||
deny: ["*.evilcorp.com"], // evilcorp.com is still good though
|
||||
allow: ["*"],
|
||||
},
|
||||
);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator.serverCandidates).toBeTruthy();
|
||||
expect(creator.serverCandidates!.length).toBe(1);
|
||||
expect(creator.serverCandidates![0]).toEqual("evilcorp.com");
|
||||
});
|
||||
|
||||
it("should consider servers not disallowed by ACLs", function () {
|
||||
const roomId = "!fake:example.org";
|
||||
const room = mockRoom(
|
||||
"!fake:example.org",
|
||||
[
|
||||
makeMemberWithPL(roomId, "@alice:evilcorp.com", 100),
|
||||
makeMemberWithPL(roomId, "@bob:chat.evilcorp.com", 0),
|
||||
],
|
||||
{
|
||||
deny: [],
|
||||
allow: ["evilcorp.com"], // implies "ban everyone else"
|
||||
},
|
||||
);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
expect(creator.serverCandidates).toBeTruthy();
|
||||
expect(creator.serverCandidates!.length).toBe(1);
|
||||
expect(creator.serverCandidates![0]).toEqual("evilcorp.com");
|
||||
});
|
||||
|
||||
it("should handle when ACL allow is not an array", function () {
|
||||
const roomId = "!fake:example.org";
|
||||
const room = mockRoom(roomId, [makeMemberWithPL(roomId, "@alice:goodcorp.com", 100)], {
|
||||
deny: ["*.evilcorp.com"],
|
||||
allow: "not-an-array" as any, // Test malformed data
|
||||
});
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
// Should fall back to default behavior (no allowed servers list = allow none)
|
||||
expect(creator.serverCandidates).toBeTruthy();
|
||||
expect(creator.serverCandidates!.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle when ACL deny is not an array", function () {
|
||||
const roomId = "!fake:example.org";
|
||||
const room = mockRoom(roomId, [makeMemberWithPL(roomId, "@alice:goodcorp.com", 100)], {
|
||||
deny: "not-an-array" as any, // Test malformed data
|
||||
allow: ["*"],
|
||||
});
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
// Should not crash and still allow the server
|
||||
expect(creator.serverCandidates).toBeTruthy();
|
||||
expect(creator.serverCandidates!.length).toBe(1);
|
||||
expect(creator.serverCandidates![0]).toEqual("goodcorp.com");
|
||||
});
|
||||
|
||||
it("should generate an event permalink for room IDs with no candidate servers", function () {
|
||||
const room = mockRoom("!somewhere:example.org", []);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
const result = creator.forEvent("$something:example.com");
|
||||
expect(result).toBe("https://matrix.to/#/!somewhere:example.org/$something:example.com");
|
||||
});
|
||||
|
||||
it("should generate an event permalink for room IDs with some candidate servers", function () {
|
||||
const roomId = "!somewhere:example.org";
|
||||
const room = mockRoom(roomId, [
|
||||
makeMemberWithPL(roomId, "@alice:first", 100),
|
||||
makeMemberWithPL(roomId, "@bob:second", 0),
|
||||
]);
|
||||
const creator = new RoomPermalinkCreator(room);
|
||||
creator.load();
|
||||
const result = creator.forEvent("$something:example.com");
|
||||
expect(result).toBe("https://matrix.to/#/!somewhere:example.org/$something:example.com?via=first&via=second");
|
||||
});
|
||||
|
||||
it("should generate a room permalink for room IDs with some candidate servers", function () {
|
||||
mockClient.getRoom.mockImplementation((roomId?: string) => {
|
||||
return mockRoom(roomId!, [
|
||||
makeMemberWithPL(roomId!, "@alice:first", 100),
|
||||
makeMemberWithPL(roomId!, "@bob:second", 0),
|
||||
]);
|
||||
});
|
||||
const result = makeRoomPermalink(mockClient, "!somewhere:example.org");
|
||||
expect(result).toBe("https://matrix.to/#/!somewhere:example.org?via=first&via=second");
|
||||
});
|
||||
|
||||
it("should generate a room permalink for room aliases with no candidate servers", function () {
|
||||
mockClient.getRoom.mockReturnValue(null);
|
||||
const result = makeRoomPermalink(mockClient, "#somewhere:example.org");
|
||||
expect(result).toBe("https://matrix.to/#/#somewhere:example.org");
|
||||
});
|
||||
|
||||
it("should generate a room permalink for room aliases without candidate servers", function () {
|
||||
mockClient.getRoom.mockImplementation((roomId?: string) => {
|
||||
return mockRoom(roomId!, [
|
||||
makeMemberWithPL(roomId!, "@alice:first", 100),
|
||||
makeMemberWithPL(roomId!, "@bob:second", 0),
|
||||
]);
|
||||
});
|
||||
const result = makeRoomPermalink(mockClient, "#somewhere:example.org");
|
||||
expect(result).toBe("https://matrix.to/#/#somewhere:example.org");
|
||||
});
|
||||
|
||||
it("should generate a user permalink", function () {
|
||||
const result = makeUserPermalink("@someone:example.org");
|
||||
expect(result).toBe("https://matrix.to/#/@someone:example.org");
|
||||
});
|
||||
|
||||
it("should use permalink_prefix for permalinks", function () {
|
||||
const sdkConfigGet = SdkConfig.get;
|
||||
vi.spyOn(SdkConfig, "get").mockImplementation((key: keyof IConfigOptions, altCaseName?: string) => {
|
||||
if (key === "permalink_prefix") {
|
||||
return "https://element.fs.tld";
|
||||
} else return sdkConfigGet(key, altCaseName);
|
||||
});
|
||||
const result = makeUserPermalink("@someone:example.org");
|
||||
expect(result).toBe("https://element.fs.tld/#/user/@someone:example.org");
|
||||
});
|
||||
|
||||
describe("parsePermalink", () => {
|
||||
it("should correctly parse room permalinks with a via argument", () => {
|
||||
const result = parsePermalink("https://matrix.to/#/!room_id:server?via=some.org");
|
||||
expect(result?.roomIdOrAlias).toBe("!room_id:server");
|
||||
expect(result?.viaServers).toEqual(["some.org"]);
|
||||
});
|
||||
|
||||
it("should correctly parse room permalink via arguments", () => {
|
||||
const result = parsePermalink("https://matrix.to/#/!room_id:server?via=foo.bar&via=bar.foo");
|
||||
expect(result?.roomIdOrAlias).toBe("!room_id:server");
|
||||
expect(result?.viaServers).toEqual(["foo.bar", "bar.foo"]);
|
||||
});
|
||||
|
||||
it("should correctly parse event permalink via arguments", () => {
|
||||
const result = parsePermalink(
|
||||
"https://matrix.to/#/!room_id:server/$event_id/some_thing_here/foobar" + "?via=m1.org&via=m2.org",
|
||||
);
|
||||
expect(result?.eventId).toBe("$event_id/some_thing_here/foobar");
|
||||
expect(result?.roomIdOrAlias).toBe("!room_id:server");
|
||||
expect(result?.viaServers).toEqual(["m1.org", "m2.org"]);
|
||||
});
|
||||
|
||||
it("should correctly parse permalinks with http protocol", () => {
|
||||
expect(parsePermalink("http://matrix.to/#/@user:example.com")).toEqual(
|
||||
new PermalinkParts(null, null, "@user:example.com", null),
|
||||
);
|
||||
});
|
||||
|
||||
it("should correctly parse permalinks without protocol", () => {
|
||||
expect(parsePermalink("matrix.to/#/@user:example.com")).toEqual(
|
||||
new PermalinkParts(null, null, "@user:example.com", null),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, type Mocked } from "vitest";
|
||||
import { type IIdentityServerProvider, type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { stubClient } from "test-utils";
|
||||
|
||||
import { DirectoryMember, ThreepidMember } from "./direct-messages";
|
||||
import { lookupThreePids, resolveThreePids } from "./threepids";
|
||||
|
||||
describe("threepids", () => {
|
||||
let client: Mocked<MatrixClient>;
|
||||
const accessToken = "s3cr3t";
|
||||
let identityServer: Mocked<IIdentityServerProvider>;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient() as Mocked<MatrixClient>;
|
||||
identityServer = {
|
||||
getAccessToken: vi.fn().mockResolvedValue(accessToken),
|
||||
} as unknown as Mocked<IIdentityServerProvider>;
|
||||
});
|
||||
|
||||
describe("resolveThreePids", () => {
|
||||
const userId = "@user1:example.com";
|
||||
const directoryMember = new DirectoryMember({
|
||||
user_id: userId,
|
||||
});
|
||||
|
||||
const threePid1Id = "three1@example.com";
|
||||
const threePid1MXID = "@three1:example.com";
|
||||
const threePid1Member = new ThreepidMember(threePid1Id);
|
||||
const threePid1Displayname = "Three Pid 1";
|
||||
const threePid2Id = "three2@example.com";
|
||||
const threePid2MXID = "@three2:example.com";
|
||||
const threePid2Member = new ThreepidMember(threePid2Id);
|
||||
const threePid3Id = "three3@example.com";
|
||||
const threePid3Member = new ThreepidMember(threePid3Id);
|
||||
const threePidPhoneId = "8801500121121";
|
||||
const threePidPhoneMember = new ThreepidMember(threePidPhoneId);
|
||||
|
||||
it("should return an empty list for an empty input", async () => {
|
||||
expect(await resolveThreePids([], client)).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return the same list for non-3rd-party members", async () => {
|
||||
expect(await resolveThreePids([directoryMember], client)).toEqual([directoryMember]);
|
||||
});
|
||||
|
||||
it("should return the same list for if no identity server is configured", async () => {
|
||||
expect(await resolveThreePids([directoryMember, threePid1Member], client)).toEqual([
|
||||
directoryMember,
|
||||
threePid1Member,
|
||||
]);
|
||||
});
|
||||
|
||||
describe("when an identity server is configured", () => {
|
||||
beforeEach(() => {
|
||||
client.identityServer = identityServer;
|
||||
});
|
||||
|
||||
it("should return the same list if the lookup doesn't return any results", async () => {
|
||||
expect(
|
||||
await resolveThreePids(
|
||||
[directoryMember, threePid1Member, threePid2Member, threePidPhoneMember],
|
||||
client,
|
||||
),
|
||||
).toEqual([directoryMember, threePid1Member, threePid2Member, threePidPhoneMember]);
|
||||
expect(client.bulkLookupThreePids).toHaveBeenCalledWith(
|
||||
[
|
||||
["email", threePid1Id],
|
||||
["email", threePid2Id],
|
||||
["msisdn", threePidPhoneId],
|
||||
],
|
||||
accessToken,
|
||||
);
|
||||
});
|
||||
|
||||
describe("and some 3-rd party members can be resolved", () => {
|
||||
beforeEach(() => {
|
||||
client.bulkLookupThreePids.mockResolvedValue({
|
||||
threepids: [
|
||||
["email", threePid1Id, threePid1MXID],
|
||||
["email", threePid2Id, threePid2MXID],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should return the resolved members", async () => {
|
||||
expect(
|
||||
await resolveThreePids(
|
||||
[directoryMember, threePid1Member, threePid2Member, threePid3Member],
|
||||
client,
|
||||
),
|
||||
).toEqual([
|
||||
directoryMember,
|
||||
new DirectoryMember({ user_id: threePid1MXID }),
|
||||
new DirectoryMember({ user_id: threePid2MXID }),
|
||||
threePid3Member,
|
||||
]);
|
||||
expect(client.bulkLookupThreePids).toHaveBeenCalledWith(
|
||||
[
|
||||
["email", threePid1Id],
|
||||
["email", threePid2Id],
|
||||
["email", threePid3Id],
|
||||
],
|
||||
accessToken,
|
||||
);
|
||||
});
|
||||
|
||||
describe("and some 3rd-party members have a profile", () => {
|
||||
beforeEach(() => {
|
||||
client.getProfileInfo.mockImplementation((matrixId: string) => {
|
||||
if (matrixId === threePid1MXID)
|
||||
return Promise.resolve({ displayname: threePid1Displayname });
|
||||
throw new Error("Profile not found");
|
||||
});
|
||||
});
|
||||
|
||||
it("should resolve the profiles", async () => {
|
||||
expect(
|
||||
await resolveThreePids(
|
||||
[directoryMember, threePid1Member, threePid2Member, threePid3Member],
|
||||
client,
|
||||
),
|
||||
).toEqual([
|
||||
directoryMember,
|
||||
new DirectoryMember({ user_id: threePid1MXID, display_name: threePid1Displayname }),
|
||||
new DirectoryMember({ user_id: threePid2MXID }),
|
||||
threePid3Member,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("lookupThreePids", () => {
|
||||
it("should return an empty list for an empty list", async () => {
|
||||
client.identityServer = identityServer;
|
||||
expect(await lookupThreePids([], client)).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import { type MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
|
||||
import { stubClient } from "test-utils";
|
||||
|
||||
import {
|
||||
clearUserStatus,
|
||||
fetchUserStatus,
|
||||
setUserStatus,
|
||||
userStatusFromProfile,
|
||||
userStatusTextWithinMaxLength,
|
||||
} from "./userStatus";
|
||||
|
||||
describe("userStatus utils", () => {
|
||||
describe("userStatusFromProfile", () => {
|
||||
it("returns the user status if it is valid", () => {
|
||||
expect(userStatusFromProfile({ emoji: "🐳", text: "Feeling a little blue" }, undefined)).toEqual({
|
||||
emoji: "🐳",
|
||||
text: "Feeling a little blue",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns undefined if the user status is invalid and there is no call status", () => {
|
||||
expect(userStatusFromProfile({ text: "Feeling a little blue" }, undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns the call status if the user status is invalid but the call status is valid", () => {
|
||||
expect(userStatusFromProfile({ text: "Feeling a little blue" }, { call_joined_ts: 12345 })).toEqual({
|
||||
emoji: "📞",
|
||||
text: "On a call",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers the user status over the call status if both are valid", () => {
|
||||
expect(
|
||||
userStatusFromProfile({ emoji: "🐳", text: "Feeling a little blue" }, { call_joined_ts: 12345 }),
|
||||
).toEqual({
|
||||
emoji: "🐳",
|
||||
text: "Feeling a little blue",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns undefined if the call status has a non-positive call_joined_ts", () => {
|
||||
expect(userStatusFromProfile(undefined, { call_joined_ts: 0 })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined if neither status is valid", () => {
|
||||
expect(userStatusFromProfile(undefined, undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("userStatusTextWithinMaxLength", () => {
|
||||
it("returns true for text within the max length", () => {
|
||||
const text = "a".repeat(256);
|
||||
expect(userStatusTextWithinMaxLength(text)).toBe(true);
|
||||
});
|
||||
it("returns false for text exceeding the max length", () => {
|
||||
const text = "a".repeat(257);
|
||||
expect(userStatusTextWithinMaxLength(text)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setUserStatus", () => {
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
});
|
||||
|
||||
it("sets the user status with valid input", async () => {
|
||||
setUserStatus(client, { emoji: "🐳", text: "Feeling a little blue" });
|
||||
|
||||
expect(client.setExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.status", {
|
||||
emoji: "🐳",
|
||||
text: "Feeling a little blue",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchUserStatus", () => {
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
client.doesServerSupportExtendedProfiles = vi.fn();
|
||||
});
|
||||
|
||||
it("returns undefined if the server does not support extended profiles", async () => {
|
||||
vi.mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(false);
|
||||
|
||||
await expect(fetchUserStatus(client, "@alice:example.com")).resolves.toBeUndefined();
|
||||
expect(client.getExtendedProfileProperty).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns the validated status if the server supports extended profiles and has a status set", async () => {
|
||||
vi.mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(true);
|
||||
vi.mocked(client.getExtendedProfileProperty).mockResolvedValue({
|
||||
emoji: "🐳",
|
||||
text: "Feeling a little blue",
|
||||
});
|
||||
|
||||
await expect(fetchUserStatus(client, "@alice:example.com")).resolves.toEqual({
|
||||
emoji: "🐳",
|
||||
text: "Feeling a little blue",
|
||||
});
|
||||
expect(client.getExtendedProfileProperty).toHaveBeenCalledWith(
|
||||
"@alice:example.com",
|
||||
"org.matrix.msc4426.status",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns undefined if the status is invalid", async () => {
|
||||
vi.mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(true);
|
||||
vi.mocked(client.getExtendedProfileProperty).mockResolvedValue({ text: "Feeling a little blue" });
|
||||
|
||||
await expect(fetchUserStatus(client, "@alice:example.com")).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined if the user has no status set", async () => {
|
||||
vi.mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(true);
|
||||
vi.mocked(client.getExtendedProfileProperty).mockRejectedValue(
|
||||
new MatrixError({ errcode: "M_NOT_FOUND" }, 404),
|
||||
);
|
||||
|
||||
await expect(fetchUserStatus(client, "@alice:example.com")).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined and logs a warning if fetching the status fails unexpectedly", async () => {
|
||||
vi.mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(true);
|
||||
const error = new Error("network error");
|
||||
vi.mocked(client.getExtendedProfileProperty).mockRejectedValue(error);
|
||||
|
||||
await expect(fetchUserStatus(client, "@alice:example.com")).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearUserStatus", () => {
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
});
|
||||
|
||||
it("clears the user status", async () => {
|
||||
clearUserStatus(client);
|
||||
|
||||
expect(client.setExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.status", null);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user