Merge branch 'develop' into midhun/fix-spotlight-1

This commit is contained in:
Michael Telatynski 2024-11-13 15:38:56 +00:00 committed by GitHub
commit ec96d33ed7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
215 changed files with 3574 additions and 2704 deletions

View file

@ -351,7 +351,7 @@ describe("Notifier", () => {
user: mockClient.getSafeUserId(),
room: testRoom.roomId,
});
addReplyToMessageContent(reply.getContent(), event, { includeLegacyFallback: true });
addReplyToMessageContent(reply.getContent(), event);
Notifier.displayPopupNotification(reply, testRoom);
expect(MockPlatform.displayNotification).toHaveBeenCalledWith(
"@bob:example.org (!room1:server)",

View file

@ -6,42 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import {
IContent,
MatrixEvent,
MsgType,
M_BEACON_INFO,
LocationAssetType,
M_ASSET,
M_POLL_END,
Room,
} from "matrix-js-sdk/src/matrix";
import { Room } from "matrix-js-sdk/src/matrix";
import {
getNestedReplyText,
getParentEventId,
shouldDisplayReply,
stripHTMLReply,
stripPlainReply,
} from "../../src/utils/Reply";
import { makePollStartEvent, mkEvent, stubClient } from "../test-utils";
import { RoomPermalinkCreator } from "../../src/utils/permalinks/Permalinks";
function makeTestEvent(type: string, content: IContent): MatrixEvent {
return mkEvent({
event: true,
type: type,
user: "@user1:server",
room: "!room1:server",
content,
});
}
const mockPermalinkGenerator = {
forEvent(eventId: string): string {
return "$$permalink$$";
},
} as RoomPermalinkCreator;
import { getParentEventId, shouldDisplayReply, stripHTMLReply, stripPlainReply } from "../../src/utils/Reply";
import { mkEvent, stubClient } from "../test-utils";
// don't litter test console with logs
jest.mock("matrix-js-sdk/src/logger");
@ -122,50 +90,6 @@ But this is not
});
});
describe("getNestedReplyText", () => {
it("Returns valid reply fallback text for m.text msgtypes", () => {
const event = makeTestEvent(MsgType.Text, {
body: "body",
msgtype: "m.text",
});
expect(getNestedReplyText(event, mockPermalinkGenerator)).toMatchSnapshot();
});
(
[
["m.room.message", MsgType.Location, LocationAssetType.Pin],
["m.room.message", MsgType.Location, LocationAssetType.Self],
[M_BEACON_INFO.name, undefined, LocationAssetType.Pin],
[M_BEACON_INFO.name, undefined, LocationAssetType.Self],
] as const
).forEach(([type, msgType, assetType]) => {
it(`should create the expected fallback text for ${assetType} ${type}/${msgType}`, () => {
const event = makeTestEvent(type, {
body: "body",
msgtype: msgType,
[M_ASSET.name]: { type: assetType },
});
expect(getNestedReplyText(event, mockPermalinkGenerator)).toMatchSnapshot();
});
});
it("should create the expected fallback text for poll end events", () => {
const event = makeTestEvent(M_POLL_END.name, {
body: "body",
});
expect(getNestedReplyText(event, mockPermalinkGenerator)).toMatchSnapshot();
});
it("should create the expected fallback text for poll start events", () => {
const event = makePollStartEvent("Will this test pass?", "@user:server.org");
expect(getNestedReplyText(event, mockPermalinkGenerator)).toMatchSnapshot();
});
});
describe("shouldDisplayReply", () => {
it("Returns false for redacted events", () => {
const event = mkEvent({

View file

@ -11,6 +11,13 @@ import { CryptoApi } from "matrix-js-sdk/src/crypto-api";
import { accessSecretStorage } from "../../src/SecurityManager";
import { filterConsole, stubClient } from "../test-utils";
import Modal from "../../src/Modal.tsx";
jest.mock("react", () => {
const React = jest.requireActual("react");
React.lazy = (children: any) => children(); // stub out lazy for dialog test
return React;
});
describe("SecurityManager", () => {
describe("accessSecretStorage", () => {
@ -50,5 +57,21 @@ describe("SecurityManager", () => {
}).rejects.toThrow("End-to-end encryption is disabled - unable to access secret storage");
});
});
it("should show CreateSecretStorageDialog if forceReset=true", async () => {
jest.mock("../../src/async-components/views/dialogs/security/CreateSecretStorageDialog", () => ({
__test: true,
__esModule: true,
default: () => jest.fn(),
}));
const spy = jest.spyOn(Modal, "createDialog");
stubClient();
const func = jest.fn();
accessSecretStorage(func, true);
expect(spy).toHaveBeenCalledTimes(1);
await expect(spy.mock.lastCall![0]).resolves.toEqual(expect.objectContaining({ __test: true }));
});
});
});

View file

@ -62,18 +62,18 @@ describe("SupportedBrowser", () => {
);
it.each([
// Safari 17.5 on macOS Sonoma
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
// Firefox 129 on macOS Sonoma
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:129.0) Gecko/20100101 Firefox/129.0",
// Safari 18.0 on macOS Sonoma
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
// Firefox 131 on macOS Sonoma
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:131.0) Gecko/20100101 Firefox/131.0",
// Edge 129 on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/129.0.2792.79",
// Edge 129 on macOS
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/129.0.2792.79",
// Firefox 129 on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0",
// Firefox 129 on Linux
"Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:129.0) Gecko/20100101 Firefox/129.0",
// Firefox 131 on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0",
// Firefox 131 on Linux
"Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0",
// Chrome 130 on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
])("should not warn for supported browsers", testUserAgentFactory());

View file

@ -1,64 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Reply getNestedReplyText Returns valid reply fallback text for m.text msgtypes 1`] = `
{
"body": "> <@user1:server> body
",
"html": "<mx-reply><blockquote><a href="$$permalink$$">In reply to</a> <a href="https://matrix.to/#/@user1:server">@user1:server</a><br>body</blockquote></mx-reply>",
}
`;
exports[`Reply getNestedReplyText should create the expected fallback text for m.pin m.room.message/m.location 1`] = `
{
"body": "> <@user1:server> shared a location.
",
"html": "<mx-reply><blockquote><a href="$$permalink$$">In reply to</a> <a href="https://matrix.to/#/@user1:server">@user1:server</a><br>shared a location.</blockquote></mx-reply>",
}
`;
exports[`Reply getNestedReplyText should create the expected fallback text for m.pin org.matrix.msc3672.beacon_info/undefined 1`] = `
{
"body": "> <@user1:server> shared a live location.
",
"html": "<mx-reply><blockquote><a href="$$permalink$$">In reply to</a> <a href="https://matrix.to/#/@user1:server">@user1:server</a><br>shared a live location.</blockquote></mx-reply>",
}
`;
exports[`Reply getNestedReplyText should create the expected fallback text for m.self m.room.message/m.location 1`] = `
{
"body": "> <@user1:server> shared their location.
",
"html": "<mx-reply><blockquote><a href="$$permalink$$">In reply to</a> <a href="https://matrix.to/#/@user1:server">@user1:server</a><br>shared their location.</blockquote></mx-reply>",
}
`;
exports[`Reply getNestedReplyText should create the expected fallback text for m.self org.matrix.msc3672.beacon_info/undefined 1`] = `
{
"body": "> <@user1:server> shared their live location.
",
"html": "<mx-reply><blockquote><a href="$$permalink$$">In reply to</a> <a href="https://matrix.to/#/@user1:server">@user1:server</a><br>shared their live location.</blockquote></mx-reply>",
}
`;
exports[`Reply getNestedReplyText should create the expected fallback text for poll end events 1`] = `
{
"body": "> <@user1:server>Ended poll
",
"html": "<mx-reply><blockquote><a href="$$permalink$$">In reply to</a> <a href="https://matrix.to/#/@user1:server">@user1:server</a><br>Ended poll</blockquote></mx-reply>",
}
`;
exports[`Reply getNestedReplyText should create the expected fallback text for poll start events 1`] = `
{
"body": "> <@user:server.org> started poll: Will this test pass?
",
"html": "<mx-reply><blockquote><a href="$$permalink$$">In reply to</a> <a href="https://matrix.to/#/@user:server.org">@user:server.org</a><br>Poll: Will this test pass?</blockquote></mx-reply>",
}
`;

View file

@ -0,0 +1,33 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen, fireEvent, waitFor } from "jest-matrix-react";
import RecoveryMethodRemovedDialog from "../../../../../src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog";
import Modal from "../../../../../src/Modal.tsx";
describe("<RecoveryMethodRemovedDialog />", () => {
afterEach(() => {
jest.restoreAllMocks();
});
it("should open CreateKeyBackupDialog on primary action click", async () => {
const onFinished = jest.fn();
const spy = jest.spyOn(Modal, "createDialog");
jest.mock("../../../../../src/async-components/views/dialogs/security/CreateKeyBackupDialog", () => ({
__test: true,
__esModule: true,
default: () => <span>mocked dialog</span>,
}));
render(<RecoveryMethodRemovedDialog onFinished={onFinished} />);
fireEvent.click(screen.getByRole("button", { name: "Set up Secure Messages" }));
await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
expect((spy.mock.lastCall![0] as any)._payload._result).toEqual(expect.objectContaining({ __test: true }));
});
});

View file

@ -149,6 +149,7 @@ describe("<MatrixChat />", () => {
isRoomEncrypted: jest.fn(),
logout: jest.fn(),
getDeviceId: jest.fn(),
getKeyBackupVersion: jest.fn().mockResolvedValue(null),
});
let mockClient: Mocked<MatrixClient>;
const serverConfig = {
@ -1515,7 +1516,7 @@ describe("<MatrixChat />", () => {
describe("when key backup failed", () => {
it("should show the new recovery method dialog", async () => {
const spy = jest.spyOn(Modal, "createDialogAsync");
const spy = jest.spyOn(Modal, "createDialog");
jest.mock("../../../../src/async-components/views/dialogs/security/NewRecoveryMethodDialog", () => ({
__test: true,
__esModule: true,
@ -1530,7 +1531,25 @@ describe("<MatrixChat />", () => {
await flushPromises();
mockClient.emit(CryptoEvent.KeyBackupFailed, "error code");
await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
expect(await spy.mock.lastCall![0]).toEqual(expect.objectContaining({ __test: true }));
expect((spy.mock.lastCall![0] as any)._payload._result).toEqual(expect.objectContaining({ __test: true }));
});
it("should show the recovery method removed dialog", async () => {
const spy = jest.spyOn(Modal, "createDialog");
jest.mock("../../../../src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog", () => ({
__test: true,
__esModule: true,
default: () => <span>mocked dialog</span>,
}));
getComponent({});
defaultDispatcher.dispatch({
action: "will_start_client",
});
await flushPromises();
mockClient.emit(CryptoEvent.KeyBackupFailed, "error code");
await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
expect((spy.mock.lastCall![0] as any)._payload._result).toEqual(expect.objectContaining({ __test: true }));
});
});
});

View file

@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { render, screen, waitFor } from "jest-matrix-react";
import { jest } from "@jest/globals";
import { mocked, MockedObject } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/matrix";

View file

@ -42,7 +42,7 @@ import {
} from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { Action } from "../../../../src/dispatcher/actions";
import dis, { defaultDispatcher } from "../../../../src/dispatcher/dispatcher";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import { ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload";
import { RoomView as _RoomView } from "../../../../src/components/structures/RoomView";
import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
@ -527,7 +527,7 @@ describe("RoomView", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_ask_to_join");
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
jest.spyOn(dis, "dispatch");
jest.spyOn(defaultDispatcher, "dispatch");
});
it("allows to request to join", async () => {
@ -536,9 +536,9 @@ describe("RoomView", () => {
await mountRoomView();
fireEvent.click(screen.getByRole("button", { name: "Request access" }));
await untilDispatch(Action.SubmitAskToJoin, dis);
await untilDispatch(Action.SubmitAskToJoin, defaultDispatcher);
expect(dis.dispatch).toHaveBeenCalledWith({
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
action: "submit_ask_to_join",
roomId: room.roomId,
opts: { reason: undefined },
@ -552,9 +552,12 @@ describe("RoomView", () => {
await mountRoomView();
fireEvent.click(screen.getByRole("button", { name: "Cancel request" }));
await untilDispatch(Action.CancelAskToJoin, dis);
await untilDispatch(Action.CancelAskToJoin, defaultDispatcher);
expect(dis.dispatch).toHaveBeenCalledWith({ action: "cancel_ask_to_join", roomId: room.roomId });
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
action: "cancel_ask_to_join",
roomId: room.roomId,
});
});
});
@ -669,7 +672,7 @@ describe("RoomView", () => {
await waitFor(() => {
expect(container.querySelector(".mx_RoomView_searchResultsPanel")).toBeVisible();
});
const prom = untilDispatch(Action.ViewRoom, dis);
const prom = untilDispatch(Action.ViewRoom, defaultDispatcher);
await userEvent.hover(getByText("search term"));
await userEvent.click(await findByLabelText("Edit"));
@ -678,8 +681,8 @@ describe("RoomView", () => {
});
it("fires Action.RoomLoaded", async () => {
jest.spyOn(dis, "dispatch");
jest.spyOn(defaultDispatcher, "dispatch");
await mountRoomView();
expect(dis.dispatch).toHaveBeenCalledWith({ action: Action.RoomLoaded });
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: Action.RoomLoaded });
});
});

View file

@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { render } from "jest-matrix-react";
import { jest } from "@jest/globals";
import { Room } from "matrix-js-sdk/src/matrix";
import { stubClient } from "../../../test-utils";

View file

@ -10,7 +10,7 @@ import React from "react";
import { mocked, MockedObject } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { CryptoApi, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import { render, RenderResult } from "jest-matrix-react";
import { fireEvent, render, RenderResult, screen } from "jest-matrix-react";
import { filterConsole, getMockClientWithEventEmitter, mockClientMethodsCrypto } from "../../../../test-utils";
import LogoutDialog from "../../../../../src/components/views/dialogs/LogoutDialog";
@ -61,6 +61,9 @@ describe("LogoutDialog", () => {
const rendered = renderComponent();
await rendered.findByText("Start using Key Backup");
expect(rendered.container).toMatchSnapshot();
fireEvent.click(await screen.findByRole("button", { name: "Manually export keys" }));
await expect(screen.findByRole("heading", { name: "Export room keys" })).resolves.toBeInTheDocument();
});
describe("when there is an error fetching backups", () => {

View file

@ -33,7 +33,6 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = `
>
Room ID: !id
<div
aria-describedby=":r2:"
aria-label="Copy"
class="mx_AccessibleButton mx_CopyableText_copyButton"
role="button"

View file

@ -9,6 +9,8 @@
import React from "react";
import { screen, render, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
// Needed to be able to mock decodeRecoveryKey
// eslint-disable-next-line no-restricted-imports
import * as recoveryKeyModule from "matrix-js-sdk/src/crypto-api/recovery-key";
@ -17,9 +19,16 @@ import RestoreKeyBackupDialog from "../../../../../../src/components/views/dialo
import { stubClient } from "../../../../../test-utils";
describe("<RestoreKeyBackupDialog />", () => {
const keyBackupRestoreResult = {
total: 2,
imported: 1,
};
let matrixClient: MatrixClient;
beforeEach(() => {
stubClient();
matrixClient = stubClient();
jest.spyOn(recoveryKeyModule, "decodeRecoveryKey").mockReturnValue(new Uint8Array(32));
jest.spyOn(matrixClient, "getKeyBackupVersion").mockResolvedValue({ version: "1" } as KeyBackupInfo);
});
it("should render", async () => {
@ -48,4 +57,71 @@ describe("<RestoreKeyBackupDialog />", () => {
await waitFor(() => expect(screen.getByText("👍 This looks like a valid Security Key!")).toBeInTheDocument());
expect(asFragment()).toMatchSnapshot();
});
it("should restore key backup when the key is cached", async () => {
jest.spyOn(matrixClient.getCrypto()!, "restoreKeyBackup").mockResolvedValue(keyBackupRestoreResult);
const { asFragment } = render(<RestoreKeyBackupDialog onFinished={jest.fn()} />);
await waitFor(() => expect(screen.getByText("Successfully restored 1 keys")).toBeInTheDocument());
expect(asFragment()).toMatchSnapshot();
});
it("should restore key backup when the key is in secret storage", async () => {
jest.spyOn(matrixClient.getCrypto()!, "restoreKeyBackup")
// Reject when trying to restore from cache
.mockRejectedValueOnce(new Error("key backup not found"))
// Resolve when trying to restore from secret storage
.mockResolvedValue(keyBackupRestoreResult);
jest.spyOn(matrixClient.secretStorage, "hasKey").mockResolvedValue(true);
jest.spyOn(matrixClient, "isKeyBackupKeyStored").mockResolvedValue({});
const { asFragment } = render(<RestoreKeyBackupDialog onFinished={jest.fn()} />);
await waitFor(() => expect(screen.getByText("Successfully restored 1 keys")).toBeInTheDocument());
expect(asFragment()).toMatchSnapshot();
});
it("should restore key backup when security key is filled by user", async () => {
jest.spyOn(matrixClient.getCrypto()!, "restoreKeyBackup")
// Reject when trying to restore from cache
.mockRejectedValueOnce(new Error("key backup not found"))
// Resolve when trying to restore from recovery key
.mockResolvedValue(keyBackupRestoreResult);
const { asFragment } = render(<RestoreKeyBackupDialog onFinished={jest.fn()} />);
await waitFor(() => expect(screen.getByText("Enter Security Key")).toBeInTheDocument());
await userEvent.type(screen.getByRole("textbox"), "my security key");
await userEvent.click(screen.getByRole("button", { name: "Next" }));
await waitFor(() => expect(screen.getByText("Successfully restored 1 keys")).toBeInTheDocument());
expect(asFragment()).toMatchSnapshot();
});
test("should restore key backup when passphrase is filled", async () => {
// Determine that the passphrase is required
jest.spyOn(matrixClient, "getKeyBackupVersion").mockResolvedValue({
version: "1",
auth_data: {
private_key_salt: "salt",
private_key_iterations: 1,
},
} as KeyBackupInfo);
jest.spyOn(matrixClient.getCrypto()!, "restoreKeyBackup")
// Reject when trying to restore from cache
.mockRejectedValue(new Error("key backup not found"));
jest.spyOn(matrixClient.getCrypto()!, "restoreKeyBackupWithPassphrase").mockResolvedValue(
keyBackupRestoreResult,
);
const { asFragment } = render(<RestoreKeyBackupDialog onFinished={jest.fn()} />);
await waitFor(() => expect(screen.getByText("Enter Security Phrase")).toBeInTheDocument());
// Not role for password https://github.com/w3c/aria/issues/935
await userEvent.type(screen.getByTestId("passphraseInput"), "my passphrase");
await userEvent.click(screen.getByRole("button", { name: "Next" }));
await waitFor(() => expect(screen.getByText("Successfully restored 1 keys")).toBeInTheDocument());
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -296,3 +296,263 @@ exports[`<RestoreKeyBackupDialog /> should render 1`] = `
/>
</DocumentFragment>
`;
exports[`<RestoreKeyBackupDialog /> should restore key backup when passphrase is filled 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Keys restored
</h1>
</div>
<div
class="mx_RestoreKeyBackupDialog_content"
>
<div>
<p>
Successfully restored 1 keys
</p>
<p>
Failed to decrypt 1 sessions!
</p>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
OK
</button>
</span>
</div>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`<RestoreKeyBackupDialog /> should restore key backup when security key is filled by user 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Keys restored
</h1>
</div>
<div
class="mx_RestoreKeyBackupDialog_content"
>
<div>
<p>
Successfully restored 1 keys
</p>
<p>
Failed to decrypt 1 sessions!
</p>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
OK
</button>
</span>
</div>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`<RestoreKeyBackupDialog /> should restore key backup when the key is cached 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Keys restored
</h1>
</div>
<div
class="mx_RestoreKeyBackupDialog_content"
>
<div>
<p>
Successfully restored 1 keys
</p>
<p>
Failed to decrypt 1 sessions!
</p>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
OK
</button>
</span>
</div>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`<RestoreKeyBackupDialog /> should restore key backup when the key is in secret storage 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Keys restored
</h1>
</div>
<div
class="mx_RestoreKeyBackupDialog_content"
>
<div>
<p>
Successfully restored 1 keys
</p>
<p>
Failed to decrypt 1 sessions!
</p>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
OK
</button>
</span>
</div>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View file

@ -7,13 +7,11 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { jest } from "@jest/globals";
import { Room, MatrixClient } from "matrix-js-sdk/src/matrix";
import { ClientWidgetApi, IWidget, MatrixWidgetType } from "matrix-widget-api";
import { Optional } from "matrix-events-sdk";
import { act, render, RenderResult } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { SpiedFunction } from "jest-mock";
import {
ApprovalOpts,
WidgetInfo,
@ -345,7 +343,7 @@ describe("AppTile", () => {
describe("for a pinned widget", () => {
let renderResult: RenderResult;
let moveToContainerSpy: SpiedFunction<typeof WidgetLayoutStore.instance.moveToContainer>;
let moveToContainerSpy: jest.SpyInstance<void, [room: Room, widget: IWidget, toContainer: Container]>;
beforeEach(() => {
renderResult = render(

View file

@ -7,13 +7,57 @@
*/
import React from "react";
import { render } from "jest-matrix-react";
import { mocked } from "jest-mock";
import { render, fireEvent, waitFor } from "jest-matrix-react";
import fetchMock from "fetch-mock-jest";
import ImageView from "../../../../../src/components/views/elements/ImageView";
import { FileDownloader } from "../../../../../src/utils/FileDownloader";
import Modal from "../../../../../src/Modal";
import ErrorDialog from "../../../../../src/components/views/dialogs/ErrorDialog";
jest.mock("../../../../../src/utils/FileDownloader");
describe("<ImageView />", () => {
beforeEach(() => {
jest.resetAllMocks();
fetchMock.reset();
});
it("renders correctly", () => {
const { container } = render(<ImageView src="https://example.com/image.png" onFinished={jest.fn()} />);
expect(container).toMatchSnapshot();
});
it("should download on click", async () => {
fetchMock.get("https://example.com/image.png", "TESTFILE");
const { getByRole } = render(
<ImageView src="https://example.com/image.png" name="filename.png" onFinished={jest.fn()} />,
);
fireEvent.click(getByRole("button", { name: "Download" }));
await waitFor(() =>
expect(mocked(FileDownloader).mock.instances[0].download).toHaveBeenCalledWith({
blob: expect.anything(),
name: "filename.png",
}),
);
expect(fetchMock).toHaveFetched("https://example.com/image.png");
});
it("should handle download errors", async () => {
const modalSpy = jest.spyOn(Modal, "createDialog");
fetchMock.get("https://example.com/image.png", { status: 500 });
const { getByRole } = render(
<ImageView src="https://example.com/image.png" name="filename.png" onFinished={jest.fn()} />,
);
fireEvent.click(getByRole("button", { name: "Download" }));
await waitFor(() =>
expect(modalSpy).toHaveBeenCalledWith(
ErrorDialog,
expect.objectContaining({
title: "Download failed",
}),
),
);
});
});

View file

@ -356,8 +356,8 @@ exports[`AppTile for a pinned widget should render permission request 1`] = `
<span>
Using this widget may share data
<div
aria-describedby=":r2n:"
aria-labelledby=":r2m:"
aria-describedby=":r2j:"
aria-labelledby=":r2i:"
class="mx_TextWithTooltip_target mx_TextWithTooltip_target--helpIcon"
>
<svg

View file

@ -21,7 +21,6 @@ exports[`<ImageView /> renders correctly 1`] = `
class="mx_ImageView_toolbar"
>
<div
aria-describedby=":r2:"
aria-label="Zoom out"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_zoomOut"
role="button"

View file

@ -23,7 +23,6 @@ exports[`<LocationViewDialog /> renders map correctly 1`] = `
class="mx_ZoomButtons"
>
<div
aria-describedby=":r2:"
aria-label="Zoom in"
class="mx_AccessibleButton mx_ZoomButtons_button"
data-testid="map-zoom-in-button"

View file

@ -6,9 +6,18 @@ exports[`<MapError /> applies class when isMinimised is truthy 1`] = `
class="mx_MapError test mx_MapError_isMinimised"
data-testid="map-rendering-error"
>
<div
<svg
class="mx_MapError_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
<h3
class="mx_Heading_h3 mx_MapError_heading"
>
@ -36,9 +45,18 @@ exports[`<MapError /> renders correctly for MapStyleUrlNotConfigured 1`] = `
class="mx_MapError test"
data-testid="map-rendering-error"
>
<div
<svg
class="mx_MapError_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
<h3
class="mx_Heading_h3 mx_MapError_heading"
>
@ -66,9 +84,18 @@ exports[`<MapError /> renders correctly for MapStyleUrlNotReachable 1`] = `
class="mx_MapError test"
data-testid="map-rendering-error"
>
<div
<svg
class="mx_MapError_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
<h3
class="mx_Heading_h3 mx_MapError_heading"
>

View file

@ -91,6 +91,12 @@ describe("DateSeparator", () => {
expect(getComponent({ ts, forExport: false }).container.textContent).toEqual(result);
});
it("renders invalid date separator correctly", () => {
const ts = new Date(-8640000000000004).getTime();
const { asFragment } = getComponent({ ts });
expect(asFragment()).toMatchSnapshot();
});
describe("when forExport is true", () => {
it.each(testCases)("formats date in full when current time is %s", (_d, ts) => {
expect(getComponent({ ts, forExport: true }).container.textContent).toEqual(

View file

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, RenderResult } from "jest-matrix-react";
import { fireEvent, render, RenderResult, waitFor } from "jest-matrix-react";
import {
MatrixEvent,
Relations,
@ -83,7 +83,7 @@ describe("MPollBody", () => {
expect(votesCount(renderResult, "poutine")).toBe("");
expect(votesCount(renderResult, "italian")).toBe("");
expect(votesCount(renderResult, "wings")).toBe("");
expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("No votes cast");
await waitFor(() => expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("No votes cast"));
expect(renderResult.getByText("What should we order for the party?")).toBeTruthy();
});

View file

@ -33,6 +33,16 @@ jest.mock("../../../../../src/components/views/messages/MImageBody", () => ({
default: () => <div data-testid="image-body" />,
}));
jest.mock("../../../../../src/components/views/messages/MVideoBody", () => ({
__esModule: true,
default: () => <div data-testid="video-body" />,
}));
jest.mock("../../../../../src/components/views/messages/MFileBody", () => ({
__esModule: true,
default: () => <div data-testid="file-body" />,
}));
jest.mock("../../../../../src/components/views/messages/MImageReplyBody", () => ({
__esModule: true,
default: () => <div data-testid="image-reply-body" />,
@ -95,8 +105,8 @@ describe("MessageEvent", () => {
describe("when an image with a caption is sent", () => {
let result: RenderResult;
beforeEach(() => {
event = mkEvent({
function createEvent(mimetype: string, filename: string, msgtype: string) {
return mkEvent({
event: true,
type: EventType.RoomMessage,
user: client.getUserId()!,
@ -105,19 +115,19 @@ describe("MessageEvent", () => {
body: "caption for a test image",
format: "org.matrix.custom.html",
formatted_body: "<strong>caption for a test image</strong>",
msgtype: MsgType.Image,
filename: "image.webp",
msgtype: msgtype,
filename: filename,
info: {
w: 40,
h: 50,
mimetype: mimetype,
},
url: "mxc://server/image",
},
});
result = renderMessageEvent();
});
}
it("should render a TextualBody and an ImageBody", () => {
function mockMedia() {
fetchMock.getOnce(
"https://server/_matrix/media/v3/download/server/image",
{
@ -125,8 +135,38 @@ describe("MessageEvent", () => {
},
{ sendAsJson: false },
);
}
it("should render a TextualBody and an ImageBody", () => {
event = createEvent("image/webp", "image.webp", MsgType.Image);
result = renderMessageEvent();
mockMedia();
result.getByTestId("image-body");
result.getByTestId("textual-body");
});
it("should render a TextualBody and a FileBody for mismatched extension", () => {
event = createEvent("image/webp", "image.exe", MsgType.Image);
result = renderMessageEvent();
mockMedia();
result.getByTestId("file-body");
result.getByTestId("textual-body");
});
it("should render a TextualBody and an VideoBody", () => {
event = createEvent("video/mp4", "video.mp4", MsgType.Video);
result = renderMessageEvent();
mockMedia();
result.getByTestId("video-body");
result.getByTestId("textual-body");
});
it("should render a TextualBody and a FileBody for non-video mimetype", () => {
event = createEvent("application/octet-stream", "video.mp4", MsgType.Video);
result = renderMessageEvent();
mockMedia();
result.getByTestId("file-body");
result.getByTestId("textual-body");
});
});
});

View file

@ -1,5 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DateSeparator renders invalid date separator correctly 1`] = `
<DocumentFragment>
<div
aria-label="Invalid timestamp"
class="mx_TimelineSeparator"
role="separator"
>
<hr
role="none"
/>
<div
class="mx_DateSeparator_dateContent"
>
<h2
aria-hidden="true"
class="mx_DateSeparator_dateHeading"
>
Invalid timestamp
</h2>
</div>
<hr
role="none"
/>
</div>
</DocumentFragment>
`;
exports[`DateSeparator renders the date separator correctly 1`] = `
<DocumentFragment>
<div

View file

@ -5,9 +5,18 @@ exports[`<MBeaconBody /> when map display is not configured renders maps unavail
class="mx_MapError mx_MBeaconBody_mapError mx_MBeaconBody_mapErrorInteractive mx_MapError_isMinimised"
data-testid="map-rendering-error"
>
<div
<svg
class="mx_MapError_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
<h3
class="mx_Heading_h3 mx_MapError_heading"
>

View file

@ -23,7 +23,7 @@ import * as settingsHooks from "../../../../../src/hooks/useSettings";
import Modal from "../../../../../src/Modal";
import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelStore";
import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases";
import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils";
import { flushPromises, stubClient } from "../../../../test-utils";
import { PollHistoryDialog } from "../../../../../src/components/views/dialogs/PollHistoryDialog";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
import { _t } from "../../../../../src/languageHandler";
@ -56,16 +56,7 @@ describe("<RoomSummaryCard />", () => {
};
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
getAccountData: jest.fn(),
isRoomEncrypted: jest.fn(),
getOrCreateFilter: jest.fn().mockResolvedValue({ filterId: 1 }),
getRoom: jest.fn(),
isGuest: jest.fn().mockReturnValue(false),
deleteRoomTag: jest.fn().mockResolvedValue({}),
setRoomTag: jest.fn().mockResolvedValue({}),
});
mockClient = mocked(stubClient());
room = new Room(roomId, mockClient, userId);
const roomCreateEvent = new MatrixEvent({
type: "m.room.create",

View file

@ -134,6 +134,7 @@ beforeEach(() => {
getUserDeviceInfo: jest.fn(),
userHasCrossSigningKeys: jest.fn().mockResolvedValue(false),
getUserVerificationStatus: jest.fn(),
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false),
} as unknown as CryptoApi);
mockClient = mocked({
@ -148,7 +149,6 @@ beforeEach(() => {
on: jest.fn(),
off: jest.fn(),
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
isRoomEncrypted: jest.fn().mockReturnValue(false),
doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false),
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false),
getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")),
@ -660,7 +660,7 @@ describe("<UserInfo />", () => {
describe("with an encrypted room", () => {
beforeEach(() => {
mockClient.isRoomEncrypted.mockReturnValue(true);
jest.spyOn(mockClient.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
});
it("renders unverified user info", async () => {

View file

@ -27,7 +27,6 @@ import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import DocumentOffset from "../../../../../src/editor/offset";
import { Layout } from "../../../../../src/settings/enums/Layout";
import { IRoomState, MainSplitContentType } from "../../../../../src/components/structures/RoomView";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
import { mockPlatformPeg } from "../../../../test-utils/platform";
import { doMaybeLocalRoomAction } from "../../../../../src/utils/local-room";
import { addTextToComposer } from "../../../../test-utils/composer";
@ -80,14 +79,12 @@ describe("<SendMessageComposer/>", () => {
viewRoomOpts: { buttons: [] },
};
describe("createMessageContent", () => {
const permalinkCreator = jest.fn() as any;
it("sends plaintext messages correctly", () => {
const model = new EditorModel([], createPartCreator());
const documentOffset = new DocumentOffset(11, true);
model.update("hello world", "insertText", documentOffset);
const content = createMessageContent("@alice:test", model, undefined, undefined, permalinkCreator);
const content = createMessageContent("@alice:test", model, undefined, undefined);
expect(content).toEqual({
"body": "hello world",
@ -101,7 +98,7 @@ describe("<SendMessageComposer/>", () => {
const documentOffset = new DocumentOffset(13, true);
model.update("hello *world*", "insertText", documentOffset);
const content = createMessageContent("@alice:test", model, undefined, undefined, permalinkCreator);
const content = createMessageContent("@alice:test", model, undefined, undefined);
expect(content).toEqual({
"body": "hello *world*",
@ -117,7 +114,7 @@ describe("<SendMessageComposer/>", () => {
const documentOffset = new DocumentOffset(22, true);
model.update("/me blinks __quickly__", "insertText", documentOffset);
const content = createMessageContent("@alice:test", model, undefined, undefined, permalinkCreator);
const content = createMessageContent("@alice:test", model, undefined, undefined);
expect(content).toEqual({
"body": "blinks __quickly__",
@ -134,7 +131,7 @@ describe("<SendMessageComposer/>", () => {
model.update("/me ✨sparkles✨", "insertText", documentOffset);
expect(model.parts.length).toEqual(4); // Emoji count as non-text
const content = createMessageContent("@alice:test", model, undefined, undefined, permalinkCreator);
const content = createMessageContent("@alice:test", model, undefined, undefined);
expect(content).toEqual({
"body": "✨sparkles✨",
@ -149,7 +146,7 @@ describe("<SendMessageComposer/>", () => {
model.update("//dev/null is my favourite place", "insertText", documentOffset);
const content = createMessageContent("@alice:test", model, undefined, undefined, permalinkCreator);
const content = createMessageContent("@alice:test", model, undefined, undefined);
expect(content).toEqual({
"body": "/dev/null is my favourite place",
@ -364,7 +361,6 @@ describe("<SendMessageComposer/>", () => {
const defaultProps = {
room: mockRoom,
toggleStickerPickerOpen: jest.fn(),
permalinkCreator: new RoomPermalinkCreator(mockRoom),
};
const getRawComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => (
<MatrixClientContext.Provider value={client}>
@ -482,6 +478,44 @@ describe("<SendMessageComposer/>", () => {
});
});
it("correctly sends a reply using a slash command", async () => {
stubClient();
mocked(doMaybeLocalRoomAction).mockImplementation(
<T,>(roomId: string, fn: (actualRoomId: string) => Promise<T>, _client?: MatrixClient) => {
return fn(roomId);
},
);
const replyToEvent = mkEvent({
type: "m.room.message",
user: "@bob:test",
room: "!abc:test",
content: { "m.mentions": {} },
event: true,
});
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });
const { container } = getComponent({ replyToEvent });
addTextToComposer(container, "/tableflip");
fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" });
await waitFor(() =>
expect(mockClient.sendMessage).toHaveBeenCalledWith("myfakeroom", null, {
"body": "(╯°□°)╯︵ ┻━┻",
"msgtype": MsgType.Text,
"m.mentions": {
user_ids: ["@bob:test"],
},
"m.relates_to": {
"m.in_reply_to": {
event_id: replyToEvent.getId(),
},
},
}),
);
});
it("shows chat effects on message sending", () => {
mocked(doMaybeLocalRoomAction).mockImplementation(
<T,>(roomId: string, fn: (actualRoomId: string) => Promise<T>, _client?: MatrixClient) => {

View file

@ -15,7 +15,6 @@ import VoiceRecordComposerTile from "../../../../../src/components/views/rooms/V
import { doMaybeLocalRoomAction } from "../../../../../src/utils/local-room";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { IUpload, VoiceMessageRecording } from "../../../../../src/audio/VoiceMessageRecording";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
import { VoiceRecordingStore } from "../../../../../src/stores/VoiceRecordingStore";
import { PlaybackClock } from "../../../../../src/audio/PlaybackClock";
import { mkEvent } from "../../../../test-utils";
@ -57,7 +56,6 @@ describe("<VoiceRecordComposerTile/>", () => {
const props = {
room,
ref: voiceRecordComposerTile,
permalinkCreator: new RoomPermalinkCreator(room),
};
mockUpload = {
mxc: "mxc://example.com/voice",
@ -142,7 +140,6 @@ describe("<VoiceRecordComposerTile/>", () => {
const props = {
room,
ref: voiceRecordComposerTile,
permalinkCreator: new RoomPermalinkCreator(room),
replyToEvent,
};
render(<VoiceRecordComposerTile {...props} />);

View file

@ -8,26 +8,13 @@ Please see LICENSE files in the repository root for full details.
import { MsgType } from "matrix-js-sdk/src/matrix";
import { filterConsole, mkEvent } from "../../../../../../test-utils";
import { RoomPermalinkCreator } from "../../../../../../../src/utils/permalinks/Permalinks";
import {
createMessageContent,
EMOTE_PREFIX,
} from "../../../../../../../src/components/views/rooms/wysiwyg_composer/utils/createMessageContent";
describe("createMessageContent", () => {
const permalinkCreator = {
forEvent(eventId: string): string {
return "$$permalink$$";
},
} as RoomPermalinkCreator;
const message = "<em><b>hello</b> world</em>";
const mockEvent = mkEvent({
type: "m.room.message",
room: "myfakeroom",
user: "myfakeuser",
content: { msgtype: "m.text", body: "Replying to this" },
event: true,
});
afterEach(() => {
jest.resetAllMocks();
@ -42,12 +29,12 @@ describe("createMessageContent", () => {
// Warm up by creating the component once, with a long timeout.
// This prevents tests timing out because of the time spent loading
// the WASM component.
await createMessageContent(message, true, { permalinkCreator });
await createMessageContent(message, true, {});
}, 10000);
it("Should create html message", async () => {
// When
const content = await createMessageContent(message, true, { permalinkCreator });
const content = await createMessageContent(message, true, {});
// Then
expect(content).toEqual({
@ -58,34 +45,13 @@ describe("createMessageContent", () => {
});
});
it("Should add reply to message content", async () => {
// When
const content = await createMessageContent(message, true, { permalinkCreator, replyToEvent: mockEvent });
// Then
expect(content).toEqual({
"body": "> <myfakeuser> Replying to this\n\n*__hello__ world*",
"format": "org.matrix.custom.html",
"formatted_body":
'<mx-reply><blockquote><a href="$$permalink$$">In reply to</a>' +
' <a href="https://matrix.to/#/myfakeuser">myfakeuser</a>' +
"<br>Replying to this</blockquote></mx-reply><em><b>hello</b> world</em>",
"msgtype": "m.text",
"m.relates_to": {
"m.in_reply_to": {
event_id: mockEvent.getId(),
},
},
});
});
it("Should add relation to message", async () => {
// When
const relation = {
rel_type: "m.thread",
event_id: "myFakeThreadId",
};
const content = await createMessageContent(message, true, { permalinkCreator, relation });
const content = await createMessageContent(message, true, { relation });
// Then
expect(content).toEqual({
@ -118,7 +84,7 @@ describe("createMessageContent", () => {
},
event: true,
});
const content = await createMessageContent(message, true, { permalinkCreator, editedEvent });
const content = await createMessageContent(message, true, { editedEvent });
// Then
expect(content).toEqual({
@ -141,20 +107,20 @@ describe("createMessageContent", () => {
it("Should strip the /me prefix from a message", async () => {
const textBody = "some body text";
const content = await createMessageContent(EMOTE_PREFIX + textBody, true, { permalinkCreator });
const content = await createMessageContent(EMOTE_PREFIX + textBody, true, {});
expect(content).toMatchObject({ body: textBody, formatted_body: textBody });
});
it("Should strip single / from message prefixed with //", async () => {
const content = await createMessageContent("//twoSlashes", true, { permalinkCreator });
const content = await createMessageContent("//twoSlashes", true, {});
expect(content).toMatchObject({ body: "/twoSlashes", formatted_body: "/twoSlashes" });
});
it("Should set the content type to MsgType.Emote when /me prefix is used", async () => {
const textBody = "some body text";
const content = await createMessageContent(EMOTE_PREFIX + textBody, true, { permalinkCreator });
const content = await createMessageContent(EMOTE_PREFIX + textBody, true, {});
expect(content).toMatchObject({ msgtype: MsgType.Emote });
});
@ -164,14 +130,14 @@ describe("createMessageContent", () => {
it("Should replace at-room mentions with `@room` in body", async () => {
const messageComposerState = `<a href="#" contenteditable="false" data-mention-type="at-room" style="some styling">@room</a> `;
const content = await createMessageContent(messageComposerState, false, { permalinkCreator });
const content = await createMessageContent(messageComposerState, false, {});
expect(content).toMatchObject({ body: "@room " });
});
it("Should replace user mentions with user name in body", async () => {
const messageComposerState = `<a href="https://matrix.to/#/@test_user:element.io" contenteditable="false" data-mention-type="user" style="some styling">a test user</a> `;
const content = await createMessageContent(messageComposerState, false, { permalinkCreator });
const content = await createMessageContent(messageComposerState, false, {});
expect(content).toMatchObject({ body: "a test user " });
});
@ -179,7 +145,7 @@ describe("createMessageContent", () => {
it("Should replace room mentions with room mxid in body", async () => {
const messageComposerState = `<a href="https://matrix.to/#/#test_room:element.io" contenteditable="false" data-mention-type="room" style="some styling">a test room</a> `;
const content = await createMessageContent(messageComposerState, false, { permalinkCreator });
const content = await createMessageContent(messageComposerState, false, {});
expect(content).toMatchObject({
body: "#test_room:element.io ",

View file

@ -17,7 +17,6 @@ import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../
import defaultDispatcher from "../../../../../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../../../src/settings/SettingLevel";
import { RoomPermalinkCreator } from "../../../../../../../src/utils/permalinks/Permalinks";
import EditorStateTransfer from "../../../../../../../src/utils/EditorStateTransfer";
import * as ConfirmRedactDialog from "../../../../../../../src/components/views/dialogs/ConfirmRedactDialog";
import * as SlashCommands from "../../../../../../../src/SlashCommands";
@ -27,11 +26,6 @@ import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
import { Action } from "../../../../../../../src/dispatcher/actions";
describe("message", () => {
const permalinkCreator = {
forEvent(eventId: string): string {
return "$$permalink$$";
},
} as RoomPermalinkCreator;
const message = "<i><b>hello</b> world</i>";
const mockEvent = mkEvent({
type: "m.room.message",
@ -71,7 +65,7 @@ describe("message", () => {
describe("sendMessage", () => {
it("Should not send empty html message", async () => {
// When
await sendMessage("", true, { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator });
await sendMessage("", true, { roomContext: defaultRoomContext, mxClient: mockClient });
// Then
expect(mockClient.sendMessage).toHaveBeenCalledTimes(0);
@ -86,7 +80,6 @@ describe("message", () => {
await sendMessage(message, true, {
roomContext: mockRoomContextWithoutId,
mxClient: mockClient,
permalinkCreator,
});
// Then
@ -100,7 +93,6 @@ describe("message", () => {
await sendMessage(message, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
});
// Then
@ -111,7 +103,6 @@ describe("message", () => {
await sendMessage(message, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
relation: {},
});
@ -123,7 +114,6 @@ describe("message", () => {
await sendMessage(message, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
relation: {
event_id: "valid_id",
rel_type: "m.does_not_match",
@ -139,7 +129,6 @@ describe("message", () => {
await sendMessage(message, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
relation: {
event_id: "valid_id",
rel_type: "m.thread",
@ -156,7 +145,6 @@ describe("message", () => {
await sendMessage(message, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
});
// Then
@ -183,7 +171,6 @@ describe("message", () => {
await sendMessage(message, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
replyToEvent: mockReplyEvent,
});
@ -195,12 +182,9 @@ describe("message", () => {
});
const expectedContent = {
"body": "> <myfakeuser2> My reply\n\n*__hello__ world*",
"body": "*__hello__ world*",
"format": "org.matrix.custom.html",
"formatted_body":
'<mx-reply><blockquote><a href="$$permalink$$">In reply to</a>' +
' <a href="https://matrix.to/#/myfakeuser2">myfakeuser2</a>' +
"<br>My reply</blockquote></mx-reply><i><b>hello</b> world</i>",
"formatted_body": "<i><b>hello</b> world</i>",
"msgtype": "m.text",
"m.relates_to": {
"m.in_reply_to": {
@ -217,7 +201,6 @@ describe("message", () => {
await sendMessage(message, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
});
// Then
@ -229,7 +212,7 @@ describe("message", () => {
it("Should handle emojis", async () => {
// When
await sendMessage("🎉", false, { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator });
await sendMessage("🎉", false, { roomContext: defaultRoomContext, mxClient: mockClient });
// Then
expect(spyDispatcher).toHaveBeenCalledWith({ action: "effects.confetti" });
@ -244,7 +227,6 @@ describe("message", () => {
await sendMessage(validCommand, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
});
// Then
@ -257,7 +239,6 @@ describe("message", () => {
await sendMessage(invalidPrefixCommand, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
});
// Then
@ -275,7 +256,6 @@ describe("message", () => {
const result = await sendMessage(validCommand, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
});
// Then
@ -290,7 +270,6 @@ describe("message", () => {
await sendMessage(inputText, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
});
expect(mockClient.sendMessage).toHaveBeenCalledWith(
"myfakeroom",
@ -309,7 +288,6 @@ describe("message", () => {
await sendMessage(inputText, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
relation: mockRelation,
});
@ -326,7 +304,6 @@ describe("message", () => {
await sendMessage("input", true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
replyToEvent: mockEvent,
});
@ -341,7 +318,6 @@ describe("message", () => {
const result = await sendMessage(input, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
replyToEvent: mockEvent,
});
@ -357,7 +333,6 @@ describe("message", () => {
await sendMessage(invalidCommandInput, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
});
// we expect the message to have been sent
@ -378,7 +353,6 @@ describe("message", () => {
const result = await sendMessage(invalidCommandInput, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
});
expect(result).toBeUndefined();

View file

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import { render, screen, waitFor } from "jest-matrix-react";
import { MatrixClient, ThreepidMedium } from "matrix-js-sdk/src/matrix";
import { MatrixClient, MatrixError, ThreepidMedium } from "matrix-js-sdk/src/matrix";
import React from "react";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
@ -16,6 +16,7 @@ import { AddRemoveThreepids } from "../../../../../src/components/views/settings
import { clearAllModals, stubClient } from "../../../../test-utils";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import Modal from "../../../../../src/Modal";
import InteractiveAuthDialog from "../../../../../src/components/views/dialogs/InteractiveAuthDialog.tsx";
const MOCK_IDENTITY_ACCESS_TOKEN = "mock_identity_access_token";
const mockGetAccessToken = jest.fn().mockResolvedValue(MOCK_IDENTITY_ACCESS_TOKEN);
@ -222,13 +223,13 @@ describe("AddRemoveThreepids", () => {
const continueButton = await screen.findByRole("button", { name: /Continue/ });
await expect(continueButton).toHaveAttribute("aria-disabled", "true");
expect(continueButton).toHaveAttribute("aria-disabled", "true");
await expect(
await screen.findByText(
screen.findByText(
`A text message has been sent to +${PHONE1.address}. Please enter the verification code it contains.`,
),
).toBeInTheDocument();
).resolves.toBeInTheDocument();
expect(client.requestAdd3pidMsisdnToken).toHaveBeenCalledWith(
"GB",
@ -481,4 +482,118 @@ describe("AddRemoveThreepids", () => {
expect(client.unbindThreePid).toHaveBeenCalledWith(ThreepidMedium.Phone, PHONE1.address);
expect(onChangeFn).toHaveBeenCalled();
});
it("should show UIA dialog when necessary for adding email", async () => {
const onChangeFn = jest.fn();
const createDialogFn = jest.spyOn(Modal, "createDialog");
mocked(client.requestAdd3pidEmailToken).mockResolvedValue({ sid: "1" });
render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Email}
threepids={[]}
isLoading={false}
onChange={onChangeFn}
/>,
{
wrapper: clientProviderWrapper,
},
);
const input = screen.getByRole("textbox", { name: "Email Address" });
await userEvent.type(input, EMAIL1.address);
const addButton = screen.getByRole("button", { name: "Add" });
await userEvent.click(addButton);
const continueButton = screen.getByRole("button", { name: "Continue" });
expect(continueButton).toBeEnabled();
mocked(client).addThreePidOnly.mockRejectedValueOnce(
new MatrixError({ errcode: "M_UNAUTHORIZED", flows: [{ stages: [] }] }, 401),
);
await userEvent.click(continueButton);
expect(createDialogFn).toHaveBeenCalledWith(
InteractiveAuthDialog,
expect.objectContaining({
title: "Add Email Address",
makeRequest: expect.any(Function),
}),
);
});
it("should show UIA dialog when necessary for adding msisdn", async () => {
const onChangeFn = jest.fn();
const createDialogFn = jest.spyOn(Modal, "createDialog");
mocked(client.requestAdd3pidMsisdnToken).mockResolvedValue({
sid: "1",
msisdn: PHONE1.address,
intl_fmt: PHONE1.address,
success: true,
submit_url: "https://some-url",
});
render(
<AddRemoveThreepids
mode="hs"
medium={ThreepidMedium.Phone}
threepids={[]}
isLoading={false}
onChange={onChangeFn}
/>,
{
wrapper: clientProviderWrapper,
},
);
const countryDropdown = screen.getByRole("button", { name: /Country Dropdown/ });
await userEvent.click(countryDropdown);
const gbOption = screen.getByRole("option", { name: "🇬🇧 United Kingdom (+44)" });
await userEvent.click(gbOption);
const input = screen.getByRole("textbox", { name: "Phone Number" });
await userEvent.type(input, PHONE1_LOCALNUM);
const addButton = screen.getByRole("button", { name: "Add" });
await userEvent.click(addButton);
const continueButton = screen.getByRole("button", { name: "Continue" });
expect(continueButton).toHaveAttribute("aria-disabled", "true");
await expect(
screen.findByText(
`A text message has been sent to +${PHONE1.address}. Please enter the verification code it contains.`,
),
).resolves.toBeInTheDocument();
expect(client.requestAdd3pidMsisdnToken).toHaveBeenCalledWith(
"GB",
PHONE1_LOCALNUM,
client.generateClientSecret(),
1,
);
const verificationInput = screen.getByRole("textbox", { name: "Verification code" });
await userEvent.type(verificationInput, "123456");
expect(continueButton).not.toHaveAttribute("aria-disabled", "true");
mocked(client).addThreePidOnly.mockRejectedValueOnce(
new MatrixError({ errcode: "M_UNAUTHORIZED", flows: [{ stages: [] }] }, 401),
);
await userEvent.click(continueButton);
expect(createDialogFn).toHaveBeenCalledWith(
InteractiveAuthDialog,
expect.objectContaining({
title: "Add Phone Number",
makeRequest: expect.any(Function),
}),
);
});
});

View file

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, waitFor } from "jest-matrix-react";
import { render, waitFor, screen, fireEvent } from "jest-matrix-react";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
@ -64,4 +64,34 @@ describe("CryptographyPanel", () => {
// Then "not supported key
await waitFor(() => expect(codes[1].innerHTML).toEqual("<strong>&lt;not supported&gt;</strong>"));
});
it("should open the export e2e keys dialog on click", async () => {
const sessionId = "ABCDEFGHIJ";
const sessionKey = "AbCDeFghIJK7L/m4nOPqRSTUVW4xyzaBCDef6gHIJkl";
TestUtils.stubClient();
const client: MatrixClient = MatrixClientPeg.safeGet();
client.deviceId = sessionId;
mocked(client.getCrypto()!.getOwnDeviceKeys).mockResolvedValue({ ed25519: sessionKey, curve25519: "1234" });
render(<CryptographyPanel />, withClientContextRenderOptions(client));
fireEvent.click(await screen.findByRole("button", { name: "Export E2E room keys" }));
await expect(screen.findByRole("heading", { name: "Export room keys" })).resolves.toBeInTheDocument();
});
it("should open the import e2e keys dialog on click", async () => {
const sessionId = "ABCDEFGHIJ";
const sessionKey = "AbCDeFghIJK7L/m4nOPqRSTUVW4xyzaBCDef6gHIJkl";
TestUtils.stubClient();
const client: MatrixClient = MatrixClientPeg.safeGet();
client.deviceId = sessionId;
mocked(client.getCrypto()!.getOwnDeviceKeys).mockResolvedValue({ ed25519: sessionKey, curve25519: "1234" });
render(<CryptographyPanel />, withClientContextRenderOptions(client));
fireEvent.click(await screen.findByRole("button", { name: "Import E2E room keys" }));
await expect(screen.findByRole("heading", { name: "Import room keys" })).resolves.toBeInTheDocument();
});
});

View file

@ -59,7 +59,7 @@ describe("<JoinRuleSettings />", () => {
onError: jest.fn(),
};
const getComponent = (props: Partial<JoinRuleSettingsProps> = {}) =>
render(<JoinRuleSettings {...defaultProps} {...props} />);
render(<JoinRuleSettings {...defaultProps} {...props} />, { legacyRoot: false });
const setRoomStateEvents = (
room: Room,

View file

@ -130,10 +130,8 @@ describe("<SecureBackupPanel />", () => {
})
.mockResolvedValue(null);
getComponent();
// flush checkKeyBackup promise
await flushPromises();
fireEvent.click(screen.getByText("Delete Backup"));
fireEvent.click(await screen.findByText("Delete Backup"));
const dialog = await screen.findByRole("dialog");

View file

@ -263,9 +263,18 @@ exports[`<CurrentDeviceSection /> renders device and correct security card when
role="button"
tabindex="0"
>
<div
<svg
class="mx_DeviceExpandDetailsButton_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212l-4.6-4.6a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l3.9 3.9 3.9-3.9a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7l-4.6 4.6c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
</div>
</div>
</div>
@ -416,9 +425,18 @@ exports[`<CurrentDeviceSection /> renders device and correct security card when
role="button"
tabindex="0"
>
<div
<svg
class="mx_DeviceExpandDetailsButton_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212l-4.6-4.6a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l3.9 3.9 3.9-3.9a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7l-4.6 4.6c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
</div>
</div>
</div>

View file

@ -9,9 +9,18 @@ exports[`<DeviceExpandDetailsButton /> renders when expanded 1`] = `
role="button"
tabindex="0"
>
<div
<svg
class="mx_DeviceExpandDetailsButton_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212l-4.6-4.6a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l3.9 3.9 3.9-3.9a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7l-4.6 4.6c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
</div>
</div>,
}
@ -26,9 +35,18 @@ exports[`<DeviceExpandDetailsButton /> renders when not expanded 1`] = `
role="button"
tabindex="0"
>
<div
<svg
class="mx_DeviceExpandDetailsButton_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212l-4.6-4.6a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l3.9 3.9 3.9-3.9a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7l-4.6 4.6c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
</div>
</div>,
}

View file

@ -12,7 +12,7 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import userEvent from "@testing-library/user-event";
import DiscoverySettings from "../../../../../../src/components/views/settings/discovery/DiscoverySettings";
import { DiscoverySettings } from "../../../../../../src/components/views/settings/discovery/DiscoverySettings";
import { stubClient } from "../../../../../test-utils";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import { UIFeature } from "../../../../../../src/settings/UIFeature";

View file

@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { render } from "jest-matrix-react";
import SettingsSubsection from "../../../../../../src/components/views/settings/shared/SettingsSubsection";
import { SettingsSubsection } from "../../../../../../src/components/views/settings/shared/SettingsSubsection";
describe("<SettingsSubsection />", () => {
const defaultProps = {

View file

@ -163,9 +163,18 @@ exports[`<SessionManagerTab /> current session section renders current session s
role="button"
tabindex="0"
>
<div
<svg
class="mx_DeviceExpandDetailsButton_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212l-4.6-4.6a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l3.9 3.9 3.9-3.9a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7l-4.6 4.6c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
</div>
</div>
</div>
@ -302,9 +311,18 @@ exports[`<SessionManagerTab /> current session section renders current session s
role="button"
tabindex="0"
>
<div
<svg
class="mx_DeviceExpandDetailsButton_icon"
/>
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212l-4.6-4.6a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l3.9 3.9 3.9-3.9a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7l-4.6 4.6c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
</div>
</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 460 B

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="19px" height="19px" viewBox="0 0 19 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 39.1 (31720) - http://www.bohemiancoding.com/sketch -->
<title>ED5D3E59-2561-4AC1-9B43-82FBC51767FC</title>
<desc>Created with sketchtool.</desc>
<defs></defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon_context">
<g>
<path d="M9.5,19 C14.7467051,19 19,14.7467051 19,9.5 C19,4.25329488 14.7467051,0 9.5,0 C4.25329488,0 0,4.25329488 0,9.5 C0,14.7467051 4.25329488,19 9.5,19 Z" id="Oval-69" fill="#ECECEC"></path>
<path d="M4.5,9.50063771 C4.5,9.13148623 4.59887838,8.85242947 4.7966381,8.66345907 C4.99439782,8.47448867 5.28224377,8.38000488 5.66018457,8.38000488 C6.0249414,8.38000488 6.3072941,8.47668596 6.50725115,8.67005103 C6.70720821,8.86341609 6.80718523,9.14027555 6.80718523,9.50063771 C6.80718523,9.84781589 6.70610956,10.1213794 6.50395517,10.3213365 C6.30180079,10.5212935 6.02054674,10.6212705 5.66018457,10.6212705 C5.29103309,10.6212705 5.00538444,10.5234908 4.80323006,10.3279284 C4.60107568,10.132366 4.5,9.85660521 4.5,9.50063771 L4.5,9.50063771 Z M8.3431114,9.50063771 C8.3431114,9.13148623 8.44198978,8.85242947 8.63974951,8.66345907 C8.83750923,8.47448867 9.12755247,8.38000488 9.50988794,8.38000488 C9.87464476,8.38000488 10.1569975,8.47668596 10.3569545,8.67005103 C10.5569116,8.86341609 10.6568886,9.14027555 10.6568886,9.50063771 C10.6568886,9.84781589 10.5558129,10.1213794 10.3536585,10.3213365 C10.1515042,10.5212935 9.8702501,10.6212705 9.50988794,10.6212705 C9.13634179,10.6212705 8.84849585,10.5234908 8.64634146,10.3279284 C8.44418708,10.132366 8.3431114,9.85660521 8.3431114,9.50063771 L8.3431114,9.50063771 Z M12.1928148,9.50063771 C12.1928148,9.13148623 12.2916931,8.85242947 12.4894529,8.66345907 C12.6872126,8.47448867 12.9750585,8.38000488 13.3529993,8.38000488 C13.7177562,8.38000488 14.0001089,8.47668596 14.2000659,8.67005103 C14.400023,8.86341609 14.5,9.14027555 14.5,9.50063771 C14.5,9.84781589 14.3989243,10.1213794 14.1967699,10.3213365 C13.9946156,10.5212935 13.7133615,10.6212705 13.3529993,10.6212705 C12.9838479,10.6212705 12.6981992,10.5234908 12.4960448,10.3279284 C12.2938904,10.132366 12.1928148,9.85660521 12.1928148,9.50063771 L12.1928148,9.50063771 Z" id="…" fill="#9B9B9B"></path>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -13,7 +13,7 @@ import { createTestClient, flushPromises, setupAsyncStoreWithClient } from "../.
import SettingsStore from "../../../src/settings/SettingsStore";
import { BreadcrumbsStore } from "../../../src/stores/BreadcrumbsStore";
import { Action } from "../../../src/dispatcher/actions";
import { defaultDispatcher } from "../../../src/dispatcher/dispatcher";
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
describe("BreadcrumbsStore", () => {
let store: BreadcrumbsStore;

View file

@ -8,7 +8,14 @@ Please see LICENSE files in the repository root for full details.
import { mocked, MockedObject } from "jest-mock";
import { last } from "lodash";
import { MatrixEvent, MatrixClient, ClientEvent, EventTimeline } from "matrix-js-sdk/src/matrix";
import {
MatrixEvent,
MatrixClient,
ClientEvent,
EventTimeline,
EventType,
MatrixEventEvent,
} from "matrix-js-sdk/src/matrix";
import { ClientWidgetApi, WidgetApiFromWidgetAction } from "matrix-widget-api";
import { waitFor } from "jest-matrix-react";
@ -134,6 +141,46 @@ describe("StopGapWidget", () => {
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent(), "!1:example.org");
});
it("feeds decrypted events asynchronously", async () => {
const event1Encrypted = new MatrixEvent({
event_id: event1.getId(),
type: EventType.RoomMessageEncrypted,
sender: event1.sender?.userId,
room_id: event1.getRoomId(),
content: {},
});
const decryptingSpy1 = jest.spyOn(event1Encrypted, "isBeingDecrypted").mockReturnValue(true);
client.emit(ClientEvent.Event, event1Encrypted);
const event2Encrypted = new MatrixEvent({
event_id: event2.getId(),
type: EventType.RoomMessageEncrypted,
sender: event2.sender?.userId,
room_id: event2.getRoomId(),
content: {},
});
const decryptingSpy2 = jest.spyOn(event2Encrypted, "isBeingDecrypted").mockReturnValue(true);
client.emit(ClientEvent.Event, event2Encrypted);
expect(messaging.feedEvent).not.toHaveBeenCalled();
// "Decrypt" the events, but in reverse order; first event 2…
event2Encrypted.event.type = event2.getType();
event2Encrypted.event.content = event2.getContent();
decryptingSpy2.mockReturnValue(false);
client.emit(MatrixEventEvent.Decrypted, event2Encrypted);
expect(messaging.feedEvent).toHaveBeenCalledTimes(1);
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2Encrypted.getEffectiveEvent(), "!1:example.org");
// …then event 1
event1Encrypted.event.type = event1.getType();
event1Encrypted.event.content = event1.getContent();
decryptingSpy1.mockReturnValue(false);
client.emit(MatrixEventEvent.Decrypted, event1Encrypted);
// The events should be fed in that same order so that event 2
// doesn't have to be blocked on the decryption of event 1 (or
// worse, dropped)
expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event1Encrypted.getEffectiveEvent(), "!1:example.org");
});
it("should not feed incoming event if not in timeline", () => {
const event = mkEvent({
event: true,

View file

@ -0,0 +1,22 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import MatrixChat from "../../../src/components/structures/MatrixChat.tsx";
import { isLoggedIn } from "../../../src/utils/login.ts";
import Views from "../../../src/Views.ts";
describe("isLoggedIn", () => {
it("should return true if MatrixChat state view is LOGGED_IN", () => {
window.matrixChat = {
state: {
view: Views.LOGGED_IN,
},
} as unknown as MatrixChat;
expect(isLoggedIn()).toBe(true);
});
});

View file

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import { act, render } from "jest-matrix-react";
import { MatrixEvent, ConditionKind, EventType, PushRuleActionName, Room, TweakName } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
@ -15,6 +15,7 @@ import { pillifyLinks } from "../../../src/utils/pillify";
import { stubClient } from "../../test-utils";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import DMRoomMap from "../../../src/utils/DMRoomMap";
import { ReactRootManager } from "../../../src/utils/react.tsx";
describe("pillify", () => {
const roomId = "!room:id";
@ -84,51 +85,55 @@ describe("pillify", () => {
it("should do nothing for empty element", () => {
const { container } = render(<div />);
const originalHtml = container.outerHTML;
const containers: Element[] = [];
const containers = new ReactRootManager();
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
expect(containers).toHaveLength(0);
expect(containers.elements).toHaveLength(0);
expect(container.outerHTML).toEqual(originalHtml);
});
it("should pillify @room", () => {
const { container } = render(<div>@room</div>);
const containers: Element[] = [];
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
expect(containers).toHaveLength(1);
const containers = new ReactRootManager();
act(() => pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers));
expect(containers.elements).toHaveLength(1);
expect(container.querySelector(".mx_Pill.mx_AtRoomPill")?.textContent).toBe("!@room");
});
it("should pillify @room in an intentional mentions world", () => {
mocked(MatrixClientPeg.safeGet().supportsIntentionalMentions).mockReturnValue(true);
const { container } = render(<div>@room</div>);
const containers: Element[] = [];
pillifyLinks(
MatrixClientPeg.safeGet(),
[container],
new MatrixEvent({
room_id: roomId,
type: EventType.RoomMessage,
content: {
"body": "@room",
"m.mentions": {
room: true,
const containers = new ReactRootManager();
act(() =>
pillifyLinks(
MatrixClientPeg.safeGet(),
[container],
new MatrixEvent({
room_id: roomId,
type: EventType.RoomMessage,
content: {
"body": "@room",
"m.mentions": {
room: true,
},
},
},
}),
containers,
}),
containers,
),
);
expect(containers).toHaveLength(1);
expect(containers.elements).toHaveLength(1);
expect(container.querySelector(".mx_Pill.mx_AtRoomPill")?.textContent).toBe("!@room");
});
it("should not double up pillification on repeated calls", () => {
const { container } = render(<div>@room</div>);
const containers: Element[] = [];
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
expect(containers).toHaveLength(1);
const containers = new ReactRootManager();
act(() => {
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
});
expect(containers.elements).toHaveLength(1);
expect(container.querySelector(".mx_Pill.mx_AtRoomPill")?.textContent).toBe("!@room");
});
});

View file

@ -12,6 +12,7 @@ import { act, render } from "jest-matrix-react";
import { tooltipifyLinks } from "../../../src/utils/tooltipify";
import PlatformPeg from "../../../src/PlatformPeg";
import BasePlatform from "../../../src/BasePlatform";
import { ReactRootManager } from "../../../src/utils/react.tsx";
describe("tooltipify", () => {
jest.spyOn(PlatformPeg, "get").mockReturnValue({ needsUrlTooltips: () => true } as unknown as BasePlatform);
@ -19,9 +20,9 @@ describe("tooltipify", () => {
it("does nothing for empty element", () => {
const { container: root } = render(<div />);
const originalHtml = root.outerHTML;
const containers: Element[] = [];
const containers = new ReactRootManager();
tooltipifyLinks([root], [], containers);
expect(containers).toHaveLength(0);
expect(containers.elements).toHaveLength(0);
expect(root.outerHTML).toEqual(originalHtml);
});
@ -31,9 +32,9 @@ describe("tooltipify", () => {
<a href="/foo">click</a>
</div>,
);
const containers: Element[] = [];
const containers = new ReactRootManager();
tooltipifyLinks([root], [], containers);
expect(containers).toHaveLength(1);
expect(containers.elements).toHaveLength(1);
const anchor = root.querySelector("a");
expect(anchor?.getAttribute("href")).toEqual("/foo");
const tooltip = anchor!.querySelector(".mx_TextWithTooltip_target");
@ -47,9 +48,9 @@ describe("tooltipify", () => {
</div>,
);
const originalHtml = root.outerHTML;
const containers: Element[] = [];
const containers = new ReactRootManager();
tooltipifyLinks([root], [root.children[0]], containers);
expect(containers).toHaveLength(0);
expect(containers.elements).toHaveLength(0);
expect(root.outerHTML).toEqual(originalHtml);
});
@ -59,12 +60,12 @@ describe("tooltipify", () => {
<a href="/foo">click</a>
</div>,
);
const containers: Element[] = [];
const containers = new ReactRootManager();
tooltipifyLinks([root], [], containers);
tooltipifyLinks([root], [], containers);
tooltipifyLinks([root], [], containers);
tooltipifyLinks([root], [], containers);
expect(containers).toHaveLength(1);
expect(containers.elements).toHaveLength(1);
const anchor = root.querySelector("a");
expect(anchor?.getAttribute("href")).toEqual("/foo");
const tooltip = anchor!.querySelector(".mx_TextWithTooltip_target");

View file

@ -0,0 +1,261 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`showError should match snapshot 1`] = `
<div
id="matrixchat"
>
<div
class="mx_ErrorView cpd-theme-light"
>
<img
alt="Element"
class="mx_ErrorView_logo"
height="160"
src="themes/element/img/logos/element-app-logo.png"
/>
<div
class="mx_ErrorView_container"
>
<h1
class="_typography_yh5dq_162 _font-heading-md-semibold_yh5dq_121"
>
Error title
</h1>
<p
class="_typography_yh5dq_162 _font-body-lg-regular_yh5dq_78"
>
msg1
</p>
<p
class="_typography_yh5dq_162 _font-body-lg-regular_yh5dq_78"
>
msg2
</p>
</div>
</div>
</div>
`;
exports[`showIncompatibleBrowser should match snapshot 1`] = `
<div
id="matrixchat"
>
<div
class="mx_ErrorView cpd-theme-light"
>
<img
alt="Element"
class="mx_ErrorView_logo"
height="160"
src="themes/element/img/logos/element-app-logo.png"
/>
<div
class="mx_ErrorView_container"
>
<h1
class="_typography_yh5dq_162 _font-heading-md-semibold_yh5dq_121"
>
Element does not support this browser
</h1>
<p
class="_typography_yh5dq_162 _font-body-lg-regular_yh5dq_78"
>
Element uses some browser features which are not available in your current browser. If you continue, some features may stop working and there is a risk that you may lose data in the future.
</p>
<p
class="_typography_yh5dq_162 _font-body-lg-regular_yh5dq_78"
>
<span>
For the best experience, use
<a
href="https://google.com/chrome"
rel="noreferrer noopener"
target="_blank"
>
Chrome
</a>
,
<a
href="https://firefox.com"
rel="noreferrer noopener"
target="_blank"
>
Firefox
</a>
,
<a
href="https://microsoft.com/edge"
rel="noreferrer noopener"
target="_blank"
>
Edge
</a>
, or
<a
href="https://apple.com/safari"
rel="noreferrer noopener"
target="_blank"
>
Safari
</a>
.
</span>
</p>
<div
class="mx_Flex mx_ErrorView_buttons"
>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
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-2Z"
/>
<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-2Z"
/>
</svg>
Learn more
</button>
<button
class="_button_i91xf_17"
data-kind="primary"
data-size="sm"
role="button"
tabindex="0"
>
Continue anyway
</button>
</div>
</div>
<div
class="_separator_144s5_17"
data-kind="primary"
data-orientation="horizontal"
role="separator"
/>
<h2
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Use Element Desktop instead
</h2>
<div
class="mx_Flex"
>
<a
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="secondary"
data-size="lg"
href="https://packages.element.io/desktop/install/macos/Element.dmg"
role="link"
tabindex="0"
>
<div
aria-hidden="true"
height="20"
width="20"
/>
Mac
</a>
<a
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="secondary"
data-size="lg"
href="https://packages.element.io/desktop/install/win32/x64/Element%20Setup.exe"
role="link"
tabindex="0"
>
<div
aria-hidden="true"
height="20"
width="20"
/>
Windows (64-bit)
</a>
<a
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="secondary"
data-size="lg"
href="https://packages.element.io/desktop/install/win32/ia32/Element%20Setup.exe"
role="link"
tabindex="0"
>
<div
aria-hidden="true"
height="20"
width="20"
/>
Windows (32-bit)
</a>
<a
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="secondary"
data-size="lg"
href="https://element.io/download#linux"
role="link"
tabindex="0"
>
<div
aria-hidden="true"
height="20"
width="20"
/>
Linux
</a>
</div>
<h2
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
>
Or use our mobile app
</h2>
<div
class="mx_Flex"
>
<a
href="https://apps.apple.com/app/vector/id1083446067"
rel="noreferrer noopener"
target="_blank"
>
<img
alt="Apple App Store"
height="64"
src="themes/element/img/download/apple.svg"
/>
</a>
<a
href="https://play.google.com/store/apps/details?id=im.vector.app"
rel="noreferrer noopener"
target="_blank"
>
<img
alt="Google Play Store"
height="64"
src="themes/element/img/download/google.svg"
/>
</a>
<a
href="https://f-droid.org/repository/browse/?fdid=im.vector.app"
rel="noreferrer noopener"
target="_blank"
>
<img
alt="F-Droid"
height="64"
src="themes/element/img/download/fdroid.svg"
/>
</a>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,33 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { showError, showIncompatibleBrowser } from "../../../src/vector/init.tsx";
function setUpMatrixChatDiv() {
document.getElementById("matrixchat")?.remove();
const div = document.createElement("div");
div.id = "matrixchat";
document.body.appendChild(div);
}
describe("showIncompatibleBrowser", () => {
beforeEach(setUpMatrixChatDiv);
it("should match snapshot", async () => {
await showIncompatibleBrowser(jest.fn());
expect(document.getElementById("matrixchat")).toMatchSnapshot();
});
});
describe("showError", () => {
beforeEach(setUpMatrixChatDiv);
it("should match snapshot", async () => {
await showError("Error title", ["msg1", "msg2"]);
expect(document.getElementById("matrixchat")).toMatchSnapshot();
});
});

View file

@ -6,7 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { getInitialScreenAfterLogin, onNewScreen } from "../../../src/vector/routing";
import { getInitialScreenAfterLogin, init, onNewScreen } from "../../../src/vector/routing";
import MatrixChat from "../../../src/components/structures/MatrixChat.tsx";
describe("onNewScreen", () => {
it("should replace history if stripping via fields", () => {
@ -95,3 +96,27 @@ describe("getInitialScreenAfterLogin", () => {
});
});
});
describe("init", () => {
afterAll(() => {
// @ts-ignore
delete window.matrixChat;
});
it("should call showScreen on MatrixChat on hashchange", () => {
Object.defineProperty(window, "location", {
value: {
hash: "#/room/!room:server?via=abc",
},
});
window.matrixChat = {
showScreen: jest.fn(),
} as unknown as MatrixChat;
init();
window.dispatchEvent(new HashChangeEvent("hashchange"));
expect(window.matrixChat.showScreen).toHaveBeenCalledWith("room/!room:server", { via: "abc" });
});
});