Migrate more jest tests to vitest (#33922)
* Migrate more jest tests to vitest * Fix jest config * Fix jest config * Make remaining jest tests type-happy * Iterate * Fix Notifier import cycle * Fix tests * Delint * Iterate * Handle SDKContextClass `client` initialisation internally Rather than via MatrixChat - this is predominantly for Lifecycle tests as they don't use a MatrixChat and it doesn't make much sense for this component to own this state. * Fix tests * Iterate * Simplify diff * Improve coverage * Improve coverage * Iterate
This commit is contained in:
@@ -10,7 +10,9 @@ import React, { type ReactElement } from "react";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { render, type RenderOptions } from "@testing-library/react";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { I18nContext } from "@element-hq/web-shared-components";
|
||||
import { I18nApi, I18nContext } from "@element-hq/web-shared-components";
|
||||
|
||||
const i18nApi = new I18nApi();
|
||||
|
||||
/**
|
||||
* Wraps the provided components in:
|
||||
@@ -26,7 +28,7 @@ const wrapWithStandardContexts = (Wrapper: RenderOptions["wrapper"]) => {
|
||||
if (Wrapper) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<I18nContext.Provider value={window.mxModuleApi.i18n}>
|
||||
<I18nContext.Provider value={i18nApi}>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
</I18nContext.Provider>
|
||||
</Wrapper>
|
||||
@@ -34,7 +36,7 @@ const wrapWithStandardContexts = (Wrapper: RenderOptions["wrapper"]) => {
|
||||
} else {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<I18nContext.Provider value={window.mxModuleApi.i18n}>{children}</I18nContext.Provider>
|
||||
<I18nContext.Provider value={i18nApi}>{children}</I18nContext.Provider>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,386 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
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 {
|
||||
formatSeconds,
|
||||
formatRelativeTime,
|
||||
formatDuration,
|
||||
formatFullDateNoDayISO,
|
||||
formatTimeLeft,
|
||||
formatPreciseDuration,
|
||||
formatLocalDateShort,
|
||||
getDaysArray,
|
||||
getMonthsArray,
|
||||
formatFullDateNoDayNoTime,
|
||||
formatTime,
|
||||
formatFullTime,
|
||||
formatFullDate,
|
||||
formatFullDateNoTime,
|
||||
formatDate,
|
||||
HOUR_MS,
|
||||
MINUTE_MS,
|
||||
DAY_MS,
|
||||
} from "../../../src/DateUtils";
|
||||
import { REPEATABLE_DATE, mockIntlDateTimeFormat, unmockIntlDateTimeFormat } from "../../test-utils";
|
||||
import * as languageSettings from "../../../src/i18n/settings";
|
||||
|
||||
describe("getDaysArray", () => {
|
||||
it("should return Sunday-Saturday in long mode", () => {
|
||||
expect(getDaysArray("long")).toMatchInlineSnapshot(`
|
||||
[
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
]
|
||||
`);
|
||||
});
|
||||
it("should return Sun-Sat in short mode", () => {
|
||||
expect(getDaysArray("short")).toMatchInlineSnapshot(`
|
||||
[
|
||||
"Sun",
|
||||
"Mon",
|
||||
"Tue",
|
||||
"Wed",
|
||||
"Thu",
|
||||
"Fri",
|
||||
"Sat",
|
||||
]
|
||||
`);
|
||||
});
|
||||
it("should return S-S in narrow mode", () => {
|
||||
expect(getDaysArray("narrow")).toMatchInlineSnapshot(`
|
||||
[
|
||||
"S",
|
||||
"M",
|
||||
"T",
|
||||
"W",
|
||||
"T",
|
||||
"F",
|
||||
"S",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMonthsArray", () => {
|
||||
it("should return January-December in long mode", () => {
|
||||
expect(getMonthsArray("long")).toMatchInlineSnapshot(`
|
||||
[
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
]
|
||||
`);
|
||||
});
|
||||
it("should return Jan-Dec in short mode", () => {
|
||||
expect(getMonthsArray("short")).toMatchInlineSnapshot(`
|
||||
[
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
]
|
||||
`);
|
||||
});
|
||||
it("should return J-D in narrow mode", () => {
|
||||
expect(getMonthsArray("narrow")).toMatchInlineSnapshot(`
|
||||
[
|
||||
"J",
|
||||
"F",
|
||||
"M",
|
||||
"A",
|
||||
"M",
|
||||
"J",
|
||||
"J",
|
||||
"A",
|
||||
"S",
|
||||
"O",
|
||||
"N",
|
||||
"D",
|
||||
]
|
||||
`);
|
||||
});
|
||||
it("should return 1-12 in numeric mode", () => {
|
||||
expect(getMonthsArray("numeric")).toMatchInlineSnapshot(`
|
||||
[
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"11",
|
||||
"12",
|
||||
]
|
||||
`);
|
||||
});
|
||||
it("should return 01-12 in 2-digit mode", () => {
|
||||
expect(getMonthsArray("2-digit")).toMatchInlineSnapshot(`
|
||||
[
|
||||
"01",
|
||||
"02",
|
||||
"03",
|
||||
"04",
|
||||
"05",
|
||||
"06",
|
||||
"07",
|
||||
"08",
|
||||
"09",
|
||||
"10",
|
||||
"11",
|
||||
"12",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDate", () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(REPEATABLE_DATE);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.setSystemTime(jest.getRealSystemTime());
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("should return time string if date is within same day", () => {
|
||||
const date = new Date(REPEATABLE_DATE.getTime() + 2 * HOUR_MS + 12 * MINUTE_MS);
|
||||
// We use en-US for these tests because there was a change in Node 22.12 which removed
|
||||
// the comma after the weekday for en-GB which makes the test output different things
|
||||
// on different node versions. I'm not sure what a better fix would be, so let's just use
|
||||
// a locale that happens to have a more stable formatting right now.
|
||||
expect(formatDate(date, false, "en-US")).toMatchInlineSnapshot(`"19:10"`);
|
||||
});
|
||||
|
||||
it("should return time string with weekday if date is within last 6 days", () => {
|
||||
const date = new Date(REPEATABLE_DATE.getTime() - 6 * DAY_MS + 2 * HOUR_MS + 12 * MINUTE_MS);
|
||||
expect(formatDate(date, false, "en-US")).toMatchInlineSnapshot(`"Fri 19:10"`);
|
||||
});
|
||||
|
||||
it("should return time & date string without year if it is within the same year", () => {
|
||||
const date = new Date(REPEATABLE_DATE.getTime() - 66 * DAY_MS + 2 * HOUR_MS + 12 * MINUTE_MS);
|
||||
expect(formatDate(date, false, "en-US")).toMatchInlineSnapshot(`"Mon, Sep 12, 19:10"`);
|
||||
});
|
||||
|
||||
it("should return full time & date string otherwise", () => {
|
||||
const date = new Date(REPEATABLE_DATE.getTime() - 666 * DAY_MS + 2 * HOUR_MS + 12 * MINUTE_MS);
|
||||
expect(formatDate(date, false, "en-US")).toMatchInlineSnapshot(`"Wed, Jan 20, 2021, 19:10"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatFullDateNoTime", () => {
|
||||
it("should match given locale en-GB", () => {
|
||||
expect(formatFullDateNoTime(REPEATABLE_DATE, "en-GB")).toMatchInlineSnapshot(`"Thu, 17 Nov 2022"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatFullDate", () => {
|
||||
it("correctly formats with seconds", () => {
|
||||
expect(formatFullDate(REPEATABLE_DATE, true, true, "en-GB")).toMatchInlineSnapshot(
|
||||
`"Thu, 17 Nov 2022, 4:58:32 pm"`,
|
||||
);
|
||||
});
|
||||
it("correctly formats without seconds", () => {
|
||||
expect(formatFullDate(REPEATABLE_DATE, false, false, "en-GB")).toMatchInlineSnapshot(
|
||||
`"Thu, 17 Nov 2022, 16:58"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatFullTime", () => {
|
||||
it("correctly formats 12 hour mode", () => {
|
||||
expect(formatFullTime(REPEATABLE_DATE, true, "en-GB")).toMatchInlineSnapshot(`"4:58:32 pm"`);
|
||||
});
|
||||
it("correctly formats 24 hour mode", () => {
|
||||
expect(formatFullTime(REPEATABLE_DATE, false, "en-GB")).toMatchInlineSnapshot(`"16:58:32"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatTime", () => {
|
||||
it("correctly formats 12 hour mode", () => {
|
||||
expect(formatTime(REPEATABLE_DATE, true, "en-GB")).toMatchInlineSnapshot(`"4:58 pm"`);
|
||||
});
|
||||
it("correctly formats 24 hour mode", () => {
|
||||
expect(formatTime(REPEATABLE_DATE, false, "en-GB")).toMatchInlineSnapshot(`"16:58"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSeconds", () => {
|
||||
it("correctly formats time with hours", () => {
|
||||
expect(formatSeconds(60 * 60 * 3 + 60 * 31 + 55)).toBe("03:31:55");
|
||||
expect(formatSeconds(60 * 60 * 3 + 60 * 0 + 55)).toBe("03:00:55");
|
||||
expect(formatSeconds(60 * 60 * 3 + 60 * 31 + 0)).toBe("03:31:00");
|
||||
expect(formatSeconds(-(60 * 60 * 3 + 60 * 31 + 0))).toBe("-03:31:00");
|
||||
});
|
||||
|
||||
it("correctly formats time without hours", () => {
|
||||
expect(formatSeconds(60 * 60 * 0 + 60 * 31 + 55)).toBe("31:55");
|
||||
expect(formatSeconds(60 * 60 * 0 + 60 * 0 + 55)).toBe("00:55");
|
||||
expect(formatSeconds(60 * 60 * 0 + 60 * 31 + 0)).toBe("31:00");
|
||||
expect(formatSeconds(-(60 * 60 * 0 + 60 * 31 + 0))).toBe("-31:00");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatRelativeTime", () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
// Tuesday, 2 November 2021 11:18:03 UTC
|
||||
jest.setSystemTime(1635851883000);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.setSystemTime(jest.getRealSystemTime());
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("returns hour format for events created in the same day", () => {
|
||||
// Tuesday, 2 November 2021 11:01:00 UTC
|
||||
const date = new Date(2021, 10, 2, 11, 1, 23, 0);
|
||||
expect(formatRelativeTime(date)).toBe("11:01");
|
||||
});
|
||||
|
||||
it("returns month and day for events created less than 24h ago but on a different day", () => {
|
||||
// Monday, 1 November 2021 23:01:00 UTC
|
||||
const date = new Date(2021, 10, 1, 23, 1, 23, 0);
|
||||
expect(formatRelativeTime(date)).toBe("Nov 1");
|
||||
});
|
||||
|
||||
it("honours the hour format setting", () => {
|
||||
const date = new Date(2021, 10, 2, 11, 1, 23, 0);
|
||||
expect(formatRelativeTime(date)).toBe("11:01");
|
||||
expect(formatRelativeTime(date, false)).toBe("11:01");
|
||||
expect(formatRelativeTime(date, true)).toBe("11:01 AM");
|
||||
});
|
||||
|
||||
it("returns month and day for events created in the current year", () => {
|
||||
const date = new Date(1632567741000);
|
||||
expect(formatRelativeTime(date, true)).toBe("Sep 25");
|
||||
});
|
||||
|
||||
it("does not return a leading 0 for single digit days", () => {
|
||||
const date = new Date(1635764541000);
|
||||
expect(formatRelativeTime(date, true)).toBe("Nov 1");
|
||||
});
|
||||
|
||||
it("appends the year for events created in previous years", () => {
|
||||
const date = new Date(1604142141000);
|
||||
expect(formatRelativeTime(date, true)).toBe("Oct 31, 2020");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDuration()", () => {
|
||||
type TestCase = [string, string, number];
|
||||
|
||||
const MINUTE_MS = 60000;
|
||||
const HOUR_MS = MINUTE_MS * 60;
|
||||
|
||||
it.each<TestCase>([
|
||||
["rounds up to nearest day when more than 24h - 40 hours", "2d", 40 * HOUR_MS],
|
||||
["rounds down to nearest day when more than 24h - 26 hours", "1d", 26 * HOUR_MS],
|
||||
["24 hours", "1d", 24 * HOUR_MS],
|
||||
["rounds to nearest hour when less than 24h - 23h", "23h", 23 * HOUR_MS],
|
||||
["rounds to nearest hour when less than 24h - 6h and 10min", "6h", 6 * HOUR_MS + 10 * MINUTE_MS],
|
||||
["rounds to nearest hours when less than 24h", "2h", 2 * HOUR_MS + 124234],
|
||||
["rounds to nearest minute when less than 1h - 59 minutes", "59m", 59 * MINUTE_MS],
|
||||
["rounds to nearest minute when less than 1h - 1 minute", "1m", MINUTE_MS],
|
||||
["rounds to nearest second when less than 1min - 59 seconds", "59s", 59000],
|
||||
["rounds to 0 seconds when less than a second - 123ms", "0s", 123],
|
||||
])("%s formats to %s", (_description, expectedResult, input) => {
|
||||
expect(formatDuration(input)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatPreciseDuration", () => {
|
||||
const MINUTE_MS = 1000 * 60;
|
||||
const HOUR_MS = MINUTE_MS * 60;
|
||||
const DAY_MS = HOUR_MS * 24;
|
||||
|
||||
it.each<[string, string, number]>([
|
||||
["3 days, 6 hours, 48 minutes, 59 seconds", "3d 6h 48m 59s", 3 * DAY_MS + 6 * HOUR_MS + 48 * MINUTE_MS + 59000],
|
||||
["6 hours, 48 minutes, 59 seconds", "6h 48m 59s", 6 * HOUR_MS + 48 * MINUTE_MS + 59000],
|
||||
["48 minutes, 59 seconds", "48m 59s", 48 * MINUTE_MS + 59000],
|
||||
["59 seconds", "59s", 59000],
|
||||
["0 seconds", "0s", 0],
|
||||
])("%s formats to %s", (_description, expectedResult, input) => {
|
||||
expect(formatPreciseDuration(input)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatFullDateNoDayISO", () => {
|
||||
it("should return ISO format", () => {
|
||||
expect(formatFullDateNoDayISO(REPEATABLE_DATE)).toEqual("2022-11-17T16:58:32.517Z");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatFullDateNoDayNoTime", () => {
|
||||
it("should return a date formatted for en-GB locale", () => {
|
||||
expect(formatFullDateNoDayNoTime(REPEATABLE_DATE, "en-GB")).toMatchInlineSnapshot(`"17/11/2022"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatTimeLeft", () => {
|
||||
it.each([
|
||||
[0, "0s left"],
|
||||
[23, "23s left"],
|
||||
[60 + 23, "1m 23s left"],
|
||||
[60 * 60, "1h 0m 0s left"],
|
||||
[60 * 60 + 23, "1h 0m 23s left"],
|
||||
[5 * 60 * 60 + 7 * 60 + 23, "5h 7m 23s left"],
|
||||
])("should format %s to %s", (seconds: number, expected: string) => {
|
||||
expect(formatTimeLeft(seconds)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatLocalDateShort()", () => {
|
||||
afterAll(() => {
|
||||
unmockIntlDateTimeFormat();
|
||||
});
|
||||
const timestamp = new Date("Fri Dec 17 2021 09:09:00 GMT+0100 (Central European Standard Time)").getTime();
|
||||
it("formats date correctly by locale", () => {
|
||||
const locale = jest.spyOn(languageSettings, "getUserLanguage");
|
||||
mockIntlDateTimeFormat();
|
||||
|
||||
// format is DD/MM/YY
|
||||
locale.mockReturnValue("en-GB");
|
||||
expect(formatLocalDateShort(timestamp)).toEqual("17/12/21");
|
||||
|
||||
// US date format is MM/DD/YY
|
||||
locale.mockReturnValue("en-US");
|
||||
expect(formatLocalDateShort(timestamp)).toEqual("12/17/21");
|
||||
|
||||
locale.mockReturnValue("de-DE");
|
||||
expect(formatLocalDateShort(timestamp)).toEqual("17.12.21");
|
||||
});
|
||||
});
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
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 { type ReactElement } from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
import { MatrixError, ConnectionError } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
adminContactStrings,
|
||||
messageForConnectionError,
|
||||
messageForLoginError,
|
||||
messageForResourceLimitError,
|
||||
messageForSyncError,
|
||||
resourceLimitStrings,
|
||||
} from "../../../src/utils/ErrorUtils";
|
||||
|
||||
describe("messageForResourceLimitError", () => {
|
||||
it("should match snapshot for monthly_active_user", () => {
|
||||
const { asFragment } = render(
|
||||
messageForResourceLimitError("monthly_active_user", "some@email", resourceLimitStrings) as ReactElement,
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot for admin contact links", () => {
|
||||
const { asFragment } = render(
|
||||
messageForResourceLimitError("", "some@email", adminContactStrings) as ReactElement,
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("messageForSyncError", () => {
|
||||
it("should match snapshot for M_RESOURCE_LIMIT_EXCEEDED", () => {
|
||||
const err = new MatrixError({
|
||||
errcode: "M_RESOURCE_LIMIT_EXCEEDED",
|
||||
data: {
|
||||
limit_type: "monthly_active_user",
|
||||
admin_contact: "some@email",
|
||||
},
|
||||
});
|
||||
const { asFragment } = render(messageForSyncError(err) as ReactElement);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot for other errors", () => {
|
||||
const err = new MatrixError({
|
||||
errcode: "OTHER_ERROR",
|
||||
});
|
||||
const { asFragment } = render(messageForSyncError(err) as ReactElement);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("messageForLoginError", () => {
|
||||
it("should match snapshot for M_RESOURCE_LIMIT_EXCEEDED", () => {
|
||||
const err = new MatrixError({
|
||||
errcode: "M_RESOURCE_LIMIT_EXCEEDED",
|
||||
data: {
|
||||
limit_type: "monthly_active_user",
|
||||
admin_contact: "some@email",
|
||||
},
|
||||
});
|
||||
const { asFragment } = render(
|
||||
messageForLoginError(err, {
|
||||
hsUrl: "hsUrl",
|
||||
hsName: "hsName",
|
||||
}) as ReactElement,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot for M_USER_DEACTIVATED", () => {
|
||||
const err = new MatrixError(
|
||||
{
|
||||
errcode: "M_USER_DEACTIVATED",
|
||||
},
|
||||
403,
|
||||
);
|
||||
const { asFragment } = render(
|
||||
messageForLoginError(err, {
|
||||
hsUrl: "hsUrl",
|
||||
hsName: "hsName",
|
||||
}) as ReactElement,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot for 401", () => {
|
||||
const err = new MatrixError(
|
||||
{
|
||||
errcode: "UNKNOWN",
|
||||
},
|
||||
401,
|
||||
);
|
||||
const { asFragment } = render(
|
||||
messageForLoginError(err, {
|
||||
hsUrl: "hsUrl",
|
||||
hsName: "hsName",
|
||||
}) as ReactElement,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot for unknown error", () => {
|
||||
const err = new MatrixError({}, 400);
|
||||
const { asFragment } = render(
|
||||
messageForLoginError(err, {
|
||||
hsUrl: "hsUrl",
|
||||
hsName: "hsName",
|
||||
}) as ReactElement,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("messageForConnectionError", () => {
|
||||
it("should match snapshot for ConnectionError", () => {
|
||||
const err = new ConnectionError("Internal Server Error", new MatrixError({}, 500));
|
||||
const { asFragment } = render(
|
||||
messageForConnectionError(err, {
|
||||
hsUrl: "hsUrl",
|
||||
hsName: "hsName",
|
||||
}) as ReactElement,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot for MatrixError M_NOT_FOUND", () => {
|
||||
const err = new MatrixError(
|
||||
{
|
||||
errcode: "M_NOT_FOUND",
|
||||
},
|
||||
404,
|
||||
);
|
||||
const { asFragment } = render(
|
||||
messageForConnectionError(err, {
|
||||
hsUrl: "hsUrl",
|
||||
hsName: "hsName",
|
||||
}) as ReactElement,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot for unknown error", () => {
|
||||
const err = new Error("What even");
|
||||
const { asFragment } = render(
|
||||
messageForConnectionError(err, {
|
||||
hsUrl: "hsUrl",
|
||||
hsName: "hsName",
|
||||
}) as ReactElement,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot for mixed content error", () => {
|
||||
const err = new ConnectionError("Mixed content maybe?");
|
||||
Object.defineProperty(window, "location", { value: { protocol: "https:" } });
|
||||
const { asFragment } = render(
|
||||
messageForConnectionError(err, {
|
||||
hsUrl: "http://server.com",
|
||||
hsName: "hsName",
|
||||
}) as ReactElement,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,460 +0,0 @@
|
||||
/*
|
||||
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 {
|
||||
M_LOCATION,
|
||||
EventStatus,
|
||||
EventType,
|
||||
type IEvent,
|
||||
type MatrixClient,
|
||||
MatrixEvent,
|
||||
MsgType,
|
||||
PendingEventOrdering,
|
||||
RelationType,
|
||||
Room,
|
||||
Thread,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import {
|
||||
canCancel,
|
||||
canEditContent,
|
||||
canEditOwnEvent,
|
||||
fetchInitialEvent,
|
||||
findEditableEvent,
|
||||
highlightEvent,
|
||||
isContentActionable,
|
||||
isLocationEvent,
|
||||
isVoiceMessage,
|
||||
} from "../../../src/utils/EventUtils";
|
||||
import { getMockClientWithEventEmitter, makeBeaconInfoEvent, makePollStartEvent, stubClient } from "../../test-utils";
|
||||
import dis from "../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
|
||||
jest.mock("../../../src/dispatcher/dispatcher");
|
||||
|
||||
describe("EventUtils", () => {
|
||||
const userId = "@user:server";
|
||||
const roomId = "!room:server";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.getUserId.mockClear().mockReturnValue(userId);
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.spyOn(MatrixClientPeg, "get").mockRestore();
|
||||
});
|
||||
|
||||
// setup events
|
||||
const unsentEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
});
|
||||
unsentEvent.status = EventStatus.ENCRYPTING;
|
||||
|
||||
const redactedEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
});
|
||||
redactedEvent.makeRedacted(
|
||||
redactedEvent,
|
||||
new Room(redactedEvent.getRoomId()!, mockClient, mockClient.getUserId()!),
|
||||
);
|
||||
|
||||
const stateEvent = new MatrixEvent({
|
||||
type: EventType.RoomTopic,
|
||||
state_key: "",
|
||||
});
|
||||
const beaconInfoEvent = makeBeaconInfoEvent(userId, roomId);
|
||||
|
||||
const roomMemberEvent = new MatrixEvent({
|
||||
type: EventType.RoomMember,
|
||||
sender: userId,
|
||||
});
|
||||
|
||||
const stickerEvent = new MatrixEvent({
|
||||
type: EventType.Sticker,
|
||||
sender: userId,
|
||||
});
|
||||
|
||||
const pollStartEvent = makePollStartEvent("What?", userId);
|
||||
|
||||
const notDecryptedEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
msgtype: "m.bad.encrypted",
|
||||
},
|
||||
});
|
||||
|
||||
const noMsgType = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
msgtype: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const noContentBody = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
msgtype: MsgType.Image,
|
||||
},
|
||||
});
|
||||
|
||||
const emptyContentBody = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "",
|
||||
},
|
||||
});
|
||||
|
||||
const objectContentBody = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
msgtype: MsgType.File,
|
||||
body: {},
|
||||
},
|
||||
});
|
||||
|
||||
const niceTextMessage = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Hello",
|
||||
},
|
||||
});
|
||||
|
||||
const bobsTextMessage = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: "@bob:server",
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Hello from Bob",
|
||||
},
|
||||
});
|
||||
|
||||
describe("isContentActionable()", () => {
|
||||
type TestCase = [string, MatrixEvent];
|
||||
it.each<TestCase>([
|
||||
["unsent event", unsentEvent],
|
||||
["redacted event", redactedEvent],
|
||||
["state event", stateEvent],
|
||||
["undecrypted event", notDecryptedEvent],
|
||||
["room member event", roomMemberEvent],
|
||||
["event without msgtype", noMsgType],
|
||||
["event without content body property", noContentBody],
|
||||
])("returns false for %s", (_description, event) => {
|
||||
expect(isContentActionable(event)).toBe(false);
|
||||
});
|
||||
|
||||
it.each<TestCase>([
|
||||
["sticker event", stickerEvent],
|
||||
["poll start event", pollStartEvent],
|
||||
["event with empty content body", emptyContentBody],
|
||||
["event with a content body", niceTextMessage],
|
||||
["beacon_info event", beaconInfoEvent],
|
||||
])("returns true for %s", (_description, event) => {
|
||||
expect(isContentActionable(event)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("editable content helpers", () => {
|
||||
const replaceRelationEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
"msgtype": MsgType.Text,
|
||||
"body": "Hello",
|
||||
["m.relates_to"]: {
|
||||
rel_type: RelationType.Replace,
|
||||
event_id: "1",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const referenceRelationEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
"msgtype": MsgType.Text,
|
||||
"body": "Hello",
|
||||
["m.relates_to"]: {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: "1",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emoteEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
msgtype: MsgType.Emote,
|
||||
body: "🧪",
|
||||
},
|
||||
});
|
||||
|
||||
type TestCase = [string, MatrixEvent];
|
||||
|
||||
const uneditableCases: TestCase[] = [
|
||||
["redacted event", redactedEvent],
|
||||
["state event", stateEvent],
|
||||
["event that is not room message", roomMemberEvent],
|
||||
["event without msgtype", noMsgType],
|
||||
["event without content body property", noContentBody],
|
||||
["event with empty content body property", emptyContentBody],
|
||||
["event with non-string body", objectContentBody],
|
||||
["event not sent by current user", bobsTextMessage],
|
||||
["event with a replace relation", replaceRelationEvent],
|
||||
];
|
||||
|
||||
const editableCases: TestCase[] = [
|
||||
["event with reference relation", referenceRelationEvent],
|
||||
["emote event", emoteEvent],
|
||||
["poll start event", pollStartEvent],
|
||||
["event with a content body", niceTextMessage],
|
||||
];
|
||||
|
||||
describe("canEditContent()", () => {
|
||||
it.each<TestCase>(uneditableCases)("returns false for %s", (_description, event) => {
|
||||
expect(canEditContent(mockClient, event)).toBe(false);
|
||||
});
|
||||
|
||||
it.each<TestCase>(editableCases)("returns true for %s", (_description, event) => {
|
||||
expect(canEditContent(mockClient, event)).toBe(true);
|
||||
});
|
||||
});
|
||||
describe("canEditOwnContent()", () => {
|
||||
it.each<TestCase>(uneditableCases)("returns false for %s", (_description, event) => {
|
||||
expect(canEditOwnEvent(mockClient, event)).toBe(false);
|
||||
});
|
||||
|
||||
it.each<TestCase>(editableCases)("returns true for %s", (_description, event) => {
|
||||
expect(canEditOwnEvent(mockClient, event)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isVoiceMessage()", () => {
|
||||
it("returns true for an event with msc2516.voice content", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
["org.matrix.msc2516.voice"]: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(isVoiceMessage(event)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for an event with msc3245.voice content", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
["org.matrix.msc3245.voice"]: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(isVoiceMessage(event)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for an event with voice content", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "hello",
|
||||
},
|
||||
});
|
||||
|
||||
expect(isVoiceMessage(event)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isLocationEvent()", () => {
|
||||
it("returns true for an event with m.location stable type", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: M_LOCATION.altName,
|
||||
});
|
||||
expect(isLocationEvent(event)).toBe(true);
|
||||
});
|
||||
it("returns true for an event with m.location unstable prefixed type", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: M_LOCATION.name,
|
||||
});
|
||||
expect(isLocationEvent(event)).toBe(true);
|
||||
});
|
||||
it("returns true for a room message with stable m.location msgtype", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
msgtype: M_LOCATION.altName,
|
||||
},
|
||||
});
|
||||
expect(isLocationEvent(event)).toBe(true);
|
||||
});
|
||||
it("returns true for a room message with unstable m.location msgtype", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
msgtype: M_LOCATION.name,
|
||||
},
|
||||
});
|
||||
expect(isLocationEvent(event)).toBe(true);
|
||||
});
|
||||
it("returns false for a non location event", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "Hello",
|
||||
},
|
||||
});
|
||||
expect(isLocationEvent(event)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("canCancel()", () => {
|
||||
it.each([[EventStatus.QUEUED], [EventStatus.NOT_SENT], [EventStatus.ENCRYPTING]])(
|
||||
"return true for status %s",
|
||||
(status) => {
|
||||
expect(canCancel(status)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[EventStatus.SENDING],
|
||||
[EventStatus.CANCELLED],
|
||||
[EventStatus.SENT],
|
||||
["invalid-status" as unknown as EventStatus],
|
||||
])("return false for status %s", (status) => {
|
||||
expect(canCancel(status)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchInitialEvent", () => {
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
let room: Room;
|
||||
let client: MatrixClient;
|
||||
|
||||
const NORMAL_EVENT = "$normalEvent";
|
||||
const THREAD_ROOT = "$threadRoot";
|
||||
const THREAD_REPLY = "$threadReply";
|
||||
|
||||
const events: Record<string, Partial<IEvent>> = {
|
||||
[NORMAL_EVENT]: {
|
||||
event_id: NORMAL_EVENT,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "Classic event",
|
||||
msgtype: MsgType.Text,
|
||||
},
|
||||
},
|
||||
[THREAD_ROOT]: {
|
||||
event_id: THREAD_ROOT,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "Thread root",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
latest_event: {
|
||||
event_id: THREAD_REPLY,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
"body": "Thread reply",
|
||||
"msgtype": MsgType.Text,
|
||||
"m.relates_to": {
|
||||
event_id: "$threadRoot",
|
||||
rel_type: RelationType.Thread,
|
||||
},
|
||||
},
|
||||
},
|
||||
count: 1,
|
||||
current_user_participated: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[THREAD_REPLY]: {
|
||||
event_id: THREAD_REPLY,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
"body": "Thread reply",
|
||||
"msgtype": MsgType.Text,
|
||||
"m.relates_to": {
|
||||
event_id: THREAD_ROOT,
|
||||
rel_type: RelationType.Thread,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
client = MatrixClientPeg.safeGet();
|
||||
|
||||
room = new Room(ROOM_ID, client, client.getUserId()!, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
jest.spyOn(client, "supportsThreads").mockReturnValue(true);
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
jest.spyOn(client, "fetchRoomEvent").mockImplementation(async (roomId, eventId) => {
|
||||
return events[eventId] ?? Promise.reject();
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null for unknown events", async () => {
|
||||
expect(await fetchInitialEvent(client, room.roomId, "$UNKNOWN")).toBeNull();
|
||||
expect(await fetchInitialEvent(client, room.roomId, NORMAL_EVENT)).toBeInstanceOf(MatrixEvent);
|
||||
});
|
||||
|
||||
it("creates a thread when needed", async () => {
|
||||
await fetchInitialEvent(client, room.roomId, THREAD_REPLY);
|
||||
expect(room.getThread(THREAD_ROOT)).toBeInstanceOf(Thread);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findEditableEvent", () => {
|
||||
it("should not explode when given empty events array", () => {
|
||||
expect(
|
||||
findEditableEvent({
|
||||
events: [],
|
||||
isForward: true,
|
||||
matrixClient: mockClient,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("highlightEvent", () => {
|
||||
const eventId = "$zLg9jResFQmMO_UKFeWpgLgOgyWrL8qIgLgZ5VywrCQ";
|
||||
|
||||
it("should dispatch an action to view the event", () => {
|
||||
highlightEvent(roomId, eventId);
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
event_id: eventId,
|
||||
highlighted: true,
|
||||
room_id: roomId,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 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 { type MediaEventContent } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { downloadLabelForFile } from "../../../src/utils/FileUtils.ts";
|
||||
|
||||
describe("FileUtils", () => {
|
||||
describe("downloadLabelForFile", () => {
|
||||
it.each([
|
||||
[
|
||||
"File with size",
|
||||
{
|
||||
input: {
|
||||
msgtype: "m.file",
|
||||
body: "Test",
|
||||
info: {
|
||||
size: 102434566,
|
||||
},
|
||||
} as MediaEventContent,
|
||||
output: "Download (97.69 MB)",
|
||||
},
|
||||
],
|
||||
[
|
||||
"Image",
|
||||
{
|
||||
input: {
|
||||
msgtype: "m.image",
|
||||
body: "Test",
|
||||
} as MediaEventContent,
|
||||
output: "Download",
|
||||
},
|
||||
],
|
||||
[
|
||||
"Video",
|
||||
{
|
||||
input: {
|
||||
msgtype: "m.video",
|
||||
body: "Test",
|
||||
} as MediaEventContent,
|
||||
output: "Download",
|
||||
},
|
||||
],
|
||||
[
|
||||
"Audio",
|
||||
{
|
||||
input: {
|
||||
msgtype: "m.audio",
|
||||
body: "Test",
|
||||
} as MediaEventContent,
|
||||
output: "Download",
|
||||
},
|
||||
],
|
||||
])("should correctly label %s", (_d, { input, output }) => expect(downloadLabelForFile(input)).toBe(output));
|
||||
});
|
||||
});
|
||||
@@ -1,141 +0,0 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`messageForConnectionError should match snapshot for ConnectionError 1`] = `
|
||||
<DocumentFragment>
|
||||
<span>
|
||||
<span>
|
||||
Can't connect to homeserver - please check your connectivity, ensure your
|
||||
<a
|
||||
class="mx_ExternalLink"
|
||||
href="hsUrl"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
homeserver's SSL certificate
|
||||
<svg
|
||||
class="mx_ExternalLink_icon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
|
||||
/>
|
||||
<path
|
||||
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
is trusted, and that a browser extension is not blocking requests.
|
||||
</span>
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForConnectionError should match snapshot for MatrixError M_NOT_FOUND 1`] = `
|
||||
<DocumentFragment>
|
||||
There was a problem communicating with the homeserver, please try again later.(M_NOT_FOUND)
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForConnectionError should match snapshot for mixed content error 1`] = `
|
||||
<DocumentFragment>
|
||||
<span>
|
||||
<span>
|
||||
Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or
|
||||
<a
|
||||
href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
enable unsafe scripts
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForConnectionError should match snapshot for unknown error 1`] = `
|
||||
<DocumentFragment>
|
||||
There was a problem communicating with the homeserver, please try again later.
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForLoginError should match snapshot for 401 1`] = `
|
||||
<DocumentFragment>
|
||||
Incorrect username and/or password.
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForLoginError should match snapshot for M_RESOURCE_LIMIT_EXCEEDED 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<div>
|
||||
This homeserver has exceeded one of its resource limits.
|
||||
</div>
|
||||
<div
|
||||
class="mx_Login_smallError"
|
||||
>
|
||||
Please contact your service administrator to continue using this service.
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForLoginError should match snapshot for M_USER_DEACTIVATED 1`] = `
|
||||
<DocumentFragment>
|
||||
This account has been deactivated.
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForLoginError should match snapshot for unknown error 1`] = `
|
||||
<DocumentFragment>
|
||||
There was a problem communicating with the homeserver, please try again later. (HTTP 400)
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForResourceLimitError should match snapshot for admin contact links 1`] = `
|
||||
<DocumentFragment>
|
||||
<span>
|
||||
Please
|
||||
<a
|
||||
href="some@email"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
contact your service administrator
|
||||
</a>
|
||||
to continue using this service.
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForResourceLimitError should match snapshot for monthly_active_user 1`] = `
|
||||
<DocumentFragment>
|
||||
This homeserver has hit its Monthly Active User limit.
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForSyncError should match snapshot for M_RESOURCE_LIMIT_EXCEEDED 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<div>
|
||||
This homeserver has exceeded one of its resource limits.
|
||||
</div>
|
||||
<div>
|
||||
Please contact your service administrator to continue using this service.
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForSyncError should match snapshot for other errors 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
Unable to connect to Homeserver. Retrying…
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
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 { mocked, type Mocked } from "jest-mock-vitest-adapter";
|
||||
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import { mkRoom, resetAsyncStoreWithClient, setupAsyncStoreWithClient, stubClient } from "../../test-utils";
|
||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { type ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import { leaveRoomBehaviour } from "../../../src/utils/leave-behaviour";
|
||||
import { SDKContextClass } from "../../../src/contexts/SDKContextClass";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import SpaceStore from "../../../src/stores/spaces/SpaceStore";
|
||||
import { MetaSpace } from "../../../src/stores/spaces";
|
||||
import { type ActionPayload } from "../../../src/dispatcher/payloads";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import { CallStore } from "../../../src/stores/CallStore";
|
||||
import { type Call } from "../../../src/models/Call";
|
||||
import LegacyCallHandler from "../../../src/LegacyCallHandler";
|
||||
|
||||
describe("leaveRoomBehaviour", () => {
|
||||
SDKContextClass.instance.constructEagerStores(); // Initialize RoomViewStore
|
||||
|
||||
let client: Mocked<MatrixClient>;
|
||||
let room: Mocked<Room>;
|
||||
let space: Mocked<Room>;
|
||||
|
||||
beforeEach(async () => {
|
||||
stubClient();
|
||||
client = mocked(MatrixClientPeg.safeGet());
|
||||
DMRoomMap.makeShared(client);
|
||||
|
||||
room = mkRoom(client, "!1:example.org");
|
||||
space = mkRoom(client, "!2:example.org");
|
||||
space.isSpaceRoom.mockReturnValue(true);
|
||||
client.getRoom.mockImplementation((roomId) => {
|
||||
switch (roomId) {
|
||||
case room.roomId:
|
||||
return room;
|
||||
case space.roomId:
|
||||
return space;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
await setupAsyncStoreWithClient(SpaceStore.instance, client);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
SpaceStore.instance.setActiveSpace(MetaSpace.Home);
|
||||
await resetAsyncStoreWithClient(SpaceStore.instance);
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const viewRoom = (room: Room) =>
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>(
|
||||
{
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
metricsTrigger: undefined,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
const expectDispatch = async <T extends ActionPayload>(payload: T) => {
|
||||
const dispatcherSpy = jest.fn();
|
||||
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
||||
await sleep(0);
|
||||
expect(dispatcherSpy).toHaveBeenCalledWith(payload);
|
||||
defaultDispatcher.unregister(dispatcherRef);
|
||||
};
|
||||
|
||||
it("hangs up legacy calls when leaving a room", async () => {
|
||||
const hangupSpy = jest.spyOn(LegacyCallHandler.instance, "hangupOrReject").mockImplementation(() => {});
|
||||
|
||||
viewRoom(room);
|
||||
await leaveRoomBehaviour(client, room.roomId);
|
||||
|
||||
expect(hangupSpy).toHaveBeenCalledWith(room.roomId);
|
||||
});
|
||||
|
||||
it("disconnects widget-based calls when leaving a room", async () => {
|
||||
const mockCall = {
|
||||
disconnect: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as Call;
|
||||
|
||||
jest.spyOn(CallStore.instance, "getActiveCall").mockReturnValue(mockCall);
|
||||
|
||||
viewRoom(room);
|
||||
await leaveRoomBehaviour(client, room.roomId);
|
||||
|
||||
expect(mockCall.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns to the home page after leaving a room outside of a space that was being viewed", async () => {
|
||||
viewRoom(room);
|
||||
|
||||
await leaveRoomBehaviour(client, room.roomId);
|
||||
await expectDispatch({ action: Action.ViewHomePage });
|
||||
});
|
||||
|
||||
it("returns to the parent space after leaving a room inside of a space that was being viewed", async () => {
|
||||
jest.spyOn(SpaceStore.instance, "getCanonicalParent").mockImplementation((roomId) =>
|
||||
roomId === room.roomId ? space : null,
|
||||
);
|
||||
viewRoom(room);
|
||||
SpaceStore.instance.setActiveSpace(space.roomId, false);
|
||||
|
||||
await leaveRoomBehaviour(client, room.roomId);
|
||||
await expectDispatch({
|
||||
action: Action.ViewRoom,
|
||||
room_id: space.roomId,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns to the home page after leaving a top-level space that was being viewed", async () => {
|
||||
viewRoom(space);
|
||||
SpaceStore.instance.setActiveSpace(space.roomId, false);
|
||||
|
||||
await leaveRoomBehaviour(client, space.roomId);
|
||||
await expectDispatch({ action: Action.ViewHomePage });
|
||||
});
|
||||
|
||||
it("returns to the parent space after leaving a subspace that was being viewed", async () => {
|
||||
room.isSpaceRoom.mockReturnValue(true);
|
||||
jest.spyOn(SpaceStore.instance, "getCanonicalParent").mockImplementation((roomId) =>
|
||||
roomId === room.roomId ? space : null,
|
||||
);
|
||||
viewRoom(room);
|
||||
SpaceStore.instance.setActiveSpace(room.roomId, false);
|
||||
|
||||
await leaveRoomBehaviour(client, room.roomId);
|
||||
await expectDispatch({
|
||||
action: Action.ViewRoom,
|
||||
room_id: space.roomId,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
describe("If the feature_dynamic_room_predecessors is not enabled", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("Passes through the dynamic predecessor setting", async () => {
|
||||
await leaveRoomBehaviour(client, room.roomId);
|
||||
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, true, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("If the feature_dynamic_room_predecessors is enabled", () => {
|
||||
beforeEach(() => {
|
||||
// Turn on feature_dynamic_room_predecessors setting
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(settingName) => settingName === "feature_dynamic_room_predecessors",
|
||||
);
|
||||
});
|
||||
|
||||
it("Passes through the dynamic predecessor setting", async () => {
|
||||
await leaveRoomBehaviour(client, room.roomId);
|
||||
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, true, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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 { type MatrixClient, type Room, RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { type MockedObject } from "jest-mock-vitest-adapter";
|
||||
import { createRef } from "react";
|
||||
|
||||
import { mkRoom, stubClient } from "../../test-utils";
|
||||
import { WidgetPipViewModel } from "../../../src/viewmodels/room/WidgetPipViewModel";
|
||||
import WidgetStore, { type IApp } from "../../../src/stores/WidgetStore";
|
||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import { WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
|
||||
import { CallStore, CallStoreEvent } from "../../../src/stores/CallStore";
|
||||
import { type Call } from "../../../src/models/Call";
|
||||
|
||||
const userId = "@example:example.org";
|
||||
const widgetId = "test-widget-id";
|
||||
|
||||
type BackClickEvent = Parameters<WidgetPipViewModel["onBackClick"]>[0];
|
||||
|
||||
const createBackClickEvent = (): BackClickEvent =>
|
||||
({
|
||||
preventDefault: jest.fn(),
|
||||
stopPropagation: jest.fn(),
|
||||
}) as unknown as BackClickEvent;
|
||||
|
||||
describe("WidgetPipViewModel", () => {
|
||||
let client: MockedObject<MatrixClient>;
|
||||
let vm: WidgetPipViewModel;
|
||||
let room: MockedObject<Room>;
|
||||
let widget: IApp;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient() as MockedObject<MatrixClient>;
|
||||
room = mkRoom(client, "!example");
|
||||
widget = {
|
||||
id: widgetId,
|
||||
roomId: room.roomId,
|
||||
creatorUserId: userId,
|
||||
type: "m.custom",
|
||||
name: "Test Widget",
|
||||
data: {},
|
||||
} as unknown as IApp;
|
||||
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([widget]);
|
||||
|
||||
vm = new WidgetPipViewModel({
|
||||
room,
|
||||
widgetId,
|
||||
onStartMoving: () => {},
|
||||
movePersistedElement: createRef(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.dispose();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("updates room name", () => {
|
||||
room.name = "New Room Name";
|
||||
room.emit(RoomEvent.Name, room);
|
||||
expect(vm.getSnapshot().roomName).toBe("New Room Name");
|
||||
});
|
||||
|
||||
it("updates onBackClick if call changes", () => {
|
||||
const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch").mockImplementation(() => {});
|
||||
|
||||
vm.onBackClick(createBackClickEvent());
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
metricsTrigger: "WebFloatingCallWindow",
|
||||
});
|
||||
dispatchSpy.mockClear();
|
||||
|
||||
const call = { widget: { id: widgetId } } as unknown as Call;
|
||||
CallStore.instance.emit(CallStoreEvent.Call, call, room.roomId);
|
||||
|
||||
vm.onBackClick(createBackClickEvent());
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
view_call: true,
|
||||
metricsTrigger: "WebFloatingCallWindow",
|
||||
});
|
||||
});
|
||||
|
||||
it("updates onBackClick if viewingRoom changes", () => {
|
||||
const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch").mockImplementation(() => {});
|
||||
const moveSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer").mockImplementation(() => {});
|
||||
|
||||
vm.setViewingRoom(true);
|
||||
vm.onBackClick(createBackClickEvent());
|
||||
expect(moveSpy).toHaveBeenCalledWith(room, widget, "center");
|
||||
|
||||
moveSpy.mockClear();
|
||||
vm.setViewingRoom(false);
|
||||
vm.onBackClick(createBackClickEvent());
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
metricsTrigger: "WebFloatingCallWindow",
|
||||
});
|
||||
expect(moveSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user